01-容器网络

Linux容器能够看见的“网络栈”,是被隔离在它自己的Network Namespace当中的。

网络栈,包括:

  1. 网卡(Network Interface)
  2. 回环设备(Loopback Device)
  3. 路由表(Routing Table)
  4. iptables规则

对于一个进程来说,这些要素就构成了它发起和响应网络请求的基本环境

作为一个容器,可以直接使用宿主机的网络栈,即不开启Network Namespace,如下:

docker run -d -net=host --name nginx-host nginx
# 这个容器启动后,直接监听宿主机的80端口

这样直接使用宿主机网络栈的方式:

  • 好处:为容器提供良好的网络性能
  • 缺点:引入共享网络资源的问题,比如端口冲突

所以在大多数情况下,都希望容器进程能使用自己的Network Namespace里的网络栈,即拥有自己的IP地址和端口

0.1. 容器通信

被隔离的容器进程,该如何与其他Network Namespace里的容器进程进行交互?

将一个容器理解为一台主机,拥有独立的网络栈,那么主机之间通信最直接的方式就是通过网线,当有多台主机时,通过网线连接到交换机再进行通信。

在Linux中,能够起到虚拟交换机作用的网络设备,就是网桥(Bridge),工作在数据链路层(Data Link)的设备,主要功能根据MAC地址学习来将数据包转发到网桥的不同端口(Port)上

Docker项目默认在宿主机上创建一个docker0网桥,凡是连接在docker0网桥上的容器,就可以通过它来进行通信。使用Veth Pair的虚拟设备把容器都连接到docker0网桥上。

Veth Pair设备的特点:它被创建后,总是以两张虚拟网卡(Veth Peer)的形式成对出现的,并且从其中一个“网卡”发出的数据包,可以直接出现在与它对应的另一张“网卡”上,哪怕这两个“网卡”在不同的Network Namespace中。所有Veth Pair常被用作连接不同Network Namespace的“网线”。

0.1.1. 例子

启动一个容器,并进入后查看它的网络设备,然后回到宿主机查看网络设备:

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网桥上,所以同一个宿主机上的两个容器默认就是互相连通的。

0.1.2. 容器间访问(同宿主机)

image

  1. 当container-1访问container-2的IP地址(172.17.0.3)时,目标IP地址会匹配container-1里面的第二条路由规则
  2. 通过二层网络到达container-2容器,就需要有172.17.0.3这个IP地址对应的MAC地址,所以container-1容器的网络协议栈需要通过eth0网卡发送一个ARP广播,来通过IP地址查找对应的MAC地址 > ARP(Address Resoultion Protocol),是通过三层的IP地址找到对应的二层MAC地址的协议
  3. 容器内的eth0网卡是Veth Pair,它的一端在容器的Network Namespace中,另一端在宿主机上(Host Namespace),并且被插在宿主机docker0网桥上 > 虚拟网卡被插在网桥上就会变成网桥的从设备(被剥夺调用网络协议栈处理数据包的资格),从而降级为网桥上的一个端口,这个端口的唯一作用就是接受流入的数据包,然后把数据包的转发丢弃全部交给对应的网桥。
  4. 在收到container-1容器中发出的ARP请求后,docker0网桥就会扮演二层交换机的角色,把ARP广播转发到其他插在docker0网桥上的虚拟网卡。container-2容器内的网络协议栈就会收到这个ARP请求,从而将172.17.0.3所对应的MAC地址回复给container-1容器
  5. container-1容器获取MAC地址后,就能把数据包从容器内eth0网卡发送出去。根据Veth Pair设备的原理,这个数据包会立刻出现在宿主机的虚拟网卡veth9c02e56上,因为虚拟网卡的网络协议栈资格被剥夺,数据就直接流入docker0网桥里
  6. docker0处理转发的过程,继续扮演二层交换机的角色,网桥根据数据包目的MAC地址,在它的CAM表(交换机通过MAC地址学习维护的端口和MAC地址的对应表)里查到对应的端口为container-2的虚拟网卡,然后把数据发送到这个端口
  7. 这个虚拟网卡也是一个Veth Pair设备,所有数据直接进入到container-2容器的Network Namespace中
  8. container-2容器看到的情况是,它自己的eth0网卡上出现了流入的数据包,这样container-2的网络协议栈就会对请求进行处理,最后将响应返回给container-1

需要注意的是,在实际的数据传递时,数据的传递过程在网络协议的不同层次,都有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设备和宿主机网桥的方式,实现了跟其他容器的数据交换

0.1.3. 宿主机访问容器

image

访问宿主机上的容器的IP地址时,这个请求的数据包下根据路由规则到达Docker0网桥,然后被转发到对应的Veth Pair设备,最后出现在容器里。

0.1.4. 容器访问宿主机

宿主机之间网络需要互通。

image

当一个容器试图连接到另外一个宿主机(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规则是否有异常。

0.1.5. 容器间访问(跨主机)

在Docker默认的配置下,一台宿主机上的docker0网桥,和其它宿主机上的docker0网桥,没有任何关联。它们互相之间也没有办法连通、所以连接在网桥上的容器,没有办法进行通信

如果通过网络创建一个整个集群“公用”的网桥,然后把集群里的所有容器都连接到整个网桥上,就可以互通了。如下图所示。

image

构建这种网络的核心在于:需要在已有的宿主机网络上,通过软件构建一个覆盖已有宿主机网络之上的、可以把所有容器连通在一起的虚拟网络。这种技术称为Overlay Network(覆盖网络)。

Overlay Network本身,可以由每台宿主机上的一个“特殊网桥”共同组成。比如,当node1上的容器1要访问node2上的容器3时,node1上的“特殊网桥”在收到数据包之后,能够通过某种方式,把数据包发送到正确的宿主机node2上。在node2上的“特殊网桥”在收到数据包后,也能够通过某种方式,把数据包转发给正确的容器,容器3。

甚至,每台宿主机上,都不要有一个“特殊网桥”,而仅仅通过某种方式配置宿主机的路由表,就能够把数据包转发到正确的宿主机上

上次修改: 14 April 2020