1.docker-proxy如何工作

在docker的源码中,docker-proxy代码位于vendor目录的proxy包内部,感兴趣的读者可以自行阅读proxy实现代码。本文绕开代码实现只讲实现原理,本文以tcp链接为例,udp效果等同。

在上一章例子中,看到docker-proxy 通过-host-ip指定了docker-proxy在主机上监听的网络接口,通过-host-port指定了监听的端口号;通过-container-ip和-container-port 指定了docker-proxy链接到容器内部的容器ip和端口号。在上例中docker-proxy监听0.0.0.0:8080,那么当主机任何网络接口上有netfliter模块处理后input链到达的目标端口为8080的tcp数据包时刻,docker-proxy会接受这个链接(accept,记为input链接),并主动在连接container-ip+container-port建立一个tcp链接(记为output链接)。当此新建的与容器的链接建立后,docker-proxy会将所有来自input链接的包 发送给output链接。

在这里插入图片描述

2.docker 设置的iptables nat规则

其实在目前的docker-proxy实现中,并不是所有的数据包都由docker-proxy完成包转发。docker配置的iptables nat也参与其中。
可以通过下面命令查看到docker配置的iptables nat规则。

ps -aux | grep -v grep | grep docker-proxy

[root@k8slys01 pgsmaster]# ps -aux | grep -v grep | grep docker-proxy
root      28308  0.0  0.0 225240  3204 ?        Sl   Sep18   0:08 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 13306 -container-ip 172.17.0.5 -container-port 3306
root      28323  0.0  0.0 290776  3212 ?        Sl   Sep18   0:07 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 13306 -container-ip 172.17.0.5 -container-port 3306
root      34497  0.0  0.0 364508  6864 ?        Sl   Dec14   0:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 5500 -container-ip 172.17.0.2 -container-port 5432
root      34504  0.0  0.0 364508  5288 ?        Sl   Dec14   0:00 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 5500 -container-ip 172.17.0.2 -container-port 5432
root      95238  0.0  0.0 438496  3236 ?        Sl   Dec14   0:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 3100 -container-ip 172.17.0.8 -container-port 3100
root      95247  0.0  0.0 299228  3236 ?        Sl   Dec14   0:00 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 3100 -container-ip 172.17.0.8 -container-port 3100
root     103664  0.0  0.1 298972 12940 ?        Sl   Aug30   0:15 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 5501 -container-ip 172.17.0.3 -container-port 5432
root     103671  0.0  0.1 218452  9104 ?        Sl   Aug30   0:16 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 5501 -container-ip 172.17.0.3 -container-port 5432
root     114961  0.0  0.0 366172  3280 ?        Sl   Sep25   0:10 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 443 -container-ip 172.17.0.6 -container-port 443
root     114973  0.0  0.0 290776  3240 ?        Sl   Sep25   0:10 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 443 -container-ip 172.17.0.6 -container-port 443
root     115364  0.0  0.1 364508  9164 ?        Sl   Dec09   0:01 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 5502 -container-ip 172.17.0.4 -container-port 5432
root     115372  0.0  0.0 225240  3232 ?        Sl   Dec09   0:01 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 5502 -container-ip 172.17.0.4 -container-port 5432
root     119408  0.0  0.0 298972  3240 ?        Sl   Sep25   0:09 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 9411 -container-ip 172.17.0.7 -container-port 9411
root     119417  0.0  0.0 364508  3236 ?        Sl   Sep25   0:11 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 9411 -container-ip 172.17.0.7 -container-port 9411
root     199800  0.0  0.0 364508  3236 ?        Sl   Dec13   0:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 3000 -container-ip 172.17.0.10 -container-port 3000
root     199807  0.0  0.0 308576  3232 ?        Sl   Dec13   0:00 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 3000 -container-ip 172.17.0.10 -container-port 3000

iptables-save -t nat

[root@k8slys01 pgsmaster]# iptables-save -t nat
# Generated by iptables-save v1.4.21 on Fri Dec 17 16:19:54 2021
*nat
:PREROUTING ACCEPT [1863890:214521050]
:INPUT ACCEPT [393:115683]
:OUTPUT ACCEPT [15872:1204760]
:POSTROUTING ACCEPT [16547:1246012]
:DOCKER - [0:0]
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A POSTROUTING -s 172.17.0.3/32 -d 172.17.0.3/32 -p tcp -m tcp --dport 5432 -j MASQUERADE
-A POSTROUTING -s 172.17.0.5/32 -d 172.17.0.5/32 -p tcp -m tcp --dport 3306 -j MASQUERADE
-A POSTROUTING -s 172.17.0.6/32 -d 172.17.0.6/32 -p tcp -m tcp --dport 443 -j MASQUERADE
-A POSTROUTING -s 172.17.0.7/32 -d 172.17.0.7/32 -p tcp -m tcp --dport 9411 -j MASQUERADE
-A POSTROUTING -s 172.17.0.4/32 -d 172.17.0.4/32 -p tcp -m tcp --dport 5432 -j MASQUERADE
-A POSTROUTING -s 172.17.0.10/32 -d 172.17.0.10/32 -p tcp -m tcp --dport 3000 -j MASQUERADE
-A POSTROUTING -s 172.17.0.8/32 -d 172.17.0.8/32 -p tcp -m tcp --dport 3100 -j MASQUERADE
-A POSTROUTING -s 172.17.0.2/32 -d 172.17.0.2/32 -p tcp -m tcp --dport 5432 -j MASQUERADE
-A DOCKER -i docker0 -j RETURN
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 5501 -j DNAT --to-destination 172.17.0.3:5432
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 13306 -j DNAT --to-destination 172.17.0.5:3306
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 443 -j DNAT --to-destination 172.17.0.6:443
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 9411 -j DNAT --to-destination 172.17.0.7:9411
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 5502 -j DNAT --to-destination 172.17.0.4:5432
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 3000 -j DNAT --to-destination 172.17.0.10:3000
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 3100 -j DNAT --to-destination 172.17.0.8:3100
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 5500 -j DNAT --to-destination 172.17.0.2:5432
COMMIT

docker-proxy是否有必要存在

在网上大量的容器最佳实践中都建议关闭docker-proxy,“原因是docker会为每个容器每个暴露的端口都启动一个docker-proxy进程,这个docker-proxy会消耗大概2M的RSS内存。当宿主机环境上有几百上千个容器的时刻,那么可能有几百上千个docker-proxy,其对物理内存消耗的是非常可观的。而docker-proxy的功能完全可以被docker配置的iptables nat规则替代。所以没有docker-proxy就没有必要开启。” 上述论断是否是正确的呢?答案是,在大多数场景下此答案正确。只有如下场景docker-proxy才是刚需:

1、ipv6场景

docker启动时刻可以通过ipv6参数开启docker ipv6支持功能。开启后所有docker容器都在ipv6下工作。但是此时docker在ipv6上的工作并为完善,docker并未在ipv6table上为容器添加相应的DNAT规则。如果此时关闭docker-proxy,那么容器外部无法访问到容器内部网络。在不借助任何外部手段的情况下(可以使用一个叫ipv6nat工具实现ip6table nat规则的自动添加),所以此场景下docker-proxy需要开启。

2、在老内核下(2.6.x)

容器内部通过hairpin 方式访问自己暴露的服务 在第一章的例子中,如果需要在容器内部访问自己暴露的服务,那么就出现了hairpin DNAT访问方式:

 #docker exec -it zxy-nginx /bin/bash
 root@8173f601424 #curl http://192.168.126.222:8080
 <html>
 <head>
 <title>Welcome to nginx!</title>
 </head>
 <body bgcolor=“white“ text=“black”>
 <center><h1>Welcome to nginx!</h1></center>
 </body>
 </html>

可以看到在容器zxy-nginx内通过主机ip+容器映射主机端口方式一样可以访问到zxy-nginx容器自己暴露的nginx服务。 这就是hairpin DNAT模式。但是关闭docker-proxy时刻,数据包进过docker0上的prerouting链时被表3-2的DNAT命中,数据包dst-ip被转换为172.17.0.4,dst-port被转换为80,在docker0的forwarding动作中,判定此包需要送回zxy-nginx在docker0上的网络接口veth17f3d1上。默认情况下,内核bridge不允许将包发送回到源接口的;只有在内核配置了hairpin mode enable时刻,才允许此类操作。在docker处理流程中,如果用户关闭了docker-proxy,那么docker会开启内核的hairpin mode(在centos 7x上通过echo “1”>/sys/class/net/docker0/brif/vethxxx/hairpin_mode开启hairpin模式)。但是在老内核2.6.x上,没有办法启用hairpin mode。所以此时无法借助iptables nat实现容器内部网络可达。此刻就必须使用docker proxy了。关于hairpin模式的解释请参考:https://wiki.mikrotik.com/wiki/Hairpin_NAT

3、总结

如果不使用ipv6或者通过ipv6nat工具实现了docker容器网络ipv6 DNAT规则,那么完全可以关闭docker-proxy。
机器上docker-proxy进程rss是1592KB,如果有100个容器,关闭了docker-proxy是否真实节省了100*1592KB 约等于1.5GB物理内存?答案是否定的!docker-proxy其实逻辑很简单,它的rss占用约1.5MB是因为docker-proxy是golang语言编写,golang默认采用静态链接方式将所有的库都静态链接到可执行程序中。所以docker-proxy RSS看起来比较大。但是这里1.5MB中有很大的部分都是docker-proxy可执行程序的代码段,这部分在linux上以map方式映射到docker-proxy可执行文件上的。当多个docker-proxy进程存在时刻,这部分maps实际上是通过文件缓存在整个系统共享的。所以在真实系统上多个docker-proxy消耗的真实物理内存,其实只有docker-proxy的堆和栈,这部分大概只有几百KB(约200KB),所以关闭docker-proxy的收益并没有想象的那么大(可以通过cat/proc/$docker-proxy-pid/pmaps 中每个map段的pss,随着docker-proxy进程数目增加反而下降证明)。

Logo

华为云1024程序员节送福利,参与活动赢单人4000元礼包,更有热门技术干货免费学习

更多推荐