S_lion's Studio

docker跨主机通信

字数统计: 2.9k阅读时长: 11 min
2022/03/22 Share

Docker默认的网络模式有三种:bridge、host、none,其中想实现不同宿主机上的容器相互访问是做不到的。对应”docker跨主通信”的需求,社区里涌现了大批的方案,其中flannel是用的最多也是较为简单的。

Flannel

Flannel本身是个框架,真正提供网络功能的是flannel的后端实现。Flannel支持三种后端实现:

  • udp
  • vxlan
  • host-gw

udp

flannel最早期的网络方案,实现最简单但是性能最差。

udp的原理图:

Flannel UDP 模式提供的其实是一个三层的 Overlay 网络,即:它首先对发出端的 IP包进行 UDP 封装,然后在接收端进行解封装拿到原始的 IP 包,进而把这个 IP 包转发给目标容器。这就好比,Flannel 在不同宿主机上的两个容器之间打通了一条“隧道”,使得这两个容器可以直接使用 IP 地址进行通信,而无需关心容器和宿主机的分布情况。

实际上,相比于两台宿主机之间的直接通信,基于 Flannel UDP 模式的容器通信多了一个额外的步骤,即 flanneld 的处理过程。而这个过程,由于使用到了 flannel0 这个 TUN 设备,仅在发出 IP包的过程中,就需要经过三次用户态与内核态之间的数据拷贝,如下所示:

第一次:用户态的容器进程发出的 IP 包经过 docker0 网桥进入内核态;

第二次:IP 包根据路由表进入 TUN(flannel0)设备,从而回到用户态的 flanneld 进程;

第三次:flanneld 进行 UDP 封包之后重新进入内核态,将 UDP 包通过宿主机的 eth0 发出去。

此外,我们还可以看到,Flannel 进行 UDP 封装(Encapsulation)和解封装(Decapsulation)的过程,也都是在用户态完成的。在 Linux 操作系统中,上述这些上下文切换和用户态操作的代价其实是比较高的,这也正是造成 Flannel UDP 模式性能不好的主要原因。

vxlan

Virtual Extensible LAN(虚拟可扩展局域网),是 Linux 内核本身就支持的一种网络虚似化技术。所以说,VXLAN 可以完全在内核态实现上述封装和解封装的工作,从而通过与前面相似的“隧道”机制,构建出覆盖网络(Overlay Network)。

VXLAN 的覆盖网络的设计思想是:在现有的三层网络之上,“覆盖”一层虚拟的、由内核 VXLAN模块负责维护的二层网络,使得连接在这个 VXLAN 二层网络上的“主机”之间,可以像在同一个局域网(LAN)里那样自由通信。为了能够在二层网络上打通“隧道”,VXLAN 会在宿主机上设置一个特殊的网络设备作为“隧道”的两端。这个设备就叫作 VTEP,即:VXLAN Tunnel End Point(虚拟隧道端点)

而 VTEP 设备的作用,其实跟前面的 flanneld 进程非常相似。只不过,它进行封装和解封装的对象,是二层数据帧(Ethernet frame);而且这个工作的执行流程,全部是在内核里完成的(因为VXLAN 本身就是 Linux 内核中的一个模块)。

图中每台宿主机上名叫 flannel.1 的设备,就是 VXLAN 所需的 VTEP 设备,它既有 IP地址,也有 MAC 地址。

可以看到,图中每台宿主机上名叫 flannel.1 的设备,就是 VXLAN 所需的 VTEP 设备,它既有 IP地址,也有 MAC 地址。

现在,我们的 container-1 的 IP 地址是 10.1.15.2,要访问的 container-2 的 IP 地址是10.1.16.3。

那么,与前面 UDP 模式的流程类似,当 container-1 发出请求之后,这个目的地址是 10.1.16.3的 IP 包,会先出现在 docker0 网桥,然后被路由到本机 flannel.1 设备进行处理。

为了能够将“原始 IP 包”封装并且发送到正确的宿主机,VXLAN 就需要找到这条“隧道”的出口,即:目的宿主机的 VTEP 设备。

而这个设备的信息,正是每台宿主机上的 flanneld 进程负责维护的。

当 一个新Node启动并加入 Flannel 网络之后,在 所有的节点上,flanneld就会添加一条去往对端的路由,凡是发往对端网段的 IP 包,都需要经过 flannel.1 设备发出。

在本例中,“源 VTEP 设备”收到“原始 IP 包”后,就要想办法把“原始 IP 包”加上一个目的 MAC 地址,封装成一个二层数据帧,然后发给“目的 VTEP 设备”

此时,根据前面的路由记录,我们已经知道了“目的 VTEP 设备”的 IP 地址。而要根据三层 IP 地址查询对应的二层 MAC 地址,这正是 ARP表的功能。而这里要用到的 ARP 记录,也是 flanneld 进程在 Node 2 节点启动时,自动添加在 Node 1 上的。我们可以通过 ip 命令看到它,如下所示:

1
2
3
# 在 Node 1 上
$ ip neigh show dev flannel.1
10.1.16.0 lladdr 5e:f8:4f:00:e3:37 PERMANENT

有了这个“目的 VTEP 设备”的 MAC 地址,Linux 内核就可以开始二层封包工作了。这个二层帧的格式,如下所示:

可以看到,Linux 内核会把“目的 VTEP 设备”的 MAC 地址,填写在图中的 Inner EthernetHeader 字段,得到一个二层数据帧。

需要注意的是,上述封包过程只是加一个二层头,不会改变“原始 IP 包”的内容。所以图中的Inner IP Header 字段,依然是 container-2 的 IP 地址,即 10.1.16.3。

但是,上面提到的这些 VTEP 设备的 MAC 地址,对于宿主机网络来说并没有什么实际意义。所以上面封装出来的这个数据帧,并不能在我们的宿主机二层网络里传输。为了方便叙述,我们把它称为“内部数据帧”。

所以接下来,Linux 内核还需要再把“内部数据帧”进一步封装成为宿主机网络里的一个普通的数据帧,好让它“载着”“内部数据帧”,通过宿主机的 eth0 网卡进行传输。

我们把这次要封装出来的、宿主机对应的数据帧称为“外部数据帧”(Outer Ethernet Frame)。为了实现这个“搭便车”的机制,Linux 内核会在“内部数据帧”前面,加上一个特殊的 VXLAN头,用来表示这个“乘客”实际上是一个 VXLAN 要使用的数据帧。

而这个 VXLAN 头里有一个重要的标志叫作VNI,它是 VTEP 设备识别某个数据帧是不是应该归自己处理的重要标识。而在 Flannel 中,VNI 的默认值是 1,这也是为何,宿主机上的 VTEP 设备都叫作 flannel.1 的原因,这里的“1”,其实就是 VNI 的值。

然后,Linux 内核会把这个数据帧封装进一个 UDP 包里发出去。

在宿主机看来,它会以为自己的 flannel.1 设备只是在向另外一台宿主机的 flannel.1 设备,发起了一次普通的 UDP 链接。它哪里会知道,这个 UDP 包里面,其实是一个完整的二层数据帧。

不过,不要忘了,一个 flannel.1 设备只知道另一端的 flannel.1 设备的 MAC 地址,却不知道对应的宿主机地址是什么。

也就是说,这个 UDP 包该发给哪台宿主机呢?

在这种场景下,flannel.1 设备实际上要扮演一个“网桥”的角色,在二层网络进行 UDP 包的转发。而在 Linux 内核里面,“网桥”设备进行转发的依据,来自于一个叫作 FDB(Forwarding Database)的转发数据库。

不难想到,这个 flannel.1“网桥”对应的 FDB 信息,也是 flanneld 进程负责维护的。它的内容可以通过 bridge fdb 命令查看到,如下所示:

1
2
3
# 在 Node 1 上,使用“目的 VTEP 设备”的 MAC 地址进行查询
$ bridge fdb show flannel.1 | grep 5e:f8:4f:00:e3:37
5e:f8:4f:00:e3:37 dev flannel.1 dst 10.168.0.3 self permanent

可以看到,在上面这条 FDB 记录里,指定了这样一条规则,即:

发往我们前面提到的“目的 VTEP 设备”(MAC 地址是 5e:f8:4f:00:e3:37)的二层数据帧,应该通过 flannel.1 设备,发往 IP 地址为10.168.0.3 的主机。显然,这台主机正是 Node 2,UDP 包要发往的目的地就找到了。

所以接下来的流程,就是一个正常的、宿主机网络上的封包工作。

我们知道,UDP 包是一个四层数据包,所以 Linux 内核会在它前面加上一个 IP 头,即原理图中的Outer IP Header,组成一个 IP 包。并且,在这个 IP 头里,会填上前面通过 FDB 查询出来的目的主机的 IP 地址,即 Node 2 的 IP 地址 10.168.0.3。

然后,Linux 内核再在这个 IP 包前面加上二层数据帧头,即原理图中的 Outer Ethernet Header,并把 Node 2 的 MAC 地址填进去。这个 MAC 地址本身,是 Node 1 的 ARP 表要学习的内容,无需 Flannel 维护。这时候,我们封装出来的“外部数据帧”的格式,如下所示:

这样,封包工作就宣告完成了。

接下来,Node 1 上的 flannel.1 设备就可以把这个数据帧从 Node 1 的 eth0 网卡发出去。显然,这个帧会经过宿主机网络来到 Node 2 的 eth0 网卡。

这时候,Node 2 的内核网络栈会发现这个数据帧里有 VXLAN Header,并且 VNI=1。所以 Linux内核会对它进行拆包,拿到里面的内部数据帧,然后根据 VNI 的值,把它交给 Node 2 上的flannel.1 设备。

而 flannel.1 设备则会进一步拆包,取出“原始 IP 包”。接下来就是docker正常的网络流转了。最终,IP 包就进入到了 container-2 容器的 Network Namespace里。

host-gw

与 udp和vxlan 模式不同,host-gw是一种纯三层网络方案。

假设现在,Node 1 上的 Infra-container-1,要访问 Node 2 上的 Infra-container-2。

设置 Flannel 使用 host-gw 模式:

重启flannel后,flanneld 会在宿主机上创建这样一条规则,以Node 1 为例:

1
2
3
$ ip route
...
10.244.1.0/24 via 10.168.0.3 dev eth0

可以看到,host-gw 模式的工作原理,其实就是将每个 Flannel 子网(Flannel Subnet,比如:10.244.1.0/24)的“下一跳”,设置成了该子网对应的宿主机的 IP 地址。

也就是说,这台“主机”(Host)会充当这条容器通信路径里的“网关”(Gateway)。这也正是“host-gw”的含义。

当然,Flannel 子网和主机的信息,都是保存在 Etcd 当中的。flanneld 只需要 WACTH 这些数据的变化,然后实时更新路由表即可。

优略点

udp模式:使用设备flannel.0进行封包解包,不是内核原生支持,上下文切换较大,性能非常差。
vxlan模式:使用flannel.1进行封包解包,内核原生支持,性能较强。
host-gw模式:无需flannel.1这样的中间设备,无需额外的解封包操作,直接宿主机当作子网的下一跳地址,性能最强。但又因host-gw是把宿主机网卡当作网关,所以集群节点要在一个局域网内(二层联通性)。

host-gw的性能损失大约在10%左右,而其他所有基于VXLAN“隧道”机制 的网络方案,性能损失在20%~30%左右。

参考文章

《深入解析容器跨主机网络》 张磊

CATALOG
  1. 1. Flannel
    1. 1.1. udp
    2. 1.2. vxlan
    3. 1.3. host-gw
  2. 2. 优略点
  3. 3. 参考文章