URL
date
AI summary
slug
status
tags
summary
type

Docker端口映射的实现机制

引言

在学习dubbo的过程中,我们了解到了dubbo优雅停机的流程:
  1. provider会先从注册中心把自己下线
  1. consumer监听到provider下线的通知,会主动和provider所在的server断开连接
于是我想看看当前我们某个server上到底有多少个连接。不过在观察连接的过程中触及了一些知识盲区,所以记录总结一下。

宿主机上的连接之谜

宿主机上的连接

由于该dubbo服务在宿主机上映射的端口是23090,我们用netstat -anp | grep ":23090"看看宿主机上的连接情况:
[root@prod-app0001 ~]# netstat -anp | grep ":23090" tcp6 0 0 :::23090 :::* LISTEN 20954/docker-proxy tcp6 0 0 10.20.1.1:23090 172.17.0.8:58882 ESTABLISHED 20954/docker-proxy tcp6 0 0 10.20.1.1:23090 172.17.0.11:37568 ESTABLISHED 20954/docker-proxy
这里的连接数量明显和实际情况不符,并且对端IP都比较奇怪,因为我们自身的网段规划是10.20.0.0/16。通过docker network inspect bridge看到这些IP应该都是bridge网络分配给对应容器的。另外还有一点比较奇怪的是,这些连接都来自于同一个进程——docker-proxy。从进程名称来看应该和docker相关。从当前的信息只能看到这里,接着我们进入第一个容器看看。
我们的应用都是跑在docker里的,并且network用的是默认的bridge模式,所以在宿主机上并未找到我们期望看到的连接。因为在bridge模式下,容器的网络被隔离在自己的网络命名空间中。

容器里的连接

进入容器,我们用netstat -anp | grep "172.17.0.7:20880"看一下容器内的连接情况(容器内的dubbo监听的端口是20880,172.17.0.7是该容器分配到的IP)。172.17.0.1是网关,也就是容器内访问宿主机的ip地址。为了更好的展示,下面我用两个命令把连接分成了两类:
  1. 目的ip为172.17.0.1,也就是容器和宿主机的连接
  1. 和其他宿主机上的连接(其实最终也都是宿主机上运行的各种容器的连接)

容器和宿主机的连接

这个是同宿主机上其他容器和20880建立的连接,172.17.0.1是gateway,也就是容器和宿主机通信的ip
[root@47383be10098 apps]# netstat -anp | grep "172.17.0.7:20880" | grep '172.17.0.1' tcp 0 0 172.17.0.7:20880 172.17.0.1:58504 ESTABLISHED 1/java tcp 0 0 172.17.0.7:20880 172.17.0.1:47808 ESTABLISHED 1/java

容器和其他宿主机的连接

这个是容器和不同宿主机上运行的其他容器建立的连接
[root@47383be10098 apps]# netstat -anp | grep "172.17.0.7:20880" | grep '172.17.0.1' -v tcp 0 0 172.17.0.7:20880 10.20.1.2:50208 ESTABLISHED 1/java tcp 0 0 172.17.0.7:20880 10.20.1.158:59002 ESTABLISHED 1/java tcp 0 0 172.17.0.7:20880 10.20.1.6:56668 ESTABLISHED 1/java tcp 0 0 172.17.0.7:20880 10.20.1.5:41880 ESTABLISHED 1/java tcp 0 0 172.17.0.7:20880 10.20.1.6:55300 ESTABLISHED 1/java tcp 0 0 172.17.0.7:20880 10.20.1.157:36786 ESTABLISHED 1/java tcp 0 0 172.17.0.7:20880 10.20.1.2:54570 ESTABLISHED 1/java tcp 0 0 172.17.0.7:20880 10.20.1.5:59428 ESTABLISHED 1/java

宿主机上的连接

看完了上面一堆的连接信息,我们回到最开始在宿主机上看到的两条连接,看看它的另一端是什么。第一条连接的端口是58882,我们通过lsof -i:58882来查看一下对应的进程是什么?
[root@prod-app0001 ~]# lsof -i:58882 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME docker-pr 20954 root 3u IPv6 1448737469 0t0 TCP iZbp1e2goe5txba65jre6qZ:23090->172.17.0.8:58882 (ESTABLISHED) [root@prod-app0001 ~]# ps -ef | grep 20954 root 16942 15120 0 14:41 pts/1 00:00:00 grep --color=auto 20954 root 20954 1267 0 Jan23 ? 00:00:04 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 23090 -container-ip 172.17.0.7 -container-port 20880

docker-proxy

巧了,又是docker-proxy。从docker-proxy的名称以及启动参数我们大概能盲猜出它的作用:监听宿主机上的23090端口,接受任意ip,并将请求代理到172.17.0.7:20880上。诶,这不就是docker的端口映射功能(--port)吗?我们再看看这个docker-proxy进程关联的连接:
[root@prod-app0001 ~]# netstat -anp | grep 20954 tcp 0 0 172.17.0.1:47808 172.17.0.7:20880 ESTABLISHED 20954/docker-proxy tcp 0 0 172.17.0.1:58504 172.17.0.7:20880 ESTABLISHED 20954/docker-proxy tcp6 0 0 :::23090 :::* LISTEN 20954/docker-proxy tcp6 0 0 10.20.1.1:23090 172.17.0.8:58882 ESTABLISHED 20954/docker-proxy tcp6 0 0 10.20.1.1:23090 172.17.0.11:37568 ESTABLISHED 20954/docker-proxy
你有没有发现,列出的4个连接中,上面2个连接是我们在容器里看到的,下面2个连接是我们在宿主机上看到的。
到这里,我们大概能猜出docker的端口映射的实现逻辑:docker-proxy会监听宿主机的端口,当接收到对应端口的连接信息时,docker-proxy会再建立一个自身和对应容器ip(172.17.0.7)和端口(20880)的连接,并把从前一条连接接收到的数据都转发到后一条连接,这也是一个典型的代理链路。所以可以看到上面除了LISTEN之外,其他连接都是成对出现的。
并且看起来同宿主机上的不同容器间通过非docker0访问时,是会走docker-proxy链路的。

DNAT

而在不同宿主机上发起网络请求,并不是走的docker-proxy,而是用到了DNAT。在linux下是通过iptables来实现的,直接把宿主机上对应端口的请求转发到对应容器。我们可以通过iptables -L -n -t nat命令来验证
[root@prod-app0001 ~]# iptables -t nat -L -v -n | grep 23090 37 2220 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:23090 to:172.17.0.7:20880
确实看到一条宿主机上23090端口->172.17.0.7:20880的转发规则。并且适用于除了docker0外的所有网卡。

实验

我们再来通过几个实验来验证一下

宿主机上用localhost/127.0.0.1通信

走的docker-proxy。可以看到telnet和docker-proxy建立了一条通信连接,docker-proxy又建立了一条和对应容器通信的连接。
[root@prod-app0001 ~]# netstat -anp | grep 23090 | grep ESTABLISHED tcp6 0 0 ::1:23090 ::1:45116 ESTABLISHED 20954/docker-proxy tcp6 0 0 ::1:45116 ::1:23090 ESTABLISHED 21960/telnet

宿主机上用eth0通信

走了iptables,从宿主机上看不到docker-proxy的连接
[root@prod-app0001 ~]# netstat -anp | grep 23090 | grep ESTABLISHED tcp 0 0 10.20.1.1:43356 10.20.1.1:23090 ESTABLISHED 24367/telnet
在容器内可以看到对应链接
[root@47383be10098 apps]# netstat -anp | grep 43356 tcp 0 0 172.17.0.7:20880 10.20.1.1:43356 ESTABLISHED 1/java

从宿主机上的其他容器(172.17.0.4)内用eth0通信

走的docker-proxy。这个感觉应该是由于DNAT规则上的!docker0不匹配导致的。
[root@prod-app0001 ~]# netstat -anp | grep 23090 | grep 172.17.0.4 | grep ESTABLISHED tcp6 0 0 10.20.1.1:23090 172.17.0.4:48040 ESTABLISHED 20954/docker-proxy

从不同宿主机上的其他容器内用eth0通信

走的iptables

用bridge网桥分配的网卡通信

无论是从宿主机上发起的,还是同个宿主机上的容器内发起的。这个都是直接通过ip port请求的,无转发逻辑。

总结

没想到在学习dubbo优雅停机的过程中对docker的端口映射有了一定的了解。最开始在使用端口映射这种方式的时候,运维还曾经提过可能对性能产生一定的影响。不过从这里看来,走DNAT的应该没有太多性能损耗,因为都是在内核态。反而是同一个宿主机上,通过docker-proxy转发可能存在一些性能问题(比如存在内核态到用户态的内存拷贝)。
在探究端口映射的过程中,发现对于iptables也不是很熟悉。于是又去恶补了一些iptables的逻辑。这其实会把这个战线拉得比较长。不过还在是有不少收获。

参考

  1. 网络地址转换:DNAT和SNAT有啥区别?分别用于什么场景?
  1. 容器端口映射到主机端口探究
  1. 四、Docker 网络原理、分类及容器互联配置
  1. docker端口映射查看 docker 端口映射原理
  1. docker 网络之端口映射不完全探索
  1. 一篇文章讲明白 Docker 网络原理
  1. Introduction to Container Networking
  1. 聊聊容器网络和 iptables
  1. iptables详解(10):iptables自定义链
  1. 20200414-iptables-principle-introduction
  1. Linux防火墙配置工具iptables中MASQUERADE的含义
  1. iptables详解(1):iptables概念
  1. 【Docker】关于Docker网络隔离与通信详解
优雅停机之Spring Cloud Gateway优雅停机之Dubbo