URL
date
AI summary
slug
status
tags
summary
type
Docker端口映射的实现机制
引言
在学习dubbo的过程中,我们了解到了dubbo优雅停机的流程:
- provider会先从注册中心把自己下线
- 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地址。为了更好的展示,下面我用两个命令把连接分成了两类:- 目的ip为
172.17.0.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的逻辑。这其实会把这个战线拉得比较长。不过还在是有不少收获。
参考
- 作者:黑微狗
- 链接:https://blog.hwgzhu.com/article/docker-port-publication-principle
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
相关文章