Docker容器基础入门认知-网络篇

这篇文章中,会从 docker 中的单机中的 netns 到 veth,再到单机多个容器之间的 bridge 网络交互,最后到跨主机容器之间的 nat 和 vxlan 通信过程,让大家对 docker 中的网络大概有个初步的了解。
先从 docker 里所使用的网络ns说起。在不同的容器中,docker 会为每个容器自动分配 ip 地址。并且在宿主机上是可以互相 ping 通的。比如下面我们起两个 busybox

$ docker run busybox sh -c "while true; do sleep 3600; done; "

Docker容器基础入门认知-网络篇
文章图片

这两个容器中,网络是互通的,并且在任何其他的容器内去 ping 这两个容器的 ip 也是联通的,这就说明在整个 docker 网络中,容器和 ip 分配还有相关的路由转发都是由 docker 内部来进行维护的。我们查看一下这两个容器的 ip
第一个容器中:
Docker容器基础入门认知-网络篇
文章图片

第二个容器中:
Docker容器基础入门认知-网络篇
文章图片

在除此之外的另一个容器中去 ping 172.17.0.2 和 172.17.0.3:
Docker容器基础入门认知-网络篇
文章图片

在 docker 中,不同的容器之间网络连通也是使用了 linux 的命名空间,用一个小实验来说明这里所用的原理其实就是使用了 veth 来实现命名空间的互联
实验步骤分为以下几个步骤:
  1. 创建端口
  2. 产生网线
  3. 分配ip
在 docker 中网络的分配也是根据这三个步骤来生成容器 ip 的,首先我们先产生两个网络虚拟命名空间
# 产生命名空间 test1, test2 $ sudo ip netns add test1 $ sudo ip netns add test2


$ ip netns list test2 (id: 3) test1 (id: 2) 产生一对 veth ,也就是所说的网线
# 产生一对 veth (veth 都是成对出现) sudo ip link add veth1-test1 type veth peer name veth2-test2

将虚拟命名空间的网卡和 veth 进行绑定
# 将两个虚拟网卡 veth 分配给 test1 和 test2 $ sudo ip link set veth1-test1 netns test1 $ sudo ip link set veth2-test2 netns test2

开启网卡,并且尝试 ping
# 开启网卡(因为新加的状态都是 DOWN) $ sudo ip netns exec test1 ip link set veth1-test1 up $ sudo ip netns exec test2 ip link set veth2-test2 up# 尝试 ping $ sudo ip netns exec test1 ping 192.168.1.2 -c 3 PING 192.168.1.2 (192.168.1.2) 56(84) bytes of data. 64 bytes from 192.168.1.2: icmp_seq=1 ttl=64 time=0.034 ms 64 bytes from 192.168.1.2: icmp_seq=2 ttl=64 time=0.045 ms 64 bytes from 192.168.1.2: icmp_seq=3 ttl=64 time=0.513 ms--- 192.168.1.2 ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 2001ms rtt min/avg/max/mdev = 0.034/0.197/0.513/0.223 ms

【Docker容器基础入门认知-网络篇】这上面只是带给大家关于 veth 比较浅显的初步认知,如果有兴趣对虚拟网路中的 veth-pair 有深入的了解,建议大家看看这个文章:Linux 虚拟网络设备 veth-pair 详解
这篇文章详细讲解了 veth 两端之间数据的联通底层原理
除此之外,我相信你了解过 docker 的网络,一定也知道有 bridge 的网络模式,bridge 其实起的就是桥梁的作用,可以理解为路由器,负责中转,连接,路由所有连接在它上面的容器
Docker容器基础入门认知-网络篇
文章图片

安装 bridge 工具
sudo yum install -y bridge-utils

可以查看 docker 内网桥与各个容器之间连接的关系
$ brctl show bridge namebridge idSTP enabledinterfaces docker08000.0242bfb37b66novethc9f5f33 vethfb9006b
# 发现这里有两个 veth 连着,这两个 veth 另一端连着的就是 docker 容器 test1 和 test2 # 在宿主机中打印所有的网卡 [vagrant@docker-node2 ~]$ ip a 1: lo: 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 forever 2: eth0: mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 link/ether 52:54:00:4d:77:d3 brd ff:ff:ff:ff:ff:ff inet 10.0.2.15/24 brd 10.0.2.255 scope global noprefixroute dynamic eth0 valid_lft 71714sec preferred_lft 71714sec inet6 fe80::5054:ff:fe4d:77d3/64 scope link valid_lft forever preferred_lft forever 3: docker0: mtu 1500 qdisc noqueue state UP group default link/ether 02:42:bf:b3:7b:66 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:bfff:feb3:7b66/64 scope link valid_lft forever preferred_lft forever 4: eth1: mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 link/ether 08:00:27:33:4f:b9 brd ff:ff:ff:ff:ff:ff inet 192.168.205.11/24 brd 192.168.205.255 scope global noprefixroute eth1 valid_lft forever preferred_lft forever inet6 fe80::a00:27ff:fe33:4fb9/64 scope link valid_lft forever preferred_lft forever 8: vethc9f5f33@if7: mtu 1500 qdisc noqueue master docker0 state UP group default link/ether 8e:12:0f:a0:7e:48 brd ff:ff:ff:ff:ff:ff link-netnsid 1 inet6 fe80::8c12:fff:fea0:7e48/64 scope link valid_lft forever preferred_lft forever 10: vethfb9006b@if9: mtu 1500 qdisc noqueue master docker0 state UP group default link/ether 16:4f:bc:53:ae:5d brd ff:ff:ff:ff:ff:ff link-netnsid 0 inet6 fe80::144f:bcff:fe53:ae5d/64 scope link valid_lft forever preferred_lft forever

可以看到上面的网卡 8 和 10 正是连着 docker0 的网桥的 veth,因为 veth 都成对出现的,所以这两个 veth,名字为 vethc9f5f33 和 vethfb9006b 的 veth,另外一端是连着上面所创建的两个容器
在上面创建的第一个 name=test1 容器中,可以看到 id=9 的网卡设备:
Docker容器基础入门认知-网络篇
文章图片

这里连接的就是宿主机中 id=10 的 vethfb9006b。

在讲完容器和容器之间的网络连通的底层后,我们再来看看外部访问和 docker 容器之间是怎么进行数据交换的?在容器中请求外部网址,他是怎么做到的呢?当在容器内 ping www.baidu.com 的时候,数据是怎么交互的呢?
还是在上面的容器内,宿主机 host 的 ip 为 172.31.243.112,如下面所视
[root@izm5e37rlunev9ij58ixy9z ~]# ifconfig docker0: flags=4163mtu 1500 inet 172.17.0.1netmask 255.255.0.0broadcast 172.17.255.255 ether 02:42:2c:a8:7c:9dtxqueuelen 0(Ethernet) RX packets 22bytes 1362 (1.3 KiB) RX errors 0dropped 0overruns 0frame 0 TX packets 22bytes 2099 (2.0 KiB) TX errors 0dropped 0 overruns 0carrier 0collisions 0eth0: flags=4163mtu 1500 inet 172.31.243.112netmask 255.255.240.0broadcast 172.31.255.255 ether 00:16:3e:05:96:e1txqueuelen 1000(Ethernet) RX packets 2247026bytes 271820827 (259.2 MiB) RX errors 0dropped 0overruns 0frame 0 TX packets 5270453bytes 378089658 (360.5 MiB) TX errors 0dropped 0 overruns 0carrier 0collisions 0lo: flags=73mtu 65536 inet 127.0.0.1netmask 255.0.0.0 looptxqueuelen 1(Local Loopback) RX packets 2098bytes 104900 (102.4 KiB) RX errors 0dropped 0overruns 0frame 0 TX packets 2098bytes 104900 (102.4 KiB) TX errors 0dropped 0 overruns 0carrier 0collisions 0veth05c8be8: flags=4163mtu 1500 ether c6:8c:35:49:68:69txqueuelen 0(Ethernet) RX packets 14bytes 1048 (1.0 KiB) RX errors 0dropped 0overruns 0frame 0 TX packets 14bytes 1334 (1.3 KiB) TX errors 0dropped 0 overruns 0carrier 0collisions 0

创建一个容器 busybox ,此容器位于 docker0 这个私有 bridge 网络中(172.17.0.0/16),当 busybox 从容器向外 ping 时,数据包是怎样到达 bing.com 的呢?这里的关键就是 NAT。我们查看一下 docker host 上的 iptables 规则:
docker run busybox sh -c "while true; do sleep 3600; done; "

查看此主机上的 iptables,因为所有容器和外部的网络交互,都是通过 NAT 来实现的
[root@izm5e37rlunev9ij58ixy9z ~]# iptables -t nat -S -P PREROUTING ACCEPT -P INPUT ACCEPT -P OUTPUT ACCEPT -P POSTROUTING ACCEPT -N DOCKER -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.17.0.0/16 ! -o docker0 -j MASQUERADE -A DOCKER -i docker0 -j RETURN

注意一下这里
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE

其含义是:如果网桥 docker0 收到来自 172.17.0.0/16 网段的外出包,把它交给 MASQUERADE 处理,而 MASQUERADE 的处理方式是将包的源地址替换成 host 的地址发送出去,即做了一次网络地址转换(NAT);
下面我们通过 tcpdump 查看地址是如何转换的。先查看 docker host 的路由表:
[root@izm5e37rlunev9ij58ixy9z ~]# ip r default via 172.31.255.253 dev eth0 169.254.0.0/16 dev eth0scope linkmetric 1002 172.17.0.0/16 dev docker0proto kernelscope linksrc 172.17.0.1 172.31.240.0/20 dev eth0proto kernelscope linksrc 172.31.243.112

默认路由通过 enp0s3 发出去,所以我们要同时监控 eth0 和 docker0 上的 icmp(ping)数据包。
当 busybox ping baidu.com 时,
/ # ping -c 3 www.baidu.com PING www.baidu.com (110.242.68.4): 56 data bytes 64 bytes from 110.242.68.4: seq=0 ttl=50 time=17.394 ms 64 bytes from 110.242.68.4: seq=1 ttl=50 time=17.433 ms 64 bytes from 110.242.68.4: seq=2 ttl=50 time=17.453 ms

这个时候在分别在宿主机对 docker0 和 eth0 抓包:
[root@izm5e37rlunev9ij58ixy9z ~]# sudo tcpdump -i docker0 -n icmp tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on docker0, link-type EN10MB (Ethernet), capture size 65535 bytes 14:26:05.241364 IP 172.17.0.2 > 110.242.68.4: ICMP echo request, id 14, seq 0, length 64 14:26:05.258772 IP 110.242.68.4 > 172.17.0.2: ICMP echo reply, id 14, seq 0, length 64 14:26:06.241458 IP 172.17.0.2 > 110.242.68.4: ICMP echo request, id 14, seq 1, length 64 14:26:06.258835 IP 110.242.68.4 > 172.17.0.2: ICMP echo reply, id 14, seq 1, length 64 14:26:07.241578 IP 172.17.0.2 > 110.242.68.4: ICMP echo request, id 14, seq 2, length 64 14:26:07.258940 IP 110.242.68.4 > 172.17.0.2: ICMP echo reply, id 14, seq 2, length 64

[root@izm5e37rlunev9ij58ixy9z ~]# sudo tcpdump -i eth0 -n icmp tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes 14:33:00.015219 IP 172.31.243.112 > 110.242.68.4: ICMP echo request, id 15, seq 0, length 64 14:33:00.032516 IP 110.242.68.4 > 172.31.243.112: ICMP echo reply, id 15, seq 0, length 64 14:33:01.015332 IP 172.31.243.112 > 110.242.68.4: ICMP echo request, id 15, seq 1, length 64 14:33:01.032650 IP 110.242.68.4 > 172.31.243.112: ICMP echo reply, id 15, seq 1, length 64 14:33:02.015433 IP 172.31.243.112 > 110.242.68.4: ICMP echo request, id 15, seq 2, length 64 14:33:02.032787 IP 110.242.68.4 > 172.31.243.112: ICMP echo reply, id 15, seq 2, length 64

docker0 收到 busybox 的 ping 包,源地址为容器 IP 172.17.0.2,然后交给 MASQUERADE 处理。这个规则就是上面我们通过 iptables 查到的 -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
ping 包的源地址变成了 eth0 的 IP 172.31.243.112,也就是宿主机 host ip,这就是 iptable NAT 规则处理的结果,从而保证数据包能够到达外网。
来总结一下整个的过程:
  • busybox 发送 ping 包:172.17.0.2 >www.baidu.com;
  • docker0 收到包,发现是发送到外网的,交给 NAT 处理;
  • NAT 将源地址换成 enp0s3 的 IP:172.31.243.112 > www.baidu.com;
  • ping 包从 enp0s3 发送出去,到达 www.baidu.com;
即通过 NAT,docker 实现了容器对外网的访问;
那么外部网络如何访问到容器?答案是:端口映射
docker 可将容器对外提供服务的端口映射到 host 的某个端口,外网通过该端口访问容器。容器启动时通过-p参数映射端口:
[root@izm5e37rlunev9ij58ixy9z ~]# docker run --name nginx-test -p 8080:80 -d nginx

查看映射:
[root@izm5e37rlunev9ij58ixy9z ~]# docker ps CONTAINER IDIMAGECOMMANDCREATEDSTATUSPORTSNAMES 9a7edd9e4133nginx"/docker-entrypoint.…"4 seconds agoUp 3 seconds0.0.0.0:8080->80/tcpnginx-test

容器启动后,可通过 docker ps 或者 docker port 查看到 host 映射的端口。可以看到,httpd 容器的 80 端口被映射到 host 8080 上,这样就可以通过 :<8080 > 访问容器的 web 服务了;
[root@izm5e37rlunev9ij58ixy9z ~]# curl 172.31.243.112:8080 Welcome to nginx! - 锐客网html { color-scheme: light dark; } body { width: 35em; margin: 0 auto; font-family: Tahoma, Verdana, Arial, sans-serif; } Welcome to nginx!If you see this page, the nginx web server is successfully installed and working. Further configuration is required.
For online documentation and support please refer to "http://nginx.org/">nginx.org.
Commercial support is available at "http://nginx.com/">nginx.com.
Thank you for using nginx.

每一个映射的端口,host 都会启动一个 docker-proxy 进程来处理访问容器的流量:
[root@izm5e37rlunev9ij58ixy9z ~]# ps -ef | grep docker-proxy root2983369120 15:15 ?00:00:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 8080 -container-ip 172.17.0.3 -container-port 80

具体的流程为
  • docker-proxy 监听 host 的 8080 端口
  • 当 curl 访问 172.31.243.112:8080 时,docker-proxy 转发给容器 172.31.243.112:80。
  • httpd 容器响应请求并返回结果
如图是 nat 和外部网络交互的示意图:
Docker容器基础入门认知-网络篇
文章图片

至于在多机通信过程中,docker 是怎么组织跨主机的统一网络的呢?这里就简单介绍一下 overlay 网络,底层使用的是 vxlan 协议。
VXLAN(Virtual Extensible Local Area Network,虚拟可扩展局域网),通过将物理服务器或虚拟机发出的数据包封装到UDP中,并使用物理网络的IP/MAC作为外层报文头进行封装,然后在IP网络上传输,到达目的地后由隧道端点解封装并将数据发送给目标物理服务器或虚拟机,扩展了大规模虚拟机网络通信。由于VLAN Header头部限制长度是12bit,导致只能分配4095个VLAN,也就是4095个网段,在大规模虚拟网络。VXLAN标准定义Header限制长度24bit,可以支持1600万个VLAN,满足大规模虚拟机网络需求。
Docker容器基础入门认知-网络篇
文章图片

在不同的跨主机网络中,想让分布在不同主机的容器能够在统一的内网中互相访问互通,那么首先第一是集群中所有主机必须是可以互联并且是可以感知的,第二是所有容器的内网ip必定是和当前的宿主ip所绑定。这些信息是维护在 etcd 存储中的。当某一个容器内需要跨机器去访问另一机器上的容器时,会第一时间在本地宿主机内查找对应的宿主机器,这一部分是 iptables 进行维护,查找到对应的 host 后直接进行 vxlan 的协议封装,让其变成普通的网络包可以在外部网络中进行传输。最后在目标宿主机上接受数据包后,发现是 vxlan 协议,会交由 docker 进行解包的操作,当把 vxlan 协议剥离后,内部的真正的容器数据包会被发送给目标容器。
关于docker中跨主机通信怎么维护一个统一的二层和三层网络,我建议读一下这一篇文章,会比较好理解:二层网络三层网络理解

在实际工程中,跨主机跨集群的网络是非常复杂的,具体看 k8s 中层出不穷的网络插件就可以了解到,建议看看相关的网络插件实现原理,比如 weave,calico,flannel 这些,具体可以看看这一篇官网文档:https://feisky.gitbooks.io/kubernetes/content/network/network.html

    推荐阅读