吹雨听风

Docker 网络之端口映射不完全探索

标签: 技术 , Docker , 容器

这篇文章实在团队内部的分享,如下:

今天的分享不包含故事背景、docker的发展与应用…

分享源于对问题 ‘我已经在 Dockerfile 中通过 EXPOSE 指定了端口为何任然无法访问?’ 深入探索。其实今天的分享也可以视为对上一期峰哥分享的一个补充。

Docker容器的端口映射

容器的服务端口绑定到宿主机的端口上。效果就是:外部程序通过宿主机的P端口访问,就像直接访问 Docker 容器网络内部容器提供的服务一样。

今天的分享主要涉及到的 Docker run 命令,参数如下:

  • -p/-P
  • –expose

expose

expose 参数有两种使用形式:

  • docker run 命令时指定 --expose 参数, 如 –expose=8080
  • 在 Dockerfile 中,通过 EXPOSE 关键字

作用

EXPOSE 指令是声明运行时容器提供服务端口。

注意

仅仅是申明。并不是说你声明了这个端口,在运行容器的时候就会自动的暴露这个端口。使用时,还要依赖于容器的操作人员进一步指定网络规则。

本质上来说, EXPOSE 或者 --expose 只是为其他命令提供所需信息的元数据,或者只是告诉容器操作人员有哪些已知选择。

验证: 通过 docker run nginx 启动一个容器, 然后通过 `docker inspect id` 查看

1
2
3
4
5
6
7
"HostConfig": {
"PortBindings": {
"443/tcp": null,
"80/tcp": null
},
}

可以看到端口被标示成已暴露,但是没有定义任何与主机的端口映射。

-p/-P

-p-P 参数的完整形式:

1
2
-p, --publish list 
-P, --publish-all

这两个参数都是发布端口到宿主主机。但用法上存在一点区别。

-p 显式将一个或者一组端口从容器里绑定到宿主机上。
-P 自动的将EXPOSE指令相关的每个端口映射到宿主机的端口上。

-p 参数常见的用法是: -p 宿主主机端口:容器端口。 如果使用 `docker run -p 8080:80 nginx` 命令启动nginx 容器,那么容器中的 80端口会绑定到主机的8080端口。

这是,我们再通过 docker inspect id 来查看,将会看到下面的绑定关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
"HostConfig": {
"PortBindings": {
"443/tcp": null,
"80/tcp": [
{
"HostIp": "0.0.0.0",
"HostPort": "8080"
}
]

},
}

因为没有指定443端口的绑定,所以能看到仅有 80端口和宿主机存在端口映射关系。

另外,在使用-p参数是,我们可以忽略指定宿主主机端口。这是,docker会帮助我们自动的选择一个合适端口和容器端口进行绑定。这样做的好处是在启动多个容器时可以避免端口冲突。

例如上面的启动命令可以改为 docker run -p 80 nginx, 这时如果我们本机的80端口被占用,docker就会自动的选择一个其他端口。我们可以通过 docker psdocker inspect 命令来查看端口的映射关系。

-P 参数用法: docker run -P nginx.
-P 参数须配合 Dockerfile 一起使用, 能够将 Expose 声明的端口映射到宿主主机。

此时,使用 docker inspect 命令,可以看到 dockerfile 中申明的端口都已经绑定到了主机上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"HostConfig": {
"PortBindings": {
"443/tcp": [
{
"HostIp": "0.0.0.0",
"HostPort": "443"
}
],
"80/tcp": [
{
"HostIp": "0.0.0.0",
"HostPort": "80"
}
]

},
}

-expose 和 -p 功能对比

我们可以通过如下的实验来更好的理解两参数之间的区别。

  • 不在 DockerfileEXPOSE,也不通过 -p 参数指定
  • DockerfileEXPOSE,但不使用 -p 参数
  • DockerfileEXPOSE,也使用 -p 参数
  • DockerfileEXPOSE,也使用 -P 参数
  • 只使用 -p 参数

结果

  • 第一中情况: 不能在外网访问,也不能被 link 的 container 访问
  • 第二种情况: 不能被外网访问,但是能被 link 的 container 访问
  • 第三种情况: 能被外网访问,也能被 link 容器访问iw
  • 第四种情况: 和第三种情况一样
  • 第五种情况: 和第三种情况一样

Docker 端口映射原理

备注: 这一块挺乱的,我也没弄得很清楚,权当抛砖引玉了。

原本我理解端口映射是这样的一个通信过程:

  • Docker进程启动的时候,会在宿主主机创建路由,同时创建docker0网桥
  • 容器启动的时候创建 vethxx 的网卡,同时链接到网桥
  • 通过 -p 参数指定端口映射后, 创建iptables规则
  • 当有流量通过宿主主机端口进来用,通过iptables 匹配到规则后,转换为容器对应的子网ip
  • 主机的路由指定了 172.xx 网段的ip由 docker0 处理
  • docker0再将请求转发到子网中容器

后面和朋友了解了,发现还有 docker-proxy 的存在,于是,上面的理解是其实就片面的,不完善的。


到现在,关于 Docker 端口映射的实现一共有2方案.

  • 1.7版本之前 docker-proxy + iptables DNAT 的方式
    即,内网访问通过 iptables
    外网访问通过 proxy
  • 1.7版本之后的 iptables DNAT
    完全由 iptables 实现

Docker 1.7版本起,Docker提供了一个配置项: -userland-proxy,以让 Docker 用户决定是否启用 docker-proxy,默认为true,即启用docker-proxy。
现在的 Docker 环境默认的是: -userland-proxy=true。iptables 和 docker-proxy 都会起作用。

-userland-proxy=true的情况下

在启用 docker-proxy 的情况下。每设置一对端口映射就会启动一个 docker-proxy 进程。

可以通过 ps 命令查看 docker-proxy 进程信息

1
2
3
4
ps -ef | grep docker-proxy

root 5532 19713 0 2月20 ? 00:00:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 443 -container-ip 172.19.0.8 -container-port 443
root 5546 19713 0 2月20 ? 00:00:01 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 80 -container-ip 172.19.0.8 -container-port 80

通过 sudo netstat -nltpu 查看本机的端口监听情况:

1
2
tcp6       0      0 :::80                   :::*                    LISTEN      5546/docker-proxy          
tcp6 0 0 :::443 :::* LISTEN 5532/docker-proxy

通过上面的命令,会发现,我们映射到宿主机的端口被 docker-proxy 进程监听了。

别急,我们再看一下iptables,发现其中增加了对应的规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
 sudo iptables-save -t nat                 
# Generated by iptables-save v1.6.1 on Fri Feb 22 15:06:17 2019
*nat
:PREROUTING ACCEPT [55751:14743102]
:INPUT ACCEPT [55615:14734886]
:OUTPUT ACCEPT [260072:22717364]
:POSTROUTING ACCEPT [260200:22725044]
: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.20.0.0/16 ! -o br-bf4c59b26d33 -j MASQUERADE
-A POSTROUTING -s 172.19.0.0/16 ! -o br-e2ab1d51063d -j MASQUERADE
-A POSTROUTING -s 172.18.0.0/16 ! -o br-c9af812dc067 -j MASQUERADE
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A POSTROUTING -s 172.19.0.2/32 -d 172.19.0.2/32 -p tcp -m tcp --dport 6379 -j MASQUERADE
-A POSTROUTING -s 172.19.0.3/32 -d 172.19.0.3/32 -p tcp -m tcp --dport 27017 -j MASQUERADE
-A POSTROUTING -s 172.19.0.5/32 -d 172.19.0.5/32 -p tcp -m tcp --dport 3306 -j MASQUERADE
-A POSTROUTING -s 172.19.0.6/32 -d 172.19.0.6/32 -p tcp -m tcp --dport 9501 -j MASQUERADE
-A POSTROUTING -s 172.19.0.6/32 -d 172.19.0.6/32 -p tcp sudo iptables -t filter -L
Chain INPUT (policy ACCEPT)
target prot opt source destination

Chain FORWARD (policy DROP)
target prot opt source destination
DOCKER-USER all -- anywhere anywhere
DOCKER-ISOLATION-STAGE-1 all -- anywhere anywhere
ACCEPT all -- anywhere anywhere ctstate RELATED,ESTABLISHED
DOCKER all -- anywhere anywhere
ACCEPT all -- anywhere anywhere
ACCEPT all -- anywhere anywhere
ACCEPT all -- anywhere anywhere ctstate RELATED,ESTABLISHED
DOCKER all -- anywhere anywhere
ACCEPT all -- anywhere anywhere
ACCEPT all -- anywhere anywhere
ACCEPT all -- anywhere anywhere ctstate RELATED,ESTABLISHED
DOCKER all -- anywhere anywhere
ACCEPT all -- anywhere anywhere
ACCEPT all -- anywhere anywhere
ACCEPT all -- anywhere anywhere ctstate RELATED,ESTABLISHED
DOCKER all -- anywhere anywhere
ACCEPT all -- anywhere anywhere
ACCEPT all -- anywhere anywhere

Chain OUTPUT (policy ACCEPT)
target prot opt source destination

Chain DOCKER (4 references)
target prot opt source destination
ACCEPT tcp -- anywhere 172.19.0.2 tcp dpt:6379
ACCEPT tcp -- anywhere 172.19.0.3 tcp dpt:27017
ACCEPT tcp -- anywhere 172.19.0.5 tcp dpt:mysql
ACCEPT tcp -- anywhere 172.19.0.6 tcp dpt:9501
ACCEPT tcp -- anywhere 172.19.0.6 tcp dpt:ssh
ACCEPT tcp -- anywhere 172.19.0.8 tcp dpt:https
ACCEPT tcp -- anywhere 172.19.0.8 tcp dpt:http

Chain DOCKER-ISOLATION-STAGE-1 (1 references)
target prot opt source destination
DOCKER-ISOLATION-STAGE-2 all -- anywhere anywhere
DOCKER-ISOLATION-STAGE-2 all -- anywhere anywhere
DOCKER-ISOLATION-STAGE-2 all -- anywhere anywhere
DOCKER-ISOLATION-STAGE-2 all -- anywhere anywhere
RETURN all -- anywhere anywhere

Chain DOCKER-ISOLATION-STAGE-2 (4 references)
target prot opt source destination
DROP all -- anywhere anywhere
DROP all -- anywhere anywhere
DROP all -- anywhere anywhere
DROP all -- anywhere anywhere
RETURN all -- anywhere anywhere

Chain DOCKER-USER (1 references)
target prot opt source destination
RETURN all -- anywhere anywhere -m tcp --dport 22 -j MASQUERADE
-A POSTROUTING -s 172.19.0.8/32 -d 172.19.0.8/32 -p tcp -m tcp --dport 443 -j MASQUERADE
-A POSTROUTING -s 172.19.0.8/32 -d 172.19.0.8/32 -p tcp -m tcp --dport 80 -j MASQUERADE
-A DOCKER -i br-bf4c59b26d33 -j RETURN
-A DOCKER -i br-e2ab1d51063d -j RETURN
-A DOCKER -i br-c9af812dc067 -j RETURN
-A DOCKER -i docker0 -j RETURN
-A DOCKER ! -i br-e2ab1d51063d -p tcp -m tcp --dport 6379 -j DNAT --to-destination 172.19.0.2:6379
-A DOCKER ! -i br-e2ab1d51063d -p tcp -m tcp --dport 27017 -j DNAT --to-destination 172.19.0.3:27017
-A DOCKER ! -i br-e2ab1d51063d -p tcp -m tcp --dport 3306 -j DNAT --to-destination 172.19.0.5:3306
-A DOCKER ! -i br-e2ab1d51063d -p tcp -m tcp --dport 9501 -j DNAT --to-destination 172.19.0.6:9501
-A DOCKER ! -i br-e2ab1d51063d -p tcp -m tcp --dport 2222 -j DNAT --to-destination 172.19.0.6:22
-A DOCKER ! -i br-e2ab1d51063d -p tcp -m tcp --dport 443 -j DNAT --to-destination 172.19.0.8:443
-A DOCKER ! -i br-e2ab1d51063d -p tcp -m tcp --dport 80 -j DNAT --to-destination 172.19.0.8:80

这里的 DOCKER 对应的是由 docker 自定义的一组过滤规则,可以通过 sudo iptables -t filter -L 查看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
sudo iptables -t filter -L
Chain INPUT (policy ACCEPT)
target prot opt source destination

Chain FORWARD (policy DROP)
target prot opt source destination
DOCKER-USER all -- anywhere anywhere
DOCKER-ISOLATION-STAGE-1 all -- anywhere anywhere
ACCEPT all -- anywhere anywhere ctstate RELATED,ESTABLISHED
DOCKER all -- anywhere anywhere
ACCEPT all -- anywhere anywhere
ACCEPT all -- anywhere anywhere
ACCEPT all -- anywhere anywhere ctstate RELATED,ESTABLISHED
DOCKER all -- anywhere anywhere
ACCEPT all -- anywhere anywhere
ACCEPT all -- anywhere anywhere
ACCEPT all -- anywhere anywhere ctstate RELATED,ESTABLISHED
DOCKER all -- anywhere anywhere
ACCEPT all -- anywhere anywhere
ACCEPT all -- anywhere anywhere
ACCEPT all -- anywhere anywhere ctstate RELATED,ESTABLISHED
DOCKER all -- anywhere anywhere
ACCEPT all -- anywhere anywhere
ACCEPT all -- anywhere anywhere

Chain OUTPUT (policy ACCEPT)
target prot opt source destination

Chain DOCKER (4 references)
target prot opt source destination
ACCEPT tcp -- anywhere 172.19.0.2 tcp dpt:6379
ACCEPT tcp -- anywhere 172.19.0.3 tcp dpt:27017
ACCEPT tcp -- anywhere 172.19.0.5 tcp dpt:mysql
ACCEPT tcp -- anywhere 172.19.0.6 tcp dpt:9501
ACCEPT tcp -- anywhere 172.19.0.6 tcp dpt:ssh
ACCEPT tcp -- anywhere 172.19.0.8 tcp dpt:https
ACCEPT tcp -- anywhere 172.19.0.8 tcp dpt:http

Chain DOCKER-ISOLATION-STAGE-1 (1 references)
target prot opt source destination
DOCKER-ISOLATION-STAGE-2 all -- anywhere anywhere
DOCKER-ISOLATION-STAGE-2 all -- anywhere anywhere
DOCKER-ISOLATION-STAGE-2 all -- anywhere anywhere
DOCKER-ISOLATION-STAGE-2 all -- anywhere anywhere
RETURN all -- anywhere anywhere

Chain DOCKER-ISOLATION-STAGE-2 (4 references)
target prot opt source destination
DROP all -- anywhere anywhere
DROP all -- anywhere anywhere
DROP all -- anywhere anywhere
DROP all -- anywhere anywhere
RETURN all -- anywhere anywhere

Chain DOCKER-USER (1 references)
target prot opt source destination
RETURN all -- anywhere anywhere

-userland-proxy=false的情况下

待研究~

性能

docker-proxy 在网络上吐槽的比较多,因为每一对端口映射都会对一个 docker-proxy进程,如果端口较多,可能就会带来性能问题。且在单个 docker-proxy 的情况下,性能比 iptables 略差。

总结

- –link 能够访问 expose 声明的端口

  • -expose 仅声明端口,并不会自动映射到宿主主机
  • -p 指定端口映射关系
  • -P 将 expose 声明的端口发布到宿主主机
  • 在处理端口映射是,iptables 规则优先,如果没有匹配到iptables规则,则由 docker-proxy处理

待深入研究的问题:

  • container <-> container、host <-> container、 container <-> host 各自怎么选择策略的
  • iptables 规则