写点什么

Docker 容器网络的七种武器

作者:王玉川
  • 2023-04-20
    上海
  • 本文字数:13148 字

    阅读完需:约 43 分钟

Docker容器网络的七种武器

知识,学过了之后,把它总结、分享出来,能让自己对它的理解更加的深入。


因此,把以前学的 Docker 容器网络模型归纳总结、并进行实验。


后续再继续对 Kubernetes、CNI 进行总结实验。




Docker 对网络的支持,可以用如下的思维导图来表示:


包括了 None、Host、Bridge、Container、Overlay、Macvlan、IPvlan 七种模型。


下面,针对每种网络模型进行介绍与实验。

一. 拔网线 - None 模型

None,啥都没有,类似于把网线给拔掉了。所以,这种模式下的容器,是一个封闭的环境。


适用于安全性高、又不需要网络访问的情景。


运行容器时,指定:--network=none 即可。


$ docker run -it --rm --name=bbox --network=none busybox sh
复制代码


运行一个 BusyBox 的容器,然后在容器内可以看到:


/ # ip link1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
复制代码


该容器除了一个 localhost 的网卡,并没有对外进行网络通信的设备。

二. 寄生 - Host 模型

使用该模式的容器,共享 Host 宿主机(运行 Docker 的机器)的网络栈、网卡设备。


这种情况下,容器的网络性能很好。但是不灵活,容器的端口容易与 Host 的端口冲突。Host A 上能正常运行,换到了 Host B 未必就能正常运行。根据我的经验,这种模式很少有实际应用。


运行容器时,指定:--network=host 即可。


$ docker run -it --rm --name=bbox --network=host busybox sh
复制代码


在容器内看到的网卡信息:


/ # ip addr1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000    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    inet6 ::1/128 scope host       valid_lft forever preferred_lft forever2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel qlen 1000    link/ether 00:0c:29:49:39:91 brd ff:ff:ff:ff:ff:ff    inet **192.168.111.128**/24 brd 192.168.111.255 scope global dynamic noprefixroute ens33       valid_lft 1242sec preferred_lft 1242sec    inet6 fe80::72bf:3960:42cd:13cb/64 scope link noprefixroute       valid_lft forever preferred_lft forever3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue    link/ether 02:42:65:3a:0c:37 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    inet6 fe80::42:65ff:fe3a:c37/64 scope link       valid_lft forever preferred_lft forever
复制代码


与在 Host 宿主机看到的信息是一致的:


$ ip addr1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000    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    inet6 ::1/128 scope host       valid_lft forever preferred_lft forever2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000    link/ether 00:0c:29:49:39:91 brd ff:ff:ff:ff:ff:ff    altname enp2s1    inet **192.168.111.128**/24 brd 192.168.111.255 scope global dynamic noprefixroute ens33       valid_lft 1148sec preferred_lft 1148sec    inet6 fe80::72bf:3960:42cd:13cb/64 scope link noprefixroute       valid_lft forever preferred_lft forever3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default    link/ether 02:42:65:3a:0c:37 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    inet6 fe80::42:65ff:fe3a:c37/64 scope link       valid_lft forever preferred_lft forever
复制代码


从容器内访问网络,一切正常:


/ # ping 8.8.8.8PING 8.8.8.8 (8.8.8.8): 56 data bytes64 bytes from 8.8.8.8: seq=0 ttl=53 time=34.735 ms64 bytes from 8.8.8.8: seq=1 ttl=53 time=35.659 ms64 bytes from 8.8.8.8: seq=2 ttl=53 time=35.603 ms64 bytes from 8.8.8.8: seq=3 ttl=53 time=35.723 ms
复制代码

三. 搭桥 - Bridge 模型

这是 Docker 在运行容器时,默认的网络模型。


Docker 在安装时,会自动在系统里面创建一个叫做 docker0 的网桥:


$ ip addr......3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default    link/ether 02:42:65:3a:0c:37 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    inet6 fe80::42:65ff:fe3a:c37/64 scope link       valid_lft forever preferred_lft forever
复制代码


继续查看该网桥的详细信息:


$ docker network inspect bridge[    {        "Name": "bridge",        "Id": "d92abe90e2bc79d8a4cd5ae73138d8da7aa0684a6a170fe7fc0ade4518057440",        "Created": "2023-04-18T11:02:11.199720084+08:00",        "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": {},        "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": {}    }]
复制代码


根据上面的信息,Docker 在运行容器时,会在 172.17.0.0/16 网段,为容器分配 IP 地址,并把 Gateway 指向 172.17.0.1,即 docker0 这个虚拟设备。


而且,Docker 会为运行的容器创建一对 veth。该 veth pair,一端接在容器内部,另一端接在 docker0 上。使得容器可以通过 docker0 与外界通信。


运行一个容器,可以看到容器里面的网络设备:


$ docker run -it --rm --name=bbox busybox sh/ # ip addr1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000    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 forever6: eth0@if7: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0       valid_lft forever preferred_lft forever
复制代码


容器内部有 eth0 网卡,它是 veth 的一端。在 Host 上,可以看到 veth 的另一端:


$ brctl showbridge name        bridge id            STP enabled        interfacesdocker0            8000.0242653a0c37    no                vethe22e54f
复制代码


网络拓扑参考官方原图:



1. 相同 Host 上的容器间网络通信


在这种模式下,同一个 Host 上的不同容器,可以通过 docker0 直接通信。比如运行一个 Nginx 的容器:


$ docker run -it --rm --name=web nginx/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d//docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh/docker-entrypoint.sh: Configuration complete; ready for start up2023/04/18 03:50:39 [notice] 1#1: using the "epoll" event method2023/04/18 03:50:39 [notice] 1#1: nginx/1.23.42023/04/18 03:50:39 [notice] 1#1: built by gcc 10.2.1 20210110 (Debian 10.2.1-6)2023/04/18 03:50:39 [notice] 1#1: OS: Linux 5.19.0-38-generic2023/04/18 03:50:39 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1048576:10485762023/04/18 03:50:39 [notice] 1#1: start worker processes2023/04/18 03:50:39 [notice] 1#1: start worker process 282023/04/18 03:50:39 [notice] 1#1: start worker process 29
复制代码


在另一个 shell 窗口里,先查看该容器的 IP 地址,确定为 172.17.0.2:


$ docker network inspect bridge[    {        "Name": "bridge",        ......        "IPAM": {            "Driver": "default",            "Options": null,            "Config": [                {                    "Subnet": "172.17.0.0/16",                    "Gateway": "172.17.0.1"                }            ]        },        ......        "Containers": {            "d3025a31c47c80bdcf711f329ff3c70677c28bfaed08d700743d91ad1bc33f15": {                "Name": "web",                "EndpointID": "d6f319d19fac52189c70fddb3a732f5b4081ff16fbe21e859c647ff8ff8ae7e6",                "MacAddress": "02:42:ac:11:00:02",                **"IPv4Address": "172.17.0.2/16",**                "IPv6Address": ""            }        },        ......    }]
复制代码


然后,运行一个 BusyBox 的容器:


$ docker run -it --rm --name=bbox busybox sh/ # wget 172.17.0.2:80Connecting to 172.17.0.2:80 (172.17.0.2:80)saving to 'index.html'index.html           100% |********************************************************************************************************************************************************************************|   615  0:00:00 ETA'index.html' saved
复制代码


可以看到,BusyBox 容器成功的访问了 Nginx 容器。


2. 容器与外部网络通信


2.1. 从内到外


Bridge 模式下的容器,默认就可以访问外部网络。它依靠 Host 上的 iptables,做了 NAT 地址转换。


启动一个 BusyBox 的容器,得到的 IP 是 172.17.0.2。它的 Host IP 是:192.168.111.128。在容器内可以直接访问外部的另一台机器:192.168.111.129。


$ docker run -it --rm --name=bbox busybox sh/ # ip addr1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000    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 forever12: eth0@if13: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0       valid_lft forever preferred_lft forever/ # ping 192.168.111.129PING 192.168.111.129 (192.168.111.129): 56 data bytes64 bytes from 192.168.111.129: seq=0 ttl=63 time=0.521 ms64 bytes from 192.168.111.129: seq=1 ttl=63 time=0.465 ms
复制代码


2.2. 从外到内


如果需要外部网络访问 Bridge 模式下的容器,可以通过端口映射功能。在运行容器时,指定 Host 端口 A 与容器端口 B 的映射。然后,通过访问:Host-IP:Host 端口 A,即可映射到:容器:容器端口 B。


我们做个试验,把前面的 Nginx 容器和 BusyBox 容器全部退出。然后重新运行一个新的 Nginx 容器,并通过-p 参数指定端口映射:


$ docker run -it --rm --name=web -p 8080:80 nginx
复制代码


从另外一台机器发起访问,192.168.111.128 是 Host 的 IP 地址:


$ wget 192.168.111.128:8080Connecting to 192.168.111.128:8080... connected.HTTP request sent, awaiting response... 200 OKLength: 615 [text/html]Saving to: ‘index.html’
index.html 100%[===============================================================================================================================>] 615 --.-KB/s in 0s
‘index.html’ saved [615/615]
复制代码

四. 如影随形 - Container 模型

这个模式我没看到官方的名字,名字我瞎取的,但是在 Kubernetes 的 Pod 里面经常用。


具体的做法,是在容器 B 运行时,指定:--network=container:容器 A 的名字或者 ID。


这样,容器 A、B 处于同一个网络空间。它们的 MAC 地址、IP 地址都一样,共享网络栈、网卡和配置信息。它们可以通过 127.0.0.1 直接通信。


在 Kubernetes 部署 Pod 的时候,就会用到这个模式。针对每个 Pod,Kubernetes 先启动 Pause 容器,然后再启动其它容器并使用 Pause 容器的网络。这样,同一个 Pod 之内的容器,共享了同一个网络空间,可以高效的通信。


试验看看,先启动一个 Nginx 容器:


$ docker run -it --rm --name=web nginx
复制代码


看看 Docker 网络情况:


$ docker network inspect bridge[    {        "Name": "bridge",        ......        "IPAM": {            "Driver": "default",            "Options": null,            "Config": [                {                    "Subnet": "172.17.0.0/16",                    "Gateway": "172.17.0.1"                }            ]        },        ......        "Containers": {            "da67b03b02c2e99fdaaa2fd75b7829c4005eba80a50f39404db4da8d8defa0e3": {                "Name": "web",                "EndpointID": "0e29b7473a8446100dc45a711536b0277ae0f911cb4c2decc7245511fd2dbb02",                **"MacAddress": "02:42:ac:11:00:02",**                **"IPv4Address": "172.17.0.2/16",**                "IPv6Address": ""            }        },    }]
复制代码


Nginx 容器的 IP 地址是:172.17.0.2,MAC 地址是:02:42:ac:11:00:02。


再启动一个 BusyBox 容器,并使用 Nginx 的网络:


$ docker run -it --rm --name=bbox --network=container:web busybox sh/ # ip addr1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000    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 forever16: eth0@if17: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue    link/ether **02:42:ac:11:00:02** brd ff:ff:ff:ff:ff:ff    inet **172.17.0.2**/16 brd 172.17.255.255 scope global eth0       valid_lft forever preferred_lft forever
复制代码


可以看到,BusyBox 的 IP 地址、MAC 地址,和 Nginx 的一模一样。


直接访问 127.0.0.1 可以得到:


/ # wget 127.0.0.1Connecting to 127.0.0.1 (127.0.0.1:80)saving to 'index.html'index.html           100% |********************************************************************************************************************************************************************************|   615  0:00:00 ETA'index.html' saved
复制代码


这种模式除了 K8S 的 Pod 之外,还可以用在简易版的 Web Server + App Server 情景。

五. 套娃 - Overlay 模型

Docker 通过 Overlay 模式,实现了对 VXLAN 的支持。这个模式的环境搭建比别的模式稍显复杂,主要是因为需要有一个地方来保存各个节点在 overlay 网络中的配置信息。一般是在另一个机器安装 etcd 或者 Consul 这种 key-value 数据库。


偷懒起见,我直接使用了 Docker 自带的 Swarm 来搭建环境。准备了两台机器 A、B。A 身兼两职,既保存数据库,又运行容器。


  • 首先,在机器 A,初始化 swarm:


  ycwang@ycwang-ubuntu:~$ docker swarm init  Swarm initialized: current node (qygp7ymrfh5g0lgky10teck4r) is now a manager.   To add a worker to this swarm, run the following command:       docker swarm join --token SWMTKN-1-3rjaah******rowo 192.168.111.128:2377   To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.
复制代码


  • 换到机器 B,Copy 上面的 join 命令,加入集群:


  ycwang@ycwang-ubuntu-slave:~$ docker swarm join --token SWMTKN-1-3rjaah******rowo 192.168.111.128:2377  This node joined a swarm as a worker.
复制代码


  • 回到机器 A,可以看到集群的情况:


  ycwang@ycwang-ubuntu:~$ docker node ls  ID                            HOSTNAME              STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION  qygp7ymrfh5g0lgky10teck4r *   ycwang-ubuntu         Ready     Active         Leader           23.0.4  sr637j4g891bsxo56tesv55y8     ycwang-ubuntu-slave   Ready     Active                          23.0.4
复制代码


  • 在机器 A 上,可以看到 Docker 为 Overlay 模式,创建了两个新的网络,docker_gwbridge 和 ingress。后面运行的容器,会通过 docker_gwbridge 与外部网络进行通信(南北向流量):


  ycwang@ycwang-ubuntu:~$ docker network ls  NETWORK ID     NAME              DRIVER    SCOPE  51276c2e1741   bridge            bridge    local  596dbdd24c3a   docker_gwbridge   bridge    local  fc504698f255   host              host      local  tmbwbg86eph4   ingress           overlay   swarm  4829db6948ad   none              null      local
复制代码


  • 在机器 A 上,为 Docker 创建 Overlay 网络:


  ycwang@ycwang-ubuntu:~$ docker network create --driver=overlay vxlanA  thya4qliq95dh81yndfqpimwn
复制代码


  • 在机器 A 上,创建服务,使用 vxlanA 这个网络,replicas 指定为 2:


  ycwang@ycwang-ubuntu:~$ docker service create --network=vxlanA --name bboxes --replicas 2 busybox ping 8.8.8.8  q44lh7mwwpgbae7fleilgenk2  overall progress: 2 out of 2 tasks   1/2: running   [==================================================>]   2/2: running   [==================================================>]   verify: Service converged
复制代码


注意,busybox 后面的 ping 8.8.8.8,并不是为了让它去 ping,目的只是让这个容器不要马上退出,否则 Service 会不停的重启这两个容器。别问我为什么知道的……


  • 分别在两个机器上查看容器的信息:


  ycwang@ycwang-ubuntu:~$ docker ps   CONTAINER ID   IMAGE            COMMAND          CREATED          STATUS          PORTS     NAMES  a9f2b06f0f9e   busybox:latest   "ping 8.8.8.8"   16 minutes ago   Up 16 minutes             bboxes.2.m6qqi8k75rvr8ukk4ll6jfrnp  ycwang@ycwang-ubuntu:~$ docker exec -it a9f2b06f0f9e sh  / # ip addr  1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000      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  62: eth0@if63: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1450 qdisc noqueue       link/ether 02:42:0a:00:01:21 brd ff:ff:ff:ff:ff:ff      inet 10.0.1.33/24 brd 10.0.1.255 scope global eth0         valid_lft forever preferred_lft forever  64: eth1@if65: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue       link/ether 02:42:ac:13:00:03 brd ff:ff:ff:ff:ff:ff      inet 172.19.0.3/16 brd 172.19.255.255 scope global eth1         valid_lft forever preferred_lft forever
复制代码


  ycwang@ycwang-ubuntu-slave:~$ docker ps -a  CONTAINER ID   IMAGE            COMMAND          CREATED          STATUS          PORTS     NAMES  04aacff98016   busybox:latest   "ping 8.8.8.8"   18 minutes ago   Up 18 minutes             bboxes.1.ky0fcmy5geudr2fothxxh05y3  ycwang@ycwang-ubuntu-slave:~$ docker exec -it 04aacff98016 sh  / # ip addr  1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000      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  62: eth0@if63: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1450 qdisc noqueue       link/ether 02:42:0a:00:01:20 brd ff:ff:ff:ff:ff:ff      inet 10.0.1.32/24 brd 10.0.1.255 scope global eth0         valid_lft forever preferred_lft forever  64: eth1@if65: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue       link/ether 02:42:ac:13:00:03 brd ff:ff:ff:ff:ff:ff      inet 172.19.0.3/16 brd 172.19.255.255 scope global eth1         valid_lft forever preferred_lft forever
复制代码


可以发现,每个容器会带两张网卡。


eth1 - 172.19.0.3 接在前面的 docker_gwbridge 网桥上,负责与外部网络的南北向流量。通过 docker network inspect docker_gwbridge 可以确认这个信息。


etho - 10.0.1.32/24,10.0.1.33/24,属于 vxlanA 网络,负责 VXLAN 内部的东西向流量。通过 docker network inspect vxlanA 可以确认这个信息。


  • 从容器 A ping 容器 B:


  / # ping 10.0.1.32  PING 10.0.1.32 (10.0.1.32): 56 data bytes  64 bytes from 10.0.1.32: seq=0 ttl=64 time=0.735 ms  64 bytes from 10.0.1.32: seq=1 ttl=64 time=0.556 ms
复制代码


两个容器之间是可以通信的。此时,用 tcpdump 在 Host 宿主机的网卡上抓包:


  $ sudo tcpdump -i ens33 udp port 4789 -s 0 -X -nnn -vvv  tcpdump: listening on ens33, link-type EN10MB (Ethernet), snapshot length 262144 bytes  15:50:57.566431 IP (tos 0x0, ttl 64, id 34769, offset 0, flags [none], proto UDP (17), length 134)      192.168.111.128.42627 > 192.168.111.129.4789: [bad udp cksum 0x60d6 -> 0xb9a9!] VXLAN, flags [I] (0x08), vni 4097  IP (tos 0x0, ttl 64, id 30207, offset 0, flags [DF], proto ICMP (1), length 84)      10.0.1.33 > 10.0.1.32: ICMP echo request, id 23, seq 0, length 64    0x0000:  4500 0086 87d1 0000 4011 9243 c0a8 6f80  E.......@..C..o.    0x0010:  c0a8 6f81 a683 12b5 0072 60d6 0800 0000  ..o......r`.....    0x0020:  0010 0100 0242 0a00 0120 0242 0a00 0121  .....B.....B...!    0x0030:  0800 4500 0054 75ff 4000 4001 ae69 0a00  ..E..Tu.@.@..i..    0x0040:  0121 0a00 0120 0800 ae96 0017 0000 bbd5  .!..............    0x0050:  8d7c 0000 0000 0000 0000 0000 0000 0000  .|..............    0x0060:  0000 0000 0000 0000 0000 0000 0000 0000  ................    0x0070:  0000 0000 0000 0000 0000 0000 0000 0000  ................    0x0080:  0000 0000 0000                           ......
复制代码


可以清楚的看到从容器 A - 10.0.1.33 到容器 B - 10.0.1.32 的 ICMP 数据包,被 VNI 为 4097 的 VXLAN 封装。


封装后,变成了从 Host A - 192.168.111.128.42627 到 Host B - 192.168.111.129.4789 的 UDP 数据包。


通过这个模型,实现了容器间的直连,虚拟的二层直连。Overlay 模型也成了许多云厂商采用的实现方案。

六. 狡兔三窟 - Macvlan 模型

Macvlan 是一种网卡虚拟化技术,将一张物理网卡(父接口)虚拟出多张网卡(子接口)。每个子接口有自己独立的 MAC 地址和 IP 地址。


物理网卡(父接口)相当于一个交换机,记录着对应的虚拟网卡(子接口)和 MAC 地址。当物理网卡收到数据包后,会根据目的 MAC 地址判断这个包属于哪一个虚拟网卡,并转发给它。


Macvlan 技术有四种模式,Docker 支持其中的 bridge 模式。


接下来,试验看看。


  • 首先,需要打开网卡的混杂模式,否则它拒绝接收 MAC 地址跟它不一样的数据报文。ens33 是 Host 机器的物理网卡:


  $ sudo ip link set ens33 promisc on
复制代码


  • 第二步,为 Docker 创建一个 Macvlan 网络。子网是:192.168.111.0/24,跟 Host 一样;指定父接口为 ens33


  $ docker network create --driver=macvlan --subnet=192.168.111.0/24 --gateway=192.168.111.1 -o parent=ens33 macvnet  0283990d6acdc9df87d5b34a999c05266e12a4423aa0041387373d8bc5ee042c
复制代码


  • 第三步,运行容器,指定其 IP 地址为:192.168.111.10,并使用上一步创建的 Macvlan 网络:


  $ docker run -it --rm --name=web --ip=192.168.111.10 --network=macvnet nginx
复制代码


这样,Nginx 这个容器就运行在了 192.168.110.10 这个地址上,从外部机器可以直接访问它:


$ wget 192.168.111.10Connecting to 192.168.111.10:80... connected.HTTP request sent, awaiting response... 200 OKLength: 615 [text/html]Saving to: ‘index.html’
index.html 100%[===============================================================================================================================>] 615 --.-KB/s in 0s
‘index.html’ saved [615/615]
复制代码


可以看到,Macvlan 是一种将容器通过二层,连接到物理网络不错的方案,配置简单、性能好。但它也有一些局限性,比如:物理网卡所连接的交换机,可能会限制同一个物理端口上的 MAC 地址数量。许多物理网卡上的 MAC 地址数量也有限制。

七. 狡兔三窟 Plus - IPvlan 模型

IPvlan 是一个比较新的特性,Linux 内核>= 4.2 之后才可以稳定的使用。


与 Macvlan 类似,都是从一个物理网卡(父接口)虚拟出多张网卡(子接口)。与 Macvlan 不同的是,这些子接口的 MAC 地址都是一样的,不一样的只是它们的 IP 地址。而且,它不像 Macvlan 那样,要求物理网卡打开混杂模式。


IPvlan 有两种模式:L2 和 L3 模式。顾名思义,L2 模式跟交换机有关,L3 模式则跟路由器有关。


1. L2 模式


IPvlan 的 L2 模式,跟之前的 Macvlan 非常类似。容器的子接口与父接口在同一子网,父接口做为交换机来转发子接口的数据。如果是与外部网络通信,则依赖父接口进行路由转发。


首先,为 Docker 创建一个 L2 模式的 IPvlan 网络:


$ docker network create --driver=ipvlan --subnet=192.168.0.0/24 --gateway=192.168.0.1 -o ipvlan_mode=l2 -o parent=ens33 ipv_l27091b861fd44798d21be6d3dcdd03e79c68d65e9149862b9f21bca42678fda19
复制代码


该网络与 Host 宿主机同在 192.168.0.0/24 网段,Host 的网卡是 ens33,IP 是 192.168.0.105。


使用该网络,运行第一个容器:


$ docker run -it --rm --network=ipv_l2 --name=bbox1 busybox sh/ # ip addr1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000    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 forever4: eth0@if2: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue     link/ether 00:0c:29:12:5b:f4 brd ff:ff:ff:ff:ff:ff    inet 192.168.0.2/24 brd 192.168.0.255 scope global eth0       valid_lft forever preferred_lft forever
复制代码


该容器的 IP 地址为 192.168.0.2。MAC 地址与 Host 宿主机的 ens33 一致。


从容器内,访问另外一台机器 B:


/ # ping 192.168.0.112PING 192.168.0.112 (192.168.0.112): 56 data bytes64 bytes from 192.168.0.112: seq=0 ttl=64 time=79.549 ms
复制代码


从另外的机器 B,访问该容器:


$ ping 192.168.0.2PING 192.168.0.2 (192.168.0.2) 56(84) bytes of data.64 bytes from 192.168.0.2: icmp_seq=1 ttl=64 time=3.94 ms
复制代码


可以看到,容器的网络访问都是没问题的。


2. L3 模式


这个模式下,容器跟 Host 宿主机可以不在同一个子网。该模式的配置,网上的资料比较少,Docker 官网也是语焉不详的。


假设 Host 宿主机的父接口是 ens33,IP 地址是 192.168.0.105/24。


现在想要创建两个容器,分别属于不同的子网,例如 10.0.1.0/24 和 10.0.2.0/24,并让它们可以相互通信,也可以访问外部网络。


可以按照如下的步骤来实现。


  • 首先,把之前环境下的容器退出,并清理资源,因为一个父接口不能同时支持 L2 和 L3 模式:


  $ docker system prune
复制代码


  • 创建一个新的 IPvlan 网络:


  $ docker network create --driver=ipvlan --subnet=10.0.1.0/24 --subnet=10.0.2.0/24 -o parent=ens33 -o ipvlan_mode=l3 ipvlan-l3
复制代码


  • 在两个 Terminal 窗口,分布运行一个容器,并连接到刚刚创建的 IPvlan 网络、使用不同的子网。它们的 IP 地址分别为 10.0.1.10 和 10.0.2.10:


  $ docker run -it --rm --name bbox1 --network ipvlan-l3 --ip 10.0.1.10 busybox sh  / # ip addr  1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000      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  4: eth0@if2: <BROADCAST,MULTICAST,NOARP,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue       link/ether 00:0c:29:12:5b:f4 brd ff:ff:ff:ff:ff:ff      inet 10.0.1.10/24 brd 10.0.1.255 scope global eth0         valid_lft forever preferred_lft forever
复制代码


  $ docker run -it --rm --name bbox2 --network ipvlan-l3 --ip 10.0.2.10 busybox sh  / # ip addr  1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000      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  5: eth0@if2: <BROADCAST,MULTICAST,NOARP,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue       link/ether 00:0c:29:12:5b:f4 brd ff:ff:ff:ff:ff:ff      inet 10.0.2.10/24 brd 10.0.2.255 scope global eth0         valid_lft forever preferred_lft forever
复制代码


  • 容器之间互通性。此时,两个容器之间已经可以互相访问。

  • 从 bbox1 访问 bbox2:


  / # ping 10.0.2.10  PING 10.0.2.10 (10.0.2.10): 56 data bytes  64 bytes from 10.0.2.10: seq=0 ttl=64 time=0.270 ms  64 bytes from 10.0.2.10: seq=1 ttl=64 time=0.061 ms
复制代码


从 bbox2 访问 bbox1:


  / # ping 10.0.1.10  PING 10.0.1.10 (10.0.1.10): 56 data bytes  64 bytes from 10.0.1.10: seq=0 ttl=64 time=0.077 ms  64 bytes from 10.0.1.10: seq=1 ttl=64 time=0.077 ms
复制代码


  • 但此时,从外部网络访问这两个容器依然是无法到达的。因为外部的网络环境里,没有关于 10.0.1.10 或者 10.0.2.10 这两个 IP 地址的路由信息。

  • 需要在外部路由器上添加相应的路由规则,让它知道如何到达容器网络。

  • 假设外部路由器的接口为 eth1,IP 地址为 192.168.0.1/24。

  • 添加路由规则,将目标地址为 10.0.1.0/24 或 10.0.2.0/24 的数据包转发到 192.168.0.105,即,转发到容器的父接口 ens33。


  $ sudo ip route add 10.0.1.0/24 via 192.168.0.105 dev eth1  $ sudo ip route add 10.0.2.0/24 via 192.168.1.105 dev eth1
复制代码


这样,就可以实现 IPvlan L3 模式的容器与外部网络的通信。Sorry,我忘了我家里路由器的密码了,暂时没法登录实验……


综合运用下来,感觉 IPvlan 模式应该比 Macvlan 模式更加实用,因为 Macvlan 拥有的功能,IPvlan 的 L2 模式都有,而且还少了混杂模式、MAC 地址数目的潜在问题。除此之外,IPvlan 还多了 L3 模式的支持。


用户头像

王玉川

关注

https://yuchuanwang.github.io/ 2018-11-13 加入

https://www.linkedin.com/in/yuchuan-wang/

评论

发布
暂无评论
Docker容器网络的七种武器_Docker_王玉川_InfoQ写作社区