Kubernetes中解决网络跨主机通信的一个经典插件就是Flannel。Flannel实质上只是一个框架,真正为我们提供网络功能的是后端的Flannel实现,目前Flannel后端实现的方式有三种:

  1. UDP
  2. VXLAN
  3. HOST-GW

一、UDP

UDP是最早的实现方式,但是由于其性能原因,现已经被废弃,但是UDP模式是最直接,也最容易理解的跨主机实现方式。

假如有两台Node,如下:

  1. Node01上有容器nginx01,其IP为172.20.1.107,其docker0的地址为172.20.1.1/24;
  2. Node02上有容器nginx02,其IP为172.20.2.133,其docker0的地址为172.20.2.1/24;

那么现在nginx01要访问nginx02,其流程应该是怎么样的呢?

  1. 首先从nginx01发送IP包,源IP是172.20.1.107,目的IP是172.20.2.133。
  2. 由于目的IP并不在Node01上的docker0网桥里,所以会将包通过默认路由转发到docker0网桥所在的宿主机上;
  3. 它会通过本地的路由规则,转发到下一个目的IP,我们可以通过ip route查看本地的路由信息,通过路由信息可以看到它被转发到一个flannel0的设备中;
  4. flannel0设备会把这个IP包交给创建这个设备的应用程序,也就是Flannel进程(从内核状态向用户状态切换);
  5. Flannel进程收到IP包后,将这个包封装在UDP中,就根据其目的地址将其转发给Node02(通过每个宿主机上监听的8285端口),这时候的源地址是Node01的地址,目的地址是Node02的地址;
  6. Node02收到包后,就会直接将其转发给flannel0设备,然后进行解包,匹配本地路由规则转发给docker0网桥,然后docker0网桥就扮演二层交换机的功能,将包转发到最终的目的地;

其流程图如下:
image.png
注:
1、flannel0是一个TUN设备,它的作用是在操作系统和应用程序之间传递IP包;
2、Flannel是根据子网(Subnet)来查看IP地址对应的容器是运行在那个Node上;
3、这些子网和Node的对应关系,是保存在Etcd中(仅限UDP模式);
4、UDP模式其实是一个三层的Overlay网络;它首先对发出的IP包进行UDP封装,然后接收端对包进行解封拿到原始IP,进而把这个包转发给目标容器。这就好比Flannel在不同的宿主机上的两容器之间打通了一条隧道,使得这个两个IP可以通信,而无需关心容器和宿主机的分布情况;

UDP之所以被废弃是主要是由于其仅在发包的过程中就在用户态和内核态进行来回的数据交换,这样的性能代价是很高的。如下:
5.1、Flannel - 图2

二、VXLAN

VXLAN:Virtual Extensible LAN(虚拟可扩展局域网),是Linux内核本身就支持的一种虚拟化网络技术,它可以完全在内核态实现上述的封装和解封装过程,减少用户态到内核态的切换次数,把核心的处理逻辑都放到内核态,其通过与前面相似的隧道技术,构建出覆盖网络或者叠加网络(Overlay Network)。

其设计思想为在现有的三层网络下,叠加一层虚拟的并由内核VXLAN维护的二层网络,使得连接在这个二层网络上的主机可以像在局域网一样通信。

为了能够在二层网络中打通隧道,VXLAN会在宿主机上设置一个特殊的网络设备作为隧道的两端,这个隧道就叫VTEP(Virtual Tunnel End Point 虚拟隧道端点)。而VTEP的作用跟上面的flanneld进程非常相似,只不过它进行封装和解封的对象是二层的数据帧,而且这个工作的执行流程全部在内核中完成。
其流程如下:
image.png
我们可以看到每台Node上都由一个flannel.1的网卡,它就是VXLAN所需要的VTEP设备,它既有IP地址,也由MAC地址。
现在我们nginx01要访问nginx02,其流程如下:

  1. nginx01发送请求包会被转发到docker0;
  2. 然后会通过路由转发到本机的flannel.1;
  3. flannel.1收到包后通过ARP记录找到目的MAC地址,并将其加原始包上,封装成二层数据帧(将源MAC地址和目的MAC地址封装在它们对应的IP头外部);
  4. Linux内核把这个数据帧封装成普通的可传输的数据帧,通过宿主机的eth0进行传输(也就是在原有的数据帧上面加一个VXLAN头VNI,它是识别某个数据帧是不是归自己处理的的重要标识,而在flannel中,VNI的默认值就是1,这是由于宿主机上的VTEP设备名称叫flannel.1,这里的1就是VNI的值);
  5. 然后Linux内核会把这个数据帧封装到UDP包里发出去;
  6. Node02收到包后发现VNI为 1,Linux内核会对其进行解包,拿到里面的数据帧,然后根据VNI的值把它交给Node02上的flannel.1设备,然后继续进行接下来的处理;

在这种场景下,flannel.1设备实际扮演的是一个网桥的角色,在二层网络进行UDP包的转发,在Linux内核中,网桥设备进行转发的依据是一个叫做FDB(Foewarding Database)的转发数据库,它的内容可以通过bridge fdb命令可以查看。

三、HOST-GW

前面的两种模式都是二层网络的解决方案,对于三层网络,Flannel提供host-gw解决方案。
以下是host-gw示意图:
image.png
如上所示,如果我nginx01要访问nginx02,则起流程如下:

  1. 转发请求包会被转发到cni0;
  2. 到达本机后会匹配本机的路由,如上的路由信息,然后发现要到172.20.2.0/24的请求要经过eth0出去,并且吓一跳地址为172.16.1.130;
  3. 到达Node2过后,通过路由规则到node02的cni0,再转发到nginx02;

其工作流程比较简单,主要是会在节点上生成许多路由规则。
host-gw的工作原理就是将Flannel的所有子网的下一跳设置成该子网对应的宿主机的IP地址,也就是说Host会充当这条容器通信路径的网关,当然,Flannel子网和主机的信息会保存在Etcd中,flanneld进程只需要WATCH这个数据的变化,然后实时更新路由表。

在这种模式下,就免除了额外的封包解包的性能损耗,在这种模式下,性能损耗大约在10%左右,而XVLAN隧道的机制,性能损耗大约在20%~30%。

从上面可以知道,host-gw的工作核心为IP包在封装成帧发送出去的时候会在使用路由表中写下一跳来设置目的的MAC地址,这样它就会经过二层转发到达目的宿主机。这就要求集群宿主机必须是二层连通的。

要修改flannel模式就修改如下配置:

  1. net-conf.json: |
  2. {
  3. "Network": "172.20.0.0/16",
  4. "Backend": {
  5. "Type": "host-gw"
  6. }
  7. }
  8. net-conf.json: |
  9. {
  10. "Network": "172.20.0.0/16",
  11. "Backend": {
  12. "Type": "vxlan",
  13. "Directrouting": true
  14. }
  15. }