Linux容器能够看见的“网络栈”,是被隔离在它自己的Network Namespace当中的。
网络栈,包括:
对于一个进程来说,这些要素就构成了它发起和响应网络请求的基本环境。
作为一个容器,可以直接使用宿主机的网络栈,即不开启Network Namespace,如下:
docker run -d -net=host --name nginx-host nginx
# 这个容器启动后,直接监听宿主机的80端口
这样直接使用宿主机网络栈的方式:
所以在大多数情况下,都希望容器进程能使用自己的Network Namespace里的网络栈,即拥有自己的IP地址和端口。
被隔离的容器进程,该如何与其他Network Namespace里的容器进程进行交互?
将一个容器理解为一台主机,拥有独立的网络栈,那么主机之间通信最直接的方式就是通过网线,当有多台主机时,通过网线连接到交换机再进行通信。
在Linux中,能够起到虚拟交换机作用的网络设备,就是网桥(Bridge),工作在数据链路层(Data Link)的设备,主要功能根据MAC地址学习来将数据包转发到网桥的不同端口(Port)上。
Docker项目默认在宿主机上创建一个docker0网桥,凡是连接在docker0网桥上的容器,就可以通过它来进行通信。使用Veth Pair
的虚拟设备把容器都连接到docker0网桥上。
Veth Pair
设备的特点:它被创建后,总是以两张虚拟网卡(Veth Peer
)的形式成对出现的,并且从其中一个“网卡”发出的数据包,可以直接出现在与它对应的另一张“网卡”上,哪怕这两个“网卡”在不同的Network Namespace中。所有Veth Pair
常被用作连接不同Network Namespace的“网线”。
启动一个容器,并进入后查看它的网络设备,然后回到宿主机查看网络设备:
docker run –d --name nginx-1 nginx
# 在宿主机上
docker exec -it nginx-1 /bin/bash
# 在容器里
root@2b3c181aecf1:/# ifconfig
# 这张网卡是Veth Pair设备在容器里的一端
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.17.0.2 netmask 255.255.0.0 broadcast 0.0.0.0
inet6 fe80::42:acff:fe11:2 prefixlen 64 scopeid 0x20<link>
ether 02:42:ac:11:00:02 txqueuelen 0 (Ethernet)
RX packets 364 bytes 8137175 (7.7 MiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 281 bytes 21161 (20.6 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10<host>
loop txqueuelen 1000 (Local Loopback)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
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 # 容器内默认路由设备,是eth0网卡
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0 # 所有对172.17.0.0/16这个网段的请求,也会被交给eth0来处理
# 网关为0.0.0.0表示这是一条直连规则,凡是匹配到这个规则的IP包,应该经过本机的eth0网卡,通过二层网络直接发往目的主机
# 在宿主机上
ifconfig
...
docker0 Link encap:Ethernet HWaddr 02:42:d8:e4:df:c1
inet addr:172.17.0.1 Bcast:0.0.0.0 Mask:255.255.0.0
inet6 addr: fe80::42:d8ff:fee4:dfc1/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:309 errors:0 dropped:0 overruns:0 frame:0
TX packets:372 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:18944 (18.9 KB) TX bytes:8137789 (8.1 MB)
# nginx-1容器对应的Veth Pair设备,在宿主机上是这个虚拟网卡
veth9c02e56 Link encap:Ethernet HWaddr 52:81:0b:24:3d:da
inet6 addr: fe80::5081:bff:fe24:3dda/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:288 errors:0 dropped:0 overruns:0 frame:0
TX packets:371 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:21608 (21.6 KB) TX bytes:8137719 (8.1 MB)
brctl show
# 查看网桥,可以看到上面的虚拟网卡被连接到了docker0网桥上
bridge name bridge id STP enabled interfaces
docker0 8000.0242d8e4dfc1 no veth9c02e56
新创建容器nginx-2的Veth Pair的一端在容器中,另一端在docker0网桥上,所以同一个宿主机上的两个容器默认就是互相连通的。
需要注意的是,在实际的数据传递时,数据的传递过程在网络协议的不同层次,都有Linux内核Netfilter参与其中。可以使用iptables的TRACE功能,查看数据包的传输过程,如下所示:
# 在宿主机上执行
iptables -t raw -A OUTPUT -p icmp -j TRACE
iptables -t raw -A PREROUTING -p icmp -j TRACE
# 在宿主机的/var/log/syslog里看到数据包传输的日志
被限制在Network Namespace里的容器进程,实际上是通过Veth Pair设备和宿主机网桥的方式,实现了跟其他容器的数据交换。
访问宿主机上的容器的IP地址时,这个请求的数据包下根据路由规则到达Docker0网桥,然后被转发到对应的Veth Pair设备,最后出现在容器里。
宿主机之间网络需要互通。
当一个容器试图连接到另外一个宿主机(10.168.0.3)时,发出的请求数据包,首先经过docker0网桥出现在宿主机上,然后根据路由表里的直连路由规则(10.168.0.0/24 via eth0),对10.168.0.3的访问请求就会交给宿主机的eth0处理。这个数据包经过宿主机的eth0网卡转发到宿主机网络上,最终到达10.168.0.3对应的宿主机上。
当出现容器不能访问外网的时候,先试一下能不能ping通docker0网桥,然后查看一下docker0和Veth Pair设备相关的iptables规则是否有异常。
在Docker默认的配置下,一台宿主机上的docker0网桥,和其它宿主机上的docker0网桥,没有任何关联。它们互相之间也没有办法连通、所以连接在网桥上的容器,没有办法进行通信。
如果通过网络创建一个整个集群“公用”的网桥,然后把集群里的所有容器都连接到整个网桥上,就可以互通了。如下图所示。
构建这种网络的核心在于:需要在已有的宿主机网络上,通过软件构建一个覆盖已有宿主机网络之上的、可以把所有容器连通在一起的虚拟网络。这种技术称为Overlay Network(覆盖网络)。
Overlay Network本身,可以由每台宿主机上的一个“特殊网桥”共同组成。比如,当node1上的容器1要访问node2上的容器3时,node1上的“特殊网桥”在收到数据包之后,能够通过某种方式,把数据包发送到正确的宿主机node2上。在node2上的“特殊网桥”在收到数据包后,也能够通过某种方式,把数据包转发给正确的容器,容器3。
甚至,每台宿主机上,都不要有一个“特殊网桥”,而仅仅通过某种方式配置宿主机的路由表,就能够把数据包转发到正确的宿主机上。