Flannel的三种工作模式

在 Docker 的默认配置下,一台宿主机上的 docker0 网桥,和其他宿主机上的 docker0 网桥,没有任何关联,它们互相之间也没办法连通。所以,连接在这些网桥上的容器,自然也没办法进行通信了。不过,万变不离其宗。如果我们通过软件的方式,创建一个整个集群“公用”的网桥,然后把集群里的所有容器都连接到这个网桥上,不就可以相互通信了吗?这样一来,我们整个集群里的容器网络就会类似于下图所示的样子

  

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

而这个 Overlay Network 本身,可以由每台宿主机上的一个“特殊网桥”共同组成。甚至,每台宿主机上,都不需要有一个这种特殊的网桥,而仅仅通过某种方式配置宿主机的路由表,就能够把数据包转发到正确的宿主机上。

为了解决容器“跨主通信”的问题,社区里出现了很多的容器网络方案,要理解容器“跨主通信”的原理,就一定要先从 Flannel 这个项目说起。Flannel 项目是 CoreOS 公司主推的容器网络方案。事实上,Flannel 项目本身只是一个框架,真正为我们提供容器网络功能的,是 Flannel 的后端实现。目前,Flannel 支持三种后端实现,分别是:UDP、VXLAN、host-gw。

1、UDP

UDP是 Flannel 项目最早支持的一种方式,却也是性能最差的一种方式,这个模式目前已经被弃用。不过,Flannel 之所以最先选择 UDP 模式,就是因为这种模式是最直接、也是最容易理解的容器跨主网络实现。

在由 Flannel 管理的容器网络里,Flannel会为每一台宿主机分配一个“子网”,一台宿主机上的所有容器,都属于这个“子网”,即都属于同一个网段。

这些子网与宿主机的对应关系,正是保存在 Etcd 当中,可通过以下命令查看:

$ etcdctl ls /coreos.com/network/subnets
/coreos.com/network/subnets/100.96.1.0-24
/coreos.com/network/subnets/100.96.2.0-24
/coreos.com/network/subnets/100.96.3.0-24 

子网对应的宿主机的 IP 地址,可通过以下命令查看:

$ etcdctl get /coreos.com/network/subnets/100.96.2.0-24
{"PublicIP":"10.168.0.3"}

而Flannel的工作模式,可通过以下命令查看:

$ etcdctl get /coreos.com/network/config 
{"Network":"172.10.0.0/16", "SubnetLen": 24, "Backend": {"Type": "host-gw"}}

Flannel 会在每个宿主机上启动一个flanneld进程,flanneld进程会创建一个叫做flannel0的设备,并添加一系列的路由规则,用来指定数据包的下一跳。这个 flannel0 设备是一个TUN 设备(Tunnel 设备),TUN 设备是一种工作在三层的虚拟网络设备,其功能非常简单,即:在操作系统内核和用户应用程序之间传递 IP 包。而添加的路由规则则包含了流入flannel0 设备和流出flannel0 设备的路由。

所以,当 IP 包从容器经过 docker0 出现在宿主机后,就会根据路由表进入 flannel0 设备,然后,宿主机上的 flanneld 进程就会收到这个 IP 包,然后,flanneld 就会根据这个 IP 包的目的地址,匹配到对应的子网,从 Etcd 中找到这个子网对应的宿主机的 IP 地址,然后,flanneld就会把这个IP包直接封装在一个 UDP 包里,通过物理网络发送给目的宿主机。这时候,容器间的通信就转变为两个节点间的通信,只要节点是互通的,那容器就可以互通。

当然,这个请求得以完成的原因是,每台宿主机上的 flanneld进程,都监听着一个 8285 端口,所以源宿主机的 flanneld 只要把 UDP 包发往目的宿主机的 8285 端口即可,目的宿主机的flanneld 就可以从这个 UDP 包里解析出封装在里面的源容器发来的原 IP 包。

接下来,flanneld 会直接把这个 IP 包发送给它所管理的 TUN 设备,即 flannel0 设备,Linux内核会根据路由规则,把这个 IP 包转发给 docker0 网桥,docker0 网桥会扮演二层交换机的角色,将数据包发送给正确的端口,进而通过 Veth Pair 设备进入到目的容器里。

以上,就是基于 Flannel UDP 模式的跨主通信的基本原理了,如下图所示:

  

可以看到,UDP 模式提供的其实是一个三层的 Overlay 网络,即:它首先对发出端的 IP 包进行 UDP 封装,然后在接收端进行解封装拿到原始的 IP 包,进而把这个 IP 包转发给目标容器。

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

  

UDP 小结:

工作在三层的overlay网络;

三次用户态与内核态之间的数据拷贝;

UDP 封装和解封装的过程,也都是由flanneld进程在用户态完成的;

这些上下文切换和用户态操作的代价其实是比较高的,这也正是造成 Flannel UDP 模式性能不好的主要原因。

我们在进行系统级编程的时候,有一个非常重要的优化原则,就是要减少用户态到内核态的切换次数,并且把核心的处理逻辑都放在内核态进行。这也是为什么,Flannel 后来支持的VXLAN 模式,逐渐成为了主流的容器网络方案的原因。

2、VXLAN

VXLAN即Virtual Extensible LAN(虚拟可扩展局域网),是 Linux 内核本身就支持的一种网络虚似化技术。VXLAN 的覆盖网络(Overlay Network)的设计思想是:在现有的三层网络之上,“覆盖”一层虚拟的、由内核 VXLAN 模块负责维护的二层网络,使得连接在这个 VXLAN 二层网络上的“主机”(虚拟机或者容器都可以)之间,可以像在同一个局域网(LAN)里那样自由通信。

VXLAN 会在宿主机上设置一个特殊的网络设备 VTEP,即:VXLAN Tunnel End Point(虚拟隧道端点),而 VTEP 设备的作用,是对二层数据帧进行封装和解封装,且这个工作的执行流程,全部是在内核里完成的。Flannel也会在各节点上添加相应的路由规则,用来指定数据包的下一跳,并维护一个ARP表,存储各节点的VTEP设备的MAC地址,也会维护一个FDP转发数据库,存储各VTEP设备所在的宿主机ip。

上述基于 VTEP 设备进行“隧道”通信的流程,如下图所示:

  

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

整体通信流程如下: 每台设备上已经有了VTEP设备和相应的路由规则,源容器发出请求后,数据包包含了源容器ip和目的容器ip,这个原始数据包首先会通过虚拟网卡对出现在docker0网桥上,然后根据路由规则到达VTEP设备,且要发往的网关地址为目的VTEP设备的ip,然后VTEP会从ARP表里获取到对应的MAC地址,并进行二层封包,得到一个二层数据帧,我们把它称为“内部数据帧”。

接下来,Linux 内核会在“内部数据帧”前面,加上一个特殊的 VXLAN 头,用来表示这实际上是一个 VXLAN 要使用的数据帧,而这个 VXLAN 头里有一个重要的标志叫作 VNI,它是 VTEP 设备识别某个数据帧是不是应该归自己处理的重要标识,而在 Flannel 中,VNI 的默认值是 1。

然后,Linux 内核会从FDB数据库里读取到对应的目的宿主机ip,把“内部数据帧”和目的宿主机ip封装进一个 UDP 包里,我们把它称为“外部数据帧”,这个“外部数据帧”里包含了源节点ip和目的节点ip,然后Linux内核会在这个“外部数据帧”前面加上目的宿主机的MAC地址,这样,封包工作就宣告完成了。

所以,这个普通UDP包的数据部分并不是单纯的数据,而是一个包含了容器ip的数据帧,这个数据帧里又包含了容器真正传输的数据。

接下来,源宿主机上的 flannel.1 设备就可以把这个数据帧从源宿主机的 eth0 网卡发出去,数据帧会经过宿主机网络来到目的宿主机的 eth0 网卡。这时候,目的宿主机的内核网络栈会发现这个数据帧里有 VXLAN Header,并且 VNI=1。所以 Linux 内核会对它进行拆包,拿到里面的内部数据帧,然后根据 VNI 的值,把它交给 flannel.1 设备。而 flannel.1 设备则会进一步拆包,取出“原始 IP 包”,然后根据路由规则,将它交给docker0网桥,经过虚拟网卡对就进入到了目的容器里。

VXLAN 小结:

工作在二层的overlay网络;

只有一次用户态到内核态之间的数据拷贝;

UDP 封装和解封装的过程,是由flannel.1设备在内核态完成;

相较UDP模式而言,性能更好。

UDP 和 VXLAN 模式对比:

都是在现有的物理网络之上,通过软件创建一层虚拟的覆盖网络(Overlay Network),把不同宿主机上创建的虚拟设备连通,从而达到容器跨主机通信的目的。在每一个节点上启动flanneld进程,分配子网,创建虚拟设备,添加路由规则,维护相关表等。

模式

OSI

虚拟设备

拷贝次数

封装/解封装

维护表

内核原生支持

UDP

三层

flannel0(TUN)

三次

flanneld在用户态完成

etcd

route

VXLAN

二层

flannel.1(VTEP)

一次

flannel.1在内核态完成

etcd、route

ARP、FDB

所以,VXLAN相较UDP模式,性能更好。

上面介绍的工作流程,也正是 Kubernetes 对容器网络的主要处理方法。只不过,Kubernetes 是通过一个叫作 CNI 的接口,维护了一个单独的网桥来代替 docker0。这个网桥的名字就叫作:CNI 网桥,它在宿主机上的设备名称默认是:cni0。

以 Flannel 的 VXLAN 模式为例,在 Kubernetes 环境里,它的工作方式跟上面描述的没有任何不同。只不过,docker0 网桥被替换成了 CNI 网桥而已,如下所示:

  

 

需要注意的是,CNI 网桥只是接管所有 CNI 插件负责的、即 Kubernetes 创建的容器(Pod)。而此时,如果你用 docker run 单独启动一个容器,那么 Docker 项目还是会把这个容器连接到 docker0 网桥上。

3、host-gw

host-gw的工作原理非常简单,就是将每个 Flannel 子网的“下一跳”,设置成了该子网对应的宿主机的 IP 地址。也就是说,这台“主机”(Host)会充当这条容器通信路径里的“网关”(Gateway)。这也正是“host-gw”的含义。当然,Flannel 子网和主机的信息,都是保存在 Etcd 当中的。flanneld 只需要 WACTH 这些数据的变化,然后实时更新路由表即可。如下图所示:

  

flanneld会在宿主机上创建相应的路由规则,设置到某一目的网段的的数据包,应该经过本机的 eth0 设备发出去,且下一跳地址是目的主机ip。

所以,当 IP 包从网络层进入链路层封装成帧的时候,eth0 设备就会使用下一跳地址对应的 MAC 地址,作为该数据帧的目的 MAC 地址,这样,这个数据帧就会从 源宿主机通过宿主机的二层网络顺利到达目的宿主机上。目的宿主机拿到数据包后,根据路由表进入cni0网桥,进而进入到目的容器中。

在这种模式下,容器通信的过程就免除了额外的封包和解包带来的性能损耗。根据实际的测试,host-gw 的性能损失大约在 10% 左右,而其他所有基于 VXLAN“隧道”机制的网络方案,性能损失都在 20%~30% 左右。

host-gw 小结:

纯三层的网络方案;

无需创建虚拟设备,只需要添加相应路由规则即可;

免除了额外的虚拟设备的封包和解封包过程;

需要集群宿主机之间二层连通;

相较UDP和VXLAN,性能最好;

posted on 2021-07-04 12:58  流年似水zlw  阅读(1629)  评论(1编辑  收藏  举报

导航