容器网络
容器的网络栈
所谓的网络栈包括:网卡、回环设备、路由表、Iptables规则,而对于一个进程来讲,这些要素是构成发起和响应网络请求的基本环境。而对于容器来讲可以直接使用宿主机的网络栈,即不开启Network Namespace,例如:
docker run -d --net=host --name nginx-host nginx
直接监控到宿主机80端口
netstat -ntlp | grep nginx tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 29133/nginx: master
虽然这种方式会提高容器网络性能,但也会不可避免的引入共享网络资源的问题,例如端口冲突等。所以,在大多数情况下,我们都希望容器进程能使用自己的Netword Namespace里的网络栈,拥有自己的IP和端口。
容器与容器之间如何通信
这里我们可以把每一个容器看做一台主机,他们都有自己的一套网络栈。如下想让两个容器互通最简单的办法就是连接一根网线。而想要多台主机相互通信,那就需要一根网线连接到交换机上。在Linux中能起虚拟交换机的作用设备那就是网桥(Bridge),它是工作在数据链路层的设备,主要功能就是根据MAC地址学习来将数据包转发到网桥的不同端口。
而Docker创建的时候会默认在宿主机上创建一个docker 0的网桥,凡是连接在docker 0网桥上的容器,就可以通过他来通信。连接docker 0网桥需要一个Veth Pair的虚拟设备。
Veth Pair特点:它被创建后,总是以两张虚拟网卡的形式成对出现,并且,从其中一个“网卡”发出的数据包可以直接出现在另一张“网卡”上,哪怕这两张网卡在不通的namespace里。这使得Veth Pair常常被称作两个容器间的“网线”。
演示:
启动一个myapp启动,命名为myapp-1
docker run -d --name myapp-1 ikubernetes/myapp:v1
进入到容器,查看一下网络
#进去容器 $ docker exec -it myapp-1 /bin/sh #查看网络 $ ifconfig eth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:02 inet addr:172.17.0.2 Bcast:0.0.0.0 Mask:255.255.0.0 inet6 addr: fe80::42:acff:fe11:2/64 Scope:Link UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:13 errors:0 dropped:0 overruns:0 frame:0 TX packets:11 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:0 RX bytes:1046 (1.0 KiB) TX bytes:866 (866.0 B) lo Link encap:Local Loopback inet addr:127.0.0.1 Mask:255.0.0.0 inet6 addr: ::1/128 Scope:Host 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:1000 RX bytes:0 (0.0 B) TX bytes:0 (0.0 B) $ route Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface default 172.17.0.1 0.0.0.0 UG 0 0 0 eth0 172.17.0.0 * 255.255.0.0 U 0 0 0 eth0
可以看到,这个容器有一张eth0的网卡,这个网卡就是Veth Pair设备在容器的这一端,通过route命令查看myapp-1的路由表,可以看到,这个eth0网卡是这个容器的默认路由设备。所有对172.17.0.0/16 网络的请求,也会被交给eth0来处理(第二条规则)
而Veth Pair设备另一端而在宿主机上,我们可以查看宿主机网络设备。如下:
$ ifconfig ..... docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 172.17.0.1 netmask 255.255.0.0 broadcast 0.0.0.0 inet6 fe80::42:a0ff:fed8:f7fb prefixlen 64 scopeid 0x20<link> ether 02:42:a0:d8:f7:fb txqueuelen 0 (Ethernet) RX packets 41 bytes 2581 (2.5 KiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 15 bytes 1178 (1.1 KiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 vethf34d9da: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet6 fe80::7cf9:b4ff:fefc:fc3c prefixlen 64 scopeid 0x20<link> ether 7e:f9:b4:fc:fc:3c txqueuelen 0 (Ethernet) RX packets 16 bytes 1173 (1.1 KiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 19 bytes 1458 (1.4 KiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 ..... $ brctl show bridge name bridge id STP enabled interfaces docker0 8000.0242a0d8f7fb no vethf34d9da
通过ifconfig命令输出,可以看到,myapp-1容器对应的Veth Pair设备在宿主机上是一张虚拟网卡。名字叫vethf34d9da。并且,通过brctl show的输出,可以看到这张网卡被“插”在docker0上了。
我们再启动一个容器,我们看一下会发生什么
docker run -d --name myapp-2 ikubernetes/myapp:v1
宿主机上查看一下网络设备
brctl show bridge name bridge id STP enabled interfaces docker0 8000.0242a0d8f7fb no veth90a25df vethf34d9da
我们发现vethf34d9da的虚拟网卡“插”在docker0上
我们在myapp-1上ping一下myapp-2的ip,我们会发现是通的。
$ docker exec -it myapp-1 /bin/sh / # ping 172.17.0.3 PING 172.17.0.3 (172.17.0.3): 56 data bytes 64 bytes from 172.17.0.3: seq=0 ttl=64 time=0.296 ms 64 bytes from 172.17.0.3: seq=1 ttl=64 time=0.310 ms
解释一下为什么会通。
当我们myapp-1访问myapp-2的时候,这个目的IP地址会匹配到myapp-1容器里路由规则的第二条,可以看到,这条路由规则的网关是“*”,这就意味是直连规则,即:凡是匹配这个规则的IP包,应该经过eth 0网卡,通过二层网络直接发往目的主机。而要通过二层网络到达nginx-2容器,就需要有172.17.0.3这个IP对应的MAC地址,所以myapp-1容器的网络协议栈,就需要通过eth0网卡发送一个ARP广播,来通过IP地址查找对应的MAC地址。这个eth0网卡是一个Veth Pair,它的一段在myapp-1容器的Network Namespace里,而另一端则位于宿主机上,并“插”在docker0网桥上。
一旦一张虚拟网卡“插”在网桥上,它就变成该网桥的“从设备”,“从设备”会被“剥夺”调用网络协议栈处理数据包的资格,从而降级成为网桥上的一个端口,而这个端口的唯一作用,就是接收流入的数据包,然后这个数据包的“生杀大权”(例如转发、丢弃)全部交给对应的网桥。
所以,在收到这些ARP请求后,docker0网桥就会扮演二层交换机的角色,把ARP广播转发到其他被“插”在docker0上虚拟网卡上。这样,同样连接在docker0上的myapp-2容器的网络协议栈就会收到这个ARP请求,从而将172.17.0.3所对应的MAC地址回复给myapp-1容器。有了这个目的MAC地址,myapp-1容器的eth0网卡就可以将数据包发出去了。而根据Veth Pair设备原理,这个数据包会立即出现在宿主机vethf34d9da虚拟网卡上,不过,此时这个vethf34d9da虚拟网卡的网络协议栈被宿主机docker0剥夺,所以这个数据包将直接流入到docker0网桥里。
对于网桥上流入的数据包,就会通过宿主机的网络协议栈进行处理。需要注意的是,在宿主机上,Docker会为你设置如下路由规则:
route Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface default bogon 0.0.0.0 UG 100 0 0 ens160 172.16.138.0 0.0.0.0 255.255.255.0 U 100 0 0 ens160 172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
而这次流入的数据包的目的地址是172.17.0.3,所以,当他出现宿主机之后,就会按照上述172.17.0.0这条路由规则,在进过docker0网桥(FORWORD)转发出去。
docker0处理转发的过程,则继续扮演二层交换机的角色,此时,docker0根据数据包的目的MAC地址(myapp-2容器的MAC地址),在它的CAM表(既交换机通过MAC地址学习维护的端口和MAC地址对应表)里查到对应的端口为: vethf34d9da,然后将数据转发到这个端口。而这个端口是myapp-2“插”在docker0网桥上的另一张虚拟网卡,当然他也是Veth Pair设备,这样数据包就进入到myapp-2容器里Network Namespace里。这样myapp-2容器的网卡上出现了流入的数据包,就可以对请求进行处理了,处理完成将响应返回给myapp-1。
上述流程示意图
与此类推,当宿主机访问容器的时候,如下示意图:
当一个容器访问另一个宿主机上的容器时,例如:ping 172.16.138.40,它发出的请求数据包,首先经过docker0网桥出现在宿主机上。然后根据宿主机的路由表里直连路由规则(172.16.138.0/24 ens160)对172.16.138.40的访问请求就会交给宿主机的eth0。数据包经过宿主机的eth0 网卡转发宿主机网络上,最终到达172.16.138.40对应的宿主机上,要实现这个首先两台宿主机的网络是连通的,如下图:
所以说当遇到容器连不通“外网”的时候,应该先试试ping docker0的地址通不通,然后在查docker0 和 Veth Pair 设备相关的iptables规则是不是正常。
网络栈
极客时间版权所有: https://time.geekbang.org/column/article/64948网络
网络栈
极客时间版权所有: https://time.geekbang.org/column/article/64948
网络栈
极客时间版权所有: https://time.geekbang.org/column/article/64948
1在大多数情况下,我们都希望容器进程能使用自己 Network...
极客时间版权所有: https://time.geekbang.org/column/article/64948