目录

Kubernetes | Kubernetes 容器网络的原理

1. 容器网络实现原理

Linux 容器假如使用了 network namespace,那么它将会有自己的一个网络栈,而这个网络栈,就包括了:网卡、回环设备、路由表和 iptables 规则等。这些要素,其实就构成了进程发起和响应网络请求的基本环境,拥有了属于自己的 IP 地址和端口。(虽然容器可以直接使用宿主机网络栈的方式,从而为容器提供良好的网络性能,但是这样也会不可避免地引入共享网络资源的问题,比如端口冲突。)

那么被隔离在自己 network namespace 中的容器该如何跟其他 network namespace 里的容器进程进行交互呢?

其实可以把每一个容器看做一台主机,它们都有一套独立的“网络栈”。那么,想要实现两台主机之间的通信,最直接的办法,就是把它们用一根网线连接起来;而如果你想要实现多台主机之间的通信,那就需要用网线,把它们连接在一台交换机上。在 Linux 中,能够起到虚拟交换机作用的网络设备,是网桥(Bridge)。网桥是一个工作在数据链路层(Data Link)的设备,主要功能是根据 MAC 地址学习来将数据包转发到网桥的不同端口(Port)上。

为了实现上述目的,Docker 项目会默认在宿主机上创建一个名叫 docker0 的网桥,凡是连接在 docker0 网桥上的容器,就可以通过它来进行通信。除此之外,还需要使用一种名叫 Veth Pair 的虚拟设备,这种设备被创建出来之后,总是以两张虚拟网卡(Veth Peer)的形式成对出现。并且从其中一个网卡发出的数据包,可以直接出现在与它对应的另一张网卡上,哪怕这两张网卡在不同的 network namespace 中。因此,这就使得 Veth Pari 常常被用于连接不同 network namespace 的网线。

1.1. 两个容器的通信过程

  • 在容器 container1 中查看相应的路由:这个容器里有一张叫作 eth0 的网卡,它正是一个 Veth Pair 设备在容器里的这一端。所有对 172.17.0.0/16 网段的请求,都会被交给 eth0 来处理(第二条 172.17.0.0 路由规则)。

    1
    2
    3
    4
    5
    
    $ 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	0.0.0.0	255.255.0.0	U	0	0	0	eth0
    

    而这个 Veth Pair 设备的另一端,则在宿主机上。你可以通过查看宿主机的网络设备看到它,如下所示。可以看到,container1 对应的 Veth Pair 设备,在宿主机上是一张虚拟网卡。它的名字叫作 veth9c02e56。并且,通过 brctl show 的输出,你可以看到这张网卡被“插”在了 docker0 上。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
      
    # 在宿主机上
    $ 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)
    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
    bridge name bridge id  STP enabled interfaces
    docker0  8000.0242d8e4dfc1 no  veth9c02e56
    
  • 当你在 container1 容器里访问 container2 容器的 IP 地址(比如 ping 172.17.0.3)的时候,这个目的 IP 地址会匹配到 container1 容器里的路由规则,相应的路由规则的网关(gatewat)是 0.0.0.0,这就意味着这是一条直连规则:即凡是匹配到这条规则的 IP 包,应该经过本机的 eth0 网卡,通过二层网络直接发往目的主机。

    而要通过二层网络到达 container2 要有 172.17.0.3 这个 IP 地址对应的 MAC 地址。所以 container1 容器的网络协议栈,就需要通过 eth0 网卡发送一个 ARP 广播,来通过 IP 地址查找对应的 MAC 地址。而 container1 中的 eth0 网卡,是一个 Veth Pair,它的一端是在 container1 的 network namespace 中,而另一端是在宿主机上,并且被插在了宿主机的 docker0 上。并且,一旦一张虚拟网卡被“插”在网桥上,它就会变成该网桥的“从设备”。从设备会被“剥夺”调用网络协议栈处理数据包的资格,从而“降级”成为网桥上的一个端口。而这个端口唯一的作用,就是接收流入的数据包,然后把这些数据包的“生杀大权”(比如转发或者丢弃),全部交给对应的网桥。所以,在收到这些 ARP 请求之后,docker0 网桥就会扮演二层交换机的角色,把 ARP 广播转发到其他被“插”在 docker0 上的虚拟网卡上。这样,同样连接在 docker0 上的 container2 容器的网络协议栈就会收到这个 ARP 请求,从而将 172.17.0.3 所对应的 MAC 地址回复给 container1 容器。

  • 有了这个目的 MAC 地址,container1 容器的 eth0 网卡就可以将数据包发出去。而根据 Veth Pair 设备的原理,这个数据包会立刻出现在宿主机上的 veth9c02e56 虚拟网卡上。不过,此时这个 veth9c02e56 网卡的网络协议栈的资格已经被“剥夺”,所以这个数据包就直接流入到了 docker0 网桥里。

    docker0 处理转发的过程,则继续扮演二层交换机的角色。此时,docker0 网桥根据数据包的目的 MAC 地址(也就是 container2 容器的 MAC 地址),在它的 CAM 表(即交换机通过 MAC 地址学习维护的端口和 MAC 地址的对应表)里查到对应的端口(Port)为:vethb4963f3,然后把数据包发往这个端口。

  • 而这个端口,正是 container2 容器“插”在 docker0 网桥上的另一块虚拟网卡,当然,它也是一个 Veth Pair 设备。这样,数据包就进入到了 container2 容器的 Network Namespace 里。所以,container2 容器看到的情况是,它自己的 eth0 网卡上出现了流入的数据包。这样,container2 的网络协议栈就会对请求进行处理,最后将响应(Pong)返回到 containerd1。

https://img.dawnguo.cn/kubernetes/e0d28e0371f93af619e91a86eda99a66.png

需要注意的是,在实际的数据传递时,上述数据的传递过程在网络协议栈的不同层次,都有 Linux 内核 Netfilter 参与其中。所以,如果感兴趣的话,你可以通过打开 iptables 的 TRACE 功能查看到数据包的传输过程,具体方法如下所示,通过上述设置,你就可以在 /var/log/syslog 里看到数据包传输的日志了。

1
2
3
# 在宿主机上执行
$ iptables -t raw -A OUTPUT -p icmp -j TRACE
$ iptables -t raw -A PREROUTING -p icmp -j TRACE

综上来说,被限制在 Network Namespace 里的容器进程,实际上是通过 Veth Pair 设备 + 宿主机网桥的方式,实现了跟同其他容器的数据交换。

1.2. 主机跟容器的通信

  • 当你在一台宿主机上,访问该宿主机上的容器的 IP 地址时,这个请求的数据包,也是先根据路由规则到达 docker0 网桥,然后被转发到对应的 Veth Pair 设备,最后出现在容器里。

https://img.dawnguo.cn/kubernetes/20210607095754.png

1.3. 容器 ping 主机的过程

  • 当一个容器试图连接到另外一个宿主机时,比如:ping 10.168.0.3,它发出的请求数据包,首先经过 docker0 网桥出现在宿主机上。然后根据宿主机的路由表里的直连路由规则(10.168.0.0/24 via eth0)),对 10.168.0.3 的访问请求就会交给宿主机的 eth0 处理。所以,接下来的数据包就会经宿主机的 eth0 网卡转发到宿主机网络上,最终到达 10.168.0.3 对应的宿主机上(当然这两台宿主机本身是要连通的)。

    所以说,当你遇到容器连不通“外网”的时候,你都应该先试试 docker0 网桥能不能 ping 通,然后查看一下跟 docker0 和 Veth Pair 设备相关的 iptables 规则是不是有异常,往往就能够找到问题的答案了。

https://img.dawnguo.cn/kubernetes/90bd630c0723ea8a1fb7ccd738ad1f95.png

容器之间的「跨主通信」请看相应的整理。

2. Q&A

  • 这个 veth pair 的虚拟设备之间的通信是在内核之间通信的吗?是的,因为根据 UDP 的 3 次内核和用户态切换,可以看到容器封装的数据包之后就在内核态了,那就是内核态之间的流转了。

  • 使用这种网卡发送数据包的过程也会使用到内核栈的东西来发送吗?个人理解虚拟网卡发送数据其实只是调用了某些接口而已,而这些接口发送数据其实只是在内核之间流转。

3. 巨人的肩膀

  1. https://www.lifewire.com/layers-of-the-osi-model-illustrated-818017

  2. 极客时间.张磊.《深入剖析Kubernetes》