容器网络的跨宿主机通信

2022-11-02 09:29:18 浏览数 (1)

容器的跨宿主机通信

通过第一章容器网络基础的学习,我们已经实现了单机容器间的互通、容器访问外部网络及容器对外提供服务。 在实际的应用场景中,为了保证业务的高可用性,我们的容器多是跨宿主机部署的,并且部署在不同宿主机上的容器会进行大量的网络通信。那么,怎么实现容器的跨宿主机通信呢?

如果熟悉网络的同学,那么一定知道解决这个问题的思路:

  1. 思路1:通过配置宿主机和容器集群的路由,实现underlay网络的打通。
  2. 思路2:把所有的容器连接在虚拟网络上,通过overlay方案实现互通。

方案1 underlay网络方案

方案2 voerlay网络方案

在社区中,用于解决跨主机通信的方案主要有以下几种:

  1. Docker 原生的overlay 和 macvlan。可参考:https://docs.docker.com/network/
  2. 第三方方案,比较常见的有flannel和calico。

1 Flannel Docker部署及配置

我们通过Flannel项目来探讨容器的跨主机网络通信原理。Flannel项目是CoreOS公司主推的overlay容器网络方案。事实上,Flannel项目本身只是一个框架,真正为我们提供网络功能的,是Flannel的后端实现。目前,主要支持四种后端,分别是:

  1. UDP。仅限于调试,诞生时间比较早,当时linux内核不支持VXLAN。因性能差,现在已经被淘汰。
  2. VXLAN。社区推荐的选择。
  3. host-gw。对于网络有极高性能要求时,基础设施能支撑,且有丰富经验的使用者,推荐使用host-gw。(在云中无法使用)。
  4. 其他后端,如知名云平台。

为了便于理解原理,我们先用2台host搭建一个Docker Flannel。具体可参考:https://www.cnblogs.com/hukey/p/14296710.html

我们自己环境信息如下

host1,OS:centos 7.9,IP:10.0.70.44,作为master节点。

host2,OS:centos 7.9,IP:10.0.70.36。

注:配置flannel时,我们为容器设置的是172.18.0.0/16的网段,选用的type是vxlan。

2. Flannel VxLAN原理浅析

我们在两台宿主机host1和host2上,分别运行busybox容器,命名为container-1,container-2.

代码语言:javascript复制
#host1
$ docker run -d -it --name container-1 busybox /bin/sh
#host2
$ docker run -d -it --name container-2 busybox /bin/sh

接下来,我们在container-1来访问container-2的IP地址,发现2个容器的网络已经连通,这正是由部署在宿主机上的flannel进程帮我们做到的。

代码语言:javascript复制
# host1 
$ docker exec -it container-1 /bin/sh
$ ping 172.18.28.2

我们通过上图对container1访问container2的流程进行分析。

container-1到docker0

当container-1访问container-2时,container-1的进程发起请求,container-1的源IP地址为172.18.57.2,目的地址为container-2的IP地址172.18.28.2。由于目的IP地址172.18.28.2并不在host1的docker0网桥的网段(172.18.57.0/24)里,所以这个IP包会被转发给默认路由规则,通过容器的网关docker0网桥(如果是同一台宿主机上的容器间通信,走的是直连规则,见上一章节),从而出现在宿主机上。

docker0到flannel.1

这个时候,这个IP包的下一个目的地,就取决于宿主机上的路由规则了,查看宿主机host1路由规则。通过最长匹配原则可知,前往目的地172.18.28.2的IP包,进入了一个叫flannel.1的设备。

实际上,flannel.1是一个VTEP设备。VTEP是什么?

我们首先来看一下VXLAN协议,VXLAN是Virtual eXtensible Local Area Network的缩写,RFC 7348的标题“A Framework for Overlaying Virtualized Layer 2 Networks over Layer 3 Networks”,说明了VXLAN是一个在传统Layer 3网络上架设出来的Layer 2 overlay网络

在下面的场景中,有2台服务器Server 1和Server 2,由3层网络连接。这2台服务器可能不在一个机架,甚至间隔很远的数据中心。有条个VXLAN隧道,标识分别是VNI 22, 34, 74,98。 注意看Server 1的 VM1-1和Server 2 VM2-4这2台机器,他们属于同一个VxLAN隧道,标识为VNI 22,两台机器可以互通,也就是我们常说的,他们处于同一个大二层网络中。事实上,对于linux机器来讲,他并不知道overlay网络,也不知道VXLAN报文的封装和解封装过程,因为这些过程都是由VTEP来完成。

所以,VTEP的功能就是负责VXLAN的封装和解封装。值得一提的是,VTEP的工作过程,全部是在内核态完成的。(这有什么好处?稍后会介绍)

flannel.1处理流程

介绍完VxLAN的基本原理和概念,我们来继续分析一下container-1访问container-2的整个流程。

前面我们介绍,container-1访问container-2的路由,经过docker0网桥,来到了VTEP设备 flannel.1,也就是VxLAN隧道的入口。

VTEP设备对原始报文进行封装,为了能够将原始报文发送到正确的宿主机,VXLAN需要知道对端VTEP设备(也就是宿主机2的VTEP设备flannel.1)信息,正是在每台机器上的flanneld进程负责维护。在host2启动并加入Flannel网络之后,在host1上,通过ip route查看路由规则会发现,flanneld添加了如下一条规则:172.18.0.0/16 dev flannel.1.

这条规则的意思是:凡是发往172.18.0.0/16网段的数据包,都要经过flannel.1设备发出。回想VXLAN原理,在3层网络上假设出来的2层网络,那就意味着,我们需要知道目的端VTEP设备的MAC地址

根据前面的路由信息,我们已经知道了目的端VTEP的IP地址,可以通过ARP(Address Resolution Protocol)根据3层IP地址来查询对应的2层MAC地址。而这里用到的ARP记录,是flanneld进程在host2节点启动时,自动添加到host1上的。我们可以通过ip命令查看,可以看到172.18.28.0对应的MAC地址为56:63:6a:db:6c:b1。

那么问题又来了,目的端VTEP的IP地址host1是如何知道的?

这里,就用到了Flannel项目非常重要的概念:子网(Subnet)。大家应该记得,我们在安装Flannel时,有设置过子网,我们设置的子网范围为172.18.0.0/16网段。在我们的例子中,分配给host1的子网为172.18.57.0/24,分配给host2的子网为172.18.28.0/24.在flannel管理的容器网络中,一台宿主机的所有容器,都属于该宿主机被分配的一个子网。在我们的环境中,可以通过如下命令查看

代码语言:javascript复制
$ etcdctl ls /atomic.io/networks/subnets

综上所述,flanneld进程在处理由flannel.1传入的IP包时,就根据目的IP地址(172.18.28.2)匹配到对应子网(172.18.28.0/24),从etcd中找到这个子网对应的宿主机IP地址,10.0.70.36.可通过执行如下命令查看。

代码语言:javascript复制
$ etcdctl get /atomic.io/network/subnets/172.18.28.0-24

而对于 flanneld 来说,只要 host1 和 host2 是互通的,那么 flanneld 作为 host1 上的一个普通进程,就一定可以通过上述 IP 地址(10.0.70.36)访问到 host2.

好了,我们继续前面的话题。有了目的VTEP的MAC地址后,linux内核就开始封装2层报文了,将目的VTEP的MAC地址填入inner Ethernet Header字段,目的容器的地址,填入Inner IP Header字段,依然是container-2的IP地址,即172.18.28.2,这个二层桢(为了方便,我们叫它vxlan数据帧)的格式如下:

需要注意的是,这些VTEP设备的MAC地址,对于宿主机并没有实在意义,也不能在宿主机的二层网络里传输。所以接下来,宿主机内核会把前面的vxlan数据帧进一步封装成为一个宿主机网络里的一个普通数据帧,好让他载着vxlan数据帧,通过宿主机的eth0网卡进行传输。

宿主机先给vxlan数据帧加一个特殊的VXLAN头,表明该数据帧是VXLAN要是用的数据帧,里面有一个重要的标志是VNI。在Flannel中,VNI默认值是1,所以宿主机上的VTEP设备叫做flannel.1,这里的1就是VNI的值。

然后,宿主机会把报文封装进一个UDP的包里面发出去。在宿主机看来,会认为是自己的flannel.1设备向另一台宿主机的falnnel.1设备发起的一次普通的UDP连接。

源端flannel.1知道了目的端flannel.1的MAC地址,却不知道目的端宿主机IP地址,那么这个UDP的数据包发往哪台宿主机呢?在linux内核里,网桥的转发的依据来自于FDB(Forwarding DataBase)。flannel.1对应的FDB信息,这是由flanneld进程负责维护,可以通过以下命令查看。

代码语言:javascript复制
$ bridge fdb show flannel.1 | grep 56:63:6a:db:6c:b1

可以看到,发往目的VTEP(56:63:6a:db:6c:b1)的数据帧,经过flannel.1设备发给10.0.70.36的宿主机,这台宿主机正是host2.

接下来,是宿主机上一个正常的封包流程,只不过宿主机没想到里面包含了这么多内容。至此,封包完成。然后经由宿主机host1的eth0端口将报文发送出去,发送给宿主机host2.

宿主机host2接到报文后,发现里面VNI标识为1.linux内核会对它拆包,根据VNI=1,将报文交给flannel.1处理。flannel会进一步拆包,对报文进行处理,这时就回到了单机容器网络通信的流程,最终达到container-2的network namespace。

重新简述一遍container-1访问container-2的流程:

  1. container-1发起访问container-2的请求,网络流量进入docker0网桥。(从内核态向用户态的转变)。
  2. docker0查看宿主机路由表,根据最长路由匹配原则,下一跳交给flannel.1(VTEP设备)进行处理。
  3. flannel.1对报文进行封装,然后将报文发给VxLAN隧道,通过UDP进行转发。
  4. UDP是4层报文,linux内核会进行普通报文的封装,加上三层目的IP地址和2层目的MAC地址,然后由网卡eth0将流量转发出去,送往host2.
  5. host2接到报文后,是一个逆向处理的过程。

3. Flannel UDP原理

理解了Flannel VxLAN的网络通信原理后,理解UDP通信原理就非常容易。UDP也是一个三层的Overlay方案,类似于VxLAN,它首先对报文进行UDP封装,而不是VxLAN封装,其原理如下:

这次,报文从docker0网桥来到了flannel0。flannel0是一个linux的TUN设备,功能主要是在操作系统内核和用户应用程序之间传递IP包。

所以,flannel0在接到报文之后,会将报文交给flnenld进程进行处理。这是一个由内核态向用户态转换的过程。

flanneld在看到这个IP报文的目的地是172.18.28.2,就将报文封装在UDP内,转发给了host2.为什么知道要发给host2呢?就是利用了前面介绍的flannel的子网的概念。

UDP之所以被弃用,主要是因为性能问题,而性能的损耗主要源自用户态和内核态之间的

转换。我们看下面这张图,由于使用了flannel0这个TUN设备,仅在发出报文的过程就经历了3次内核态和用户态之间的转换。

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

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

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

此外,我们还可以看到,Flannel 进行 UDP 封装和解封装的过程,也都是在用户态完成的。在 Linux 操作系统中,上述这些上下文切换和用户态操作的代价其实是比较高的,这也正是造成 Flannel UDP 模式性能不好的主要原因。这就是为什么,Flannel 后来支持的VXLAN 模式,逐渐成为了主流的容器网络方案的原因。

0 人点赞