Docker 四种网络模型介绍

10,157 阅读11分钟

1. 前言

前面在调试一个 docker-compose 启动 zk 集群时,由于怀疑是 Docker 的网络问题,于是花了点时间研究了一下 Docker 的网络配置。这个是笔记。

这篇文章会先假设大家对 IP 层和链路层有一定的了解,但我在第 3 部分 概念 上也有补充说明。

参考连接上也会附上一些扩展阅读的资料。

2. 拓扑

3. 概念

在讨论网络通信前,必须有一个关键的概念要清楚,这样理解 Docker 的网络配置。

3.1 IP和Port

这是网络通信的原子概念。只有一个IP和一个Port才能锁定一个通信目标。通常来说就是一个进程。一个 IP 和 Port 会指定一个进程。

3.2 网络

多个进程在同一个子网中通信,称之为一个子网。在同一个子网中,不同的进程可以通过 IP 和 Port 进行数据通信。一般形容一个子网,会有子网掩码、网关的概念。在讨论网络这一层,我们可以忽略 Port 这个参数。

"Subnet": "172.18.0.0/16",
"Gateway": "172.18.0.1"
一个 Docker 的 子网

在如上一个网络中,假如主机 A(172.18.0.2) 要向主机 B(172.18.0.3) 发送数据,由于 A 自己的掩码配置时 255.255.0.0 , 所以他能够发现自己的数据包是可以直接发送到到链路中的。假如要发送到 C(173.1.1.1),A 会主动把数据包发送给 网关,由网关帮忙转发(当然,发送数据包前,得先完成IP 可达性探索)。

3.3 网桥 bridge

这是一个链路层(L2)的设备。这是组建网络的一种设备。有了它,就可以使得多个网络能像单个网络一样工作。不同网络的主机可以互联。

# ifconfig (物理主机)
br-3caa0c53581b Link encap:Ethernet  HWaddr 02:42:0e:a5:45:5b
          inet addr:172.18.0.1  Bcast:172.18.255.255  Mask:255.255.0.0
          UP BROADCAST MULTICAST  MTU:1500  Metric:1
          RX packets:338 errors:0 dropped:0 overruns:0 frame:0
          TX packets:385 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:17445 (17.4 KB)  TX bytes:29882 (29.8 KB)

Bridged 模式是 Docker 容器和主机一个非常常用的通信模式。当我们启动了一个容器,并且希望把端口委托给宿主机完成转发时。就会使用这种模式。在这种模式下,物理主机会给自己虚拟构建一个网卡,并接入该网络,并且身份就是网关

这里就可以思考一下,在一个容器内 ping baidu.com,其实通过通过虚拟网桥,把数据包提交到物理主机,再发送出去。

4. Docker 网络原型

Docker 有四种网络原型。

  • Closed 容器
  • Joined 容器
  • Bridged 容器
  • Open 容器

4.1 Closed 容器

非常简单的一种,这个容器并不会接入任何的子网络,它只能访问本地回环接口。它的数据包也不会通过虚拟网桥发送出来。

// ping dns
root@ubuntu:~# docker run --rm --net none alpine:latest ping -w 2 *.*.*.*(河蟹)
PING *.*.*.* (*.*.*.*): 56 data bytes
ping: sendto: Network unreachable

// ping 回环接口
root@ubuntu:~# docker run --rm --net none alpine:latest ping -w 2 127.0.0.1
PING 127.0.0.1 (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: seq=0 ttl=64 time=0.090 ms
64 bytes from 127.0.0.1: seq=1 ttl=64 time=0.088 ms

--- 127.0.0.1 ping statistics ---
3 packets transmitted, 2 packets received, 33% packet loss
round-trip min/avg/max = 0.088/0.089/0.090 ms

4.2 Bridged 容器

比 Closed 开放了网络的隔离程度,被认为是最佳实践。这类容器会拥有两个网络接口

  • 本地回环接口
  • 私有接口 通过网桥连接到宿主机的其他容器

测试:查看 bridge 容器的网络接口。有两个接口,一个是连接 Docker 0 的,一个是本地回环接口。

root@ubuntu:~# docker run --rm --net bridge alpine:latest 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:0 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:0 (0.0 B)  TX bytes:0 (0.0 B)

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

4.2.1 容器的自定义 DNS 解析

我们尝试去连接 MySQL 的时候,除了会直接连接 IP,还可以使用域名去连接。使用后者的是会带来非常便捷的功能,一个应用、客户端不用依赖 IP,而转为依赖域名,这样会让我们的配置解耦,配置文件不用时常改变。

因此我们也希望,Web 容器在依赖 MySQL 容器的时候,是通过域名,而不是 IP 去连接。

测试:通过 --hostname,为容器增加 DNS 配置。shi

root@ubuntu:~/compose_software/zookeeper_cluster# docker run --rm --hostname barker alpine:latest ping barker
PING barker (172.17.0.2): 56 data bytes
64 bytes from 172.17.0.2: seq=0 ttl=64 time=0.091 ms
64 bytes from 172.17.0.2: seq=1 ttl=64 time=0.085 ms
64 bytes from 172.17.0.2: seq=2 ttl=64 time=0.083 ms
64 bytes from 172.17.0.2: seq=3 ttl=64 time=0.080 ms
^C64 bytes from 172.17.0.2: seq=4 ttl=64 time=0.078 ms

--- barker ping statistics ---
5 packets transmitted, 5 packets received, 0% packet loss
round-trip min/avg/max = 0.078/0.083/0.091 ms

// 原理也非常简单,只是往容器增加一个域名解析
root@ubuntu:~/compose_software/zookeeper_cluster# docker run --rm --hostname barker alpine:latest cat /etc/hosts
127.0.0.1       localhost
::1     localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.17.0.2      barker  # 请看这个!

4.2.2 开放接口

我们上文讨论的接口,都仅仅是把容器当成一个主机来看待。一个 Web 容器启动后,总是需要公开自己,向宿主机以外的网络提供服务。以80端口为例,假如要开发 Http 服务,就必须要接收到宿主机的80端口请求,并转发到容器中。

这其实是很熟悉的操作了,说的就是 -P 80:80 的宿主机端口和容器端口的绑定操作。

root@ubuntu:~/compose_software/zookeeper_cluster# docker-compose ps
          Name                        Command               State                          Ports
------------------------------------------------------------------------------------------------------------------------
zookeeper_cluster_zoo1_1   /docker-entrypoint.sh zkSe ...   Up      0.0.0.0:2181->2181/tcp, 2888/tcp, 3888/tcp, 8080/tcp
zookeeper_cluster_zoo2_1   /docker-entrypoint.sh zkSe ...   Up      0.0.0.0:2182->2181/tcp, 2888/tcp, 3888/tcp, 8080/tcp
zookeeper_cluster_zoo3_1   /docker-entrypoint.sh zkSe ...   Up      0.0.0.0:2183->2181/tcp, 2888/tcp, 3888/tcp, 8080/tcp

宿主机分别绑定了:2181、2182、2183 端口,并转发到响应的 ZooKeeper 容器中。Docker 并没有在转发的问题上做太多创新,我查阅了一些关于端口转发的技术,认为这是使用了 NAPT 技术,这种技术会在网关处维护一张带有 IP 以及端口号的 NAT 表,类似于下面的。这样,多个容器就可以共享一个宿主机IP了。当然,这仅仅是我的一种推导猜测,并没严格证明。

内网IP 外网IP
192.168.1.55:5566 219.152.168.222:9200
192.168.1.59:80 219.152.168.222:9201
192.168.1.59:4465 219.152.168.222:9202

4.2.3 容器间通信

我会尝试在宿主机上搭建 ZK 集群和 Kafka 集群,这样就意味着,我需要处理一些关于容器间的通信问题。

我通过 docker-compose.yml 启动了一个 ZK 集群。Docker 帮我构建了一个默认的网络zookeeper_cluster_default, 子网是172.18.0.0/16。毫无疑问,子网内的 ZK 节点是互相连通的。

root@ubuntu:~/compose_software/kafka_cluster# docker inspect zookeeper_cluster_default
[
    {
        "Name": "zookeeper_cluster_default",
        "Id": "3caa0c53581bcf694128451416f54ff1ecb786dc013f13bdb130d18aeb67aa2a",
        "Created": "2020-06-05T23:10:36.409186068+08:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.18.0.0/16",
                    "Gateway": "172.18.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": true,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "477beaab870af74d94fde666652ba3e8891b376815d2a4d2270ca35c6d89cdc4": {
                "Name": "zookeeper_cluster_zoo3_1",
                "EndpointID": "2c15895b1c58e037378e49b0af5d1aeec9c85bdaff4c52767df65f4caaaa9000",
                "MacAddress": "02:42:ac:12:00:03",
                "IPv4Address": "172.18.0.3/16",
                "IPv6Address": ""
            },
            "4fd6de645117cfe6ac147b33f4151f009647b9e72fa6c749dc81a30d79129d34": {
                "Name": "zookeeper_cluster_zoo2_1",
                "EndpointID": "beb516396282e33bd5e207436592fc104e6a613d0a54cb24102de7948618d46f",
                "MacAddress": "02:42:ac:12:00:02",
                "IPv4Address": "172.18.0.2/16",
                "IPv6Address": ""
            },
            "d3eb5893f1e8f03eef6f72a3c14a955ed303f61df9583a88b63edda0d8f3dfa2": {
                "Name": "zookeeper_cluster_zoo1_1",
                "EndpointID": "dcea290111b059c35eb0b0c1385c822a5a87a973f9c8e28b86aa311a0d9b9f8e",
                "MacAddress": "02:42:ac:12:00:04",
                "IPv4Address": "172.18.0.4/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {
            "com.docker.compose.network": "default",
            "com.docker.compose.project": "zookeeper_cluster",
            "com.docker.compose.version": "1.25.5"
        }
    }
]

Bridged 网络模式是 Docker 的重点,所以多说一点。后面的两个容器,简单了解一下其特定即可。

4.3 Joined 容器

在此种模式下,容器间会共享同一个网络接口。这个可以理解为:两个进程共享一个网络接口,他们的应用端口会冲突的。

4.4 Open 容器

在此种模式下,容器是和主机直接共享网络接口的,意味着容器对主机网络有完全的访问权,包括重要的访问权。所以这是一种非常危险的模式。

为什么说这个很危险呢?因为容器也是有自己的 root 权限的,假如直接把网络接口交给容器,它也可以使用 root 直接对网络接口进行最高级别权限的操作。

// 从容器获取宿主机的网络接口
root@ubuntu:~/compose_software/kafka_cluster# docker run --rm --net host alpine:latest ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: eno1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 90:b1:1c:68:d2:a6 brd ff:ff:ff:ff:ff:ff
    inet 192.168.31.210/24 brd 192.168.31.255 scope global eno1
       valid_lft forever preferred_lft forever
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
    link/ether 02:42:77:c3:cb:6b brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
7: veth7fa20b1@if6: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue master docker0 state UP
    link/ether 96:92:14:ee:52:64 brd ff:ff:ff:ff:ff:ff
9: veth4dc18fd@if8: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue master docker0 state UP
    link/ether c6:a9:25:e6:da:35 brd ff:ff:ff:ff:ff:ff
17: veth5e4791a@if16: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue master docker0 state UP
    link/ether 62:cc:9c:46:cf:6d brd ff:ff:ff:ff:ff:ff
18: br-3caa0c53581b: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
    link/ether 02:42:0e:a5:45:5b brd ff:ff:ff:ff:ff:ff
    inet 172.18.0.1/16 brd 172.18.255.255 scope global br-3caa0c53581b
       valid_lft forever preferred_lft forever
280: veth4929fcd@if279: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue master br-3caa0c53581b state UP
    link/ether 3a:d1:86:fc:5f:d8 brd ff:ff:ff:ff:ff:ff
282: veth9bc379c@if281: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue master br-3caa0c53581b state UP
    link/ether c2:f4:58:ae:c8:5d brd ff:ff:ff:ff:ff:ff
284: veth2845296@if283: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue master br-3caa0c53581b state UP
    link/ether 7e:7f:a1:db:4c:bb brd ff:ff:ff:ff:ff:ff
55: br-04802571e5c4: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN
    link/ether 02:42:50:a5:4e:57 brd ff:ff:ff:ff:ff:ff
    inet 172.19.0.1/16 brd 172.19.255.255 scope global br-04802571e5c4
       valid_lft forever preferred_lft forever

后记

至此,Docker 的网络暂时介绍完了,从这篇文章中,希望你可以了解到几个基础的计算机网络概念,还有 Docker 的几种网络级别。其中 Bridged 模式是需要进一步了解的,因为它用得最多。

另外,在了解和学习 Docker 的网络通信,以往的计算机网络知识会大大加快学习的速度。我觉得 Docker 在这个网络通信层上,没使用太多技术的创新,仅仅是把一些抽象概念给命令话,让我们轻松使用而已。

因此,在阅读这篇文章时,我会假设你对 IP 层和链路层都有一定的了解,这些抽象的概念不熟悉,估计读起来会比较困难。我在编写这篇文章的过程中,也刚好复习了一下计网的知识。

参考