浅析Docker 独立容器的网络通信模式

很早就接触过Docker容器相关的概念,之前还使用过Jenkins提供的k8s插件进行容器编排。但说实话,自己工作还是生活中使用的比较少,基础处于我知道是怎么样的,但缺乏实践经验。

搭建xxl-job-admin docker container 遇到的疑问

在阅读xxl-job的文档时,发现其提供Docker容器部署的方式,于是就在本机装好了Docker环境,实践一番。提前说明一下,本次实验使用的Docker版本为19.03.8。
xxl-job-admin的容器部署很简单,一共就分两步:

  • 先部署一个MySql,部署完需要执行好xxl-job相应的SQL脚本
  • 再部署xxl-job-admin,其中MySql链接指向刚刚部署好的MySql实例

部署命令也很简单,一共就3条:

1
2
3
4
5
6
docker network create simple-network

docker run --name xxl-mysql --network simple-network -e MYSQL_ROOT_PASSWORD=123456 -d mysql

docker run --network simple-network -e PARAMS="--spring.datasource.url=jdbc:mysql://xxl-mysql:3306/xxl_job?Unicode=true&characterEncoding=UTF-8&useSSL=false --spring.datasource.password=123456" -p 8080:8080 -v /tmp:/data/applogs --name xxl-job-admin -d xuxueli/xxl-job-admin:2.1.2

我们在结尾再解释为什么是这样写的,我先提出我的困惑。和单机部署应用进程都处在同一个网络环境中不同的是,在同一台主机上的两个容器,他们的网络环境是不是隔离的?如果不是隔离的,容器之间是怎么通信的,容器和宿主机之间又是怎么通行的呢?

Docker network drivers 介绍

在解答疑惑之前,我们需要先了解一下Docker的network驱动,通常情况下,默认是以下几种:

  • bridge:默认网络驱动模式,通常如果不特别指定别的网络驱动模式,一般默认就是bridge。
  • host:在独立容器中,使用host网络驱动会移除容器和容器主机之间的网络隔离性。也就是说,这个容器和主机的网络环境完全一样。不过目前只在Docker 17.06以上版本支持,且不支持Docker Desktop for Mac, Docker Desktop for Windows, or Docker EE for Windows Server.
  • overlay:overlay 网络将多个 Docker 守护进程连接在一起,并使集群服务能够相互通信。您还可以使用 overlay 网络来实现 swarm 集群和独立容器之间的通信,或者不同 Docker 守护进程上的两个独立容器之间的通信。该策略实现了在这些容器之间进行操作系统级别路由的需求。[^cnkirito_footnote]
  • macvlan:Macvlan 网络允许为容器分配 MAC 地址,使其显示为网络上的物理设备。 Docker 守护进程通过其 MAC 地址将流量路由到容器。对于希望直连到物理网络的传统应用程序而言,使用 macvlan 模式一般是最佳选择,而不应该通过 Docker 宿主机的网络进行路由。[^cnkirito_footnote]
  • none:对于此容器,禁用所有联网。通常与自定义网络驱动程序一起使用。none 模式不适用于集群服务。[^cnkirito_footnote]

对于运行在一台主机的单独容器而言,只需要关心以上的bridge、host、none。
查看当前容器下已有的网络驱动

1
2
3
4
5
6
➜  ~ docker network ls
NETWORK ID NAME DRIVER SCOPE
04675183a24d bridge bridge local
245b36b452af host host local
43a8d2311d38 none null local

可以看到,默认在一个Docker容器中包含三种网络驱动,其中SCOPE指的是网络范围,可以是local或swarm范围。区别在于local是在主机范围内提供连接和网络服务(例如DNS或IPAM)。swarm可跨群群集提供连接和网络服务。swarm网络在整个群集中具有相同的网络ID,而local范围网络在每个主机上具有唯一的网络ID。

接下来我们主要介绍host并着重介绍bridge网络驱动。

Docker Host Network Driver

如果在创建容器时指定--net=host,host网络中的所有容器都可以在宿主机上相互通信。从网络角度来看,这等于在没有容器的主机上运行的多个进程。因为它们使用相同的主机环境,所以没有两个容器能够绑定到相同的TCP端口,如果在同一主机上调度了多个容器,则可能出现端口冲突的情况。

1
2
3
4
5
6
7
8
9
10
11
#Create containers on the host network
host $ docker run --rm -itd --net host --name C1 alpine sh

#Show host eth0
host $ ip -o -4 address show dev eth0 |cut -d’ ‘ -f1-7
2: eth0 inet 172.31.21.213/20

#Show eth0 from C1
host $ docker exec -it C1 sh
C1 # ip -o -4 address show dev eth0 |cut -d' ' -f1-7
2: eth0 inet 172.31.21.213/20

可见,C1和宿主机的公用一套网络环境,所以eth0网卡的IP均为172.31.21.213。在host网络下,容器网络环境和宿主机网络没有任何隔离性,容器和容器之间,容器和宿主机之间的网络通信不存在任何障碍。

Docker Bridge Network Driver

容器创建时倘若没有特别指定其他网络驱动,则默认使用的是Bridge 桥接网络驱动。而在docker中,bridge分为系统默认和用户自定义的两种;

Default Docker Bridge Network

可以看到上面执行执行docker network ls 时,第一行展示的network
就是一个名叫Bridge的Bridge网络驱动。
比如启动一个名叫bb-default-bridge的容器,在容器内部,其IP为172.17.0.2

1
2
3
4
5
6
7
8
9
➜  ~ docker run --rm -it --name bb-default-bridge busybox sh
/ # ifconfig
eth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:02
inet addr:172.17.0.2 Bcast:172.17.255.255 Mask:255.255.0.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:10 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:828 (828.0 B) TX bytes:0 (0.0 B)

那如何反映,该容器使用的是默认的Bridge网络驱动呢,docker提供了docker network inspect命令展现使用该网络驱动下的所有容器。

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
➜  ~ docker network inspect bridge
[
{
"Name": "bridge",
"Id": "04675183a24d5f702da598aa3fc03c3a68684ff66b45e4c9eddbbf80c94031d7",
"Created": "2020-03-30T14:00:38.974673641Z",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.17.0.0/16",
"Gateway": "172.17.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"3af6ea18328e0414ecaac61756ce4e658b24ea89ccd182808a73b424f36ab01d": {
"Name": "bb-default-bridge",
"EndpointID": "6e4cc1b42cd4c0ffd52bfcbe9e69ac4169a3e41a4e013ec5bb9aa30b82ec1ce5",
"MacAddress": "02:42:ac:11:00:02",
"IPv4Address": "172.17.0.2/16",
"IPv6Address": ""
}
},
"Options": {
"com.docker.network.bridge.default_bridge": "true",
"com.docker.network.bridge.enable_icc": "true",
"com.docker.network.bridge.enable_ip_masquerade": "true",
"com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
"com.docker.network.bridge.name": "docker0",
"com.docker.network.driver.mtu": "1500"
},
"Labels": {}
}
]

可见,你同时可以发现,在Containers.IPv4Address的内容和容器内获取到的IP是一致的,且com.docker.network.bridge.name所指是网桥名称为docker0。

实验宿主机的IP为192.168.1.10,那在bb-default-bridge中能不能访问到宿主机呢

1
2
3
4
5
/ # ping 192.168.1.105
PING 192.168.1.105 (192.168.1.105): 56 data bytes
64 bytes from 192.168.1.105: seq=0 ttl=37 time=4.473 ms
64 bytes from 192.168.1.105: seq=1 ttl=37 time=0.773 ms
64 bytes from 192.168.1.105: seq=2 ttl=37 time=1.157 ms

结果发现是可以的,反过来宿主机ping容器则ping不通(是不是有点类似Windows中的NAT网络类型)。如果宿主机需要访问容器时,可以在创建容器时使用 -p 参数将宿主机端口和容器端口映射。
假如再创建一个bb-default-bridge-1,依旧使用的是默认的Bridge网络驱动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"Containers": {
"3af6ea18328e0414ecaac61756ce4e658b24ea89ccd182808a73b424f36ab01d": {
"Name": "bb-default-bridge",
"EndpointID": "6e4cc1b42cd4c0ffd52bfcbe9e69ac4169a3e41a4e013ec5bb9aa30b82ec1ce5",
"MacAddress": "02:42:ac:11:00:02",
"IPv4Address": "172.17.0.2/16",
"IPv6Address": ""
},
"453703548c1416316e776d9dab9a87afb5d006fe23da87ca25416cdad390bffc": {
"Name": "bb-default-bridge-1",
"EndpointID": "e18409cabc6a10510529c8ded1a6c7f0ceba057473585418b36e1c7406e67af7",
"MacAddress": "02:42:ac:11:00:03",
"IPv4Address": "172.17.0.3/16",
"IPv6Address": ""
}
},

会发现,两者均可以互相ping通。也就说,在同一个宿主机的不同容器之间,使用默认Bridge network driver 是可以通过IP相互访问对方的。

User-Defined Bridge Networks

用户自定的Bridge网络驱动和默认的Bridge网络驱动类似,那么如何创建一个用户自定义的Bridge网络驱动呢。最简单的方法就是docker network create 语句,更多用法可以参照docker network Command-line reference
比如创建一个叫lazyallen的User-Defined Bridge Network,其中-d是drive的缩写,且是可选项,若不指定默认创建的是Bridge类型的网络驱动。

1
2
3
4
5
6
7
8
➜  ~ docker network create -d bridge lazyallen
75561be3d1901b8213b795c3c9113392375fe6d57aea2eb1e9b097e2bdc0c0bd
➜ ~ docker network ls
NETWORK ID NAME DRIVER SCOPE
04675183a24d bridge bridge local
245b36b452af host host local
75561be3d190 lazyallen bridge local
43a8d2311d38 none null local

可见,的确出现了一个叫lazyallen的Bridge Network。和默认的Bridge网络驱动相比,在用法上的差别就是 User-Defined Bridge Networks 提供了容器间自动的DNS解析。
什么意思呢?我们同样创建两个容器,让他们都使用lazyallen Bridge Driver,使用 –network
可指定network driver

1
2
docker run --rm -it --name bb-user-defined-bridge-0 --network lazyallen busybox sh
docker run --rm -it --name bb-user-defined-bridge-1 --network lazyallen busybox sh

容器在网络通信时,会将容器名直接DNS解析为目标容器IP。

1
2
3
4
PING bb-user-defined-bridge-0 (172.18.0.2): 56 data bytes
64 bytes from 172.18.0.2: seq=0 ttl=64 time=0.176 ms
64 bytes from 172.18.0.2: seq=1 ttl=64 time=0.175 ms
64 bytes from 172.18.0.2: seq=2 ttl=64 time=0.185 ms

这样最大的好处就是,容器之间不用再使用IP作为hardcode嵌入到配置中,只要使用容器名称作为引用即可,就算因为网络环境导致容器的IP动态变化,也不用再修改配置。

同样,下面这张图片就可以很好解释Bridge network driver

回过头解释

1
2
3
4
5
6
docker network create simple-network

docker run --name xxl-mysql --network simple-network -e MYSQL_ROOT_PASSWORD=123456 -d mysql

docker run --network simple-network -e PARAMS="--spring.datasource.url=jdbc:mysql://xxl-mysql:3306/xxl_job?Unicode=true&characterEncoding=UTF-8&useSSL=false --spring.datasource.password=123456" -p 8080:8080 -v /tmp:/data/applogs --name xxl-job-admin -d xuxueli/xxl-job-admin:2.1.2

通过以上解释,这里就显而易见地做了3件事

  • 创建了一个叫simple-network的自定义Bridge 网络驱动
  • 创建两个容器时均指定simple-network网络驱动
  • 后端应用的配置文件将MySql的地址使用xxl-mysql引用

参考链接

[^cnkirito_footnote]: 引用自文章「Docker Network—Bridge 模式」的翻译