docker-proxy存在合理性分析

访问docker容器网络

熟悉docker的朋友也许都知道在使用docker默认情况下为所有的容器准备了一个网络(docker0),并且可以通过-p参数将将主机上某个端口映射到容器内部的固定端口上。例如:

docker run -name zxy-nginx -itd -p 8080:80 mynginx:v1 /bin/bash

上面命令运行后,可以在主机上通过docker ps命令查看到多了一个叫zxy-nginx的容器(上例中假设我们主机上有一个叫mynginx:v1的nginx 容器镜像)。且docker在主机上将任意网络接口的8080访问导入到zxy-nginx容器内部。

# docker ps
CONTAINER ID.              IMAGE.            COMMAND.            CREATED.        STATUS          PORTS.         NAME
81713f601424                mynginx:v1      “/bin/bash”              4 seconds ago  up 3 seconds 0.0.0.0:8080->80/tcp zxy-nginx

在zxy-nginx内部启动nginx服务,并在主机上通过curl 访问nginx服务。

#docker exec -it zxy-nginx /usr/local/nginx/sbin/nginx -c /usr/local/nginx/nginx.conf
#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>

在上例中,我在主机上通过主机外部ip地址192.168.126.222+端口8080 访问到容器zxy-nginx内部的nginx服务。当然也可以在另外一台与主机相连的机器上,通过curl 192.168.126.222:8080访问到主机上的zxy-nginx容器内部。这是如何达成的呢?在docker默认配置时刻,docker端口暴露是通过docker-proxy加适当的iptables规则实现的。通过下面命令可以看到docker-proxy的存在:

#ps aux|grep docker-proxy
root     29724    0.0   0.0      42852.   1592  ?       Sl  22:41    0:00 /usr/bin/docker-proxy  -proto tcp -host-ip 0.0.0.0 -host-port 8080 -container-ip 172.17.0.4  -container-port 80

但是docker-proxy却不是一定需要的,通过下面命令可以看到是否使用docker-proxy也是可以配置的:

#dockerd --help|grep proxy
       --userland-proxy             Use userland proxy for loopback traffic (default true)
       --userland-proxy-path     Path to the userland proxy binary

在centos 7.x上docker rpm安装方式下,可以通过/etc/docker/daemon.json里添加 “userland-proxy”:false 关闭docker-proxy
那么docker-proxy是不是可以关闭呢?关闭了容器内部暴露端口又如何实现呢?

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链接。

docker-proxy转发包

docker 设置的iptables nat规则

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

#iptables-save -t nat 

通过上述命令我们将docker-proxy开启/关闭情况下,将iptables nat表导出可以得出下面的规则表

场景 开启docker-proxy 关闭docker-proxy
A -t nat -A OUTPUT -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER -t nat -A OUTPUT -m addrtype --dst-type LOCAL -j DOCKER
B -t nat -A DOCKER -i docker0 -j RETURN n/a
C n/a -t nat -A POSTROUTING -o docker0 -m addrtype --src-type LOCAL -j MASQUERADE
D -i nat -A DOCKER -i docker0 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 172.17.0.4 同开启docker-proxy时一致

表3-1

下面我们以第一章出现的例子,看看docker-proxy和iptables如何共同作用访问docker容器内部

场景 开启docker-proxy时刻 关闭docker-proxy时刻
外部主机访问目标主机192.168.126.222:8080 通过iptables nat规则访问 通过iptables nat规则访问
在主机上访问容器192.168.126.222:8080 通过iptables nat规则访问 通过iptables nat规则访问
在主机上访问目标容器127.0.0.1:8080 通过docker-proxy 转发 通过iptables nat规则访问
在主机上其他容器内部访问目标容器 172.17.0.4:8080 通过docker proxy转发 通过iptables nat规则访问

表3-2

细心的读者也许会有这样的疑惑,即使在开启docker-proxy的配置下,也不是所有的包都直接由docker-proxy处理。对于外部主机访问目标主机192.168.126.222:8080和在主机上访问容器192.168.126.222:8080时刻依然会通过iptables nat表处理。其实这两种场景下iptables DNAT规则确实多余了,可以通过下面命令手动将iptables nat表中DOCKER链规则直接绕开:

#iptables -t nat -I DOCKER 1 -p tcp -j RETURN

添加这条规则后iptables nat中所有到达DOCKER链的tcp包都会直接不做dnat处理返回,此时docker-proxy起作用将数据包转发到容器内部。手动添加上述iptables规则后,表3-1中通过192.168.126.222:8080访问容器内部的场景依然可达。对于这种冗余功能逻辑的情况,我们尚未找到原因。
通过表3-2,我们可以看到在常规使用场景中,docker-proxy存在与否都不影响docker容器的通讯。那么docker-proxy存在是否还有必要呢?

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规则的自动添加,但即便如此ipv6场景下,docker-proxy依然有存在意义详见后文场景3),所以此场景下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、在内核无法开启route_localnet的情况下
正常情况下,内核不会对地址为localnet(127.0.0.0/8)的地址做forwarding,因为这部分地址被认为为martian 。但是在内核中可以通过配置

echo “1”>/proc/sys/net/ipv4/conf/$BridgeName/route_localnet 

开启。docker在启动阶段会配置此配置项。但是对于低版本内核无此参数或对于ipv6地址场景(ipv6内核无此配置项,无此功能)内核依然不会对localhost(ipv6下地址为[::1])进行forwarding;所以在此部分场景下,如果需要在主机上,使用localhost:Port 与容器通讯依然需要依赖于docker-proxy

总结

1、在9102年的今天,如果不使用ipv6,那么完全可以关闭docker-proxy。
2、在ipv6场景下,如果使用了ipv6nat工具配置容器的dnat,且不在主机上使用localhost:port方式与容器通讯,那么也可以关闭docker-proxy

尾声:关闭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,所以关闭docker-proxy的收益并没有想象的那么大。可以通过cat
/proc/$docker-proxy-pid/smaps 中每个map段的pss,随着docker-proxy进程数目增加反而下降证明。通过下面命令可以统计docker-proxy真实的物理内存消耗:

cat /proc/$docker-proxy-pid/smaps |grep -i Private |gawk ‘BEGIN{sum=0}{sum=sum+$2} END{print sum}’

在笔者的环境上,统计出来docke-proxy在有负荷(通过nginx镜像下载10G的文件)情况下,docker-proxy内存消耗在500KB左右。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,313评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,369评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,916评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,333评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,425评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,481评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,491评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,268评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,719评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,004评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,179评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,832评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,510评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,153评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,402评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,045评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,071评论 2 352