Kubernetes容器网络(一):Flannel网络原理

前言

本文主要分享Flannel如何解决跨主机容器之间通信问题的,如果你对主机内容器之间通信流程还不了解,建议先看下这篇文章:Docker网络原理

1、前置网络知识

1)、tun/tap设备

tun/tap设备在虚拟机的组网过程中起到作用。tun表示虚拟的是点对点设备,tap表示虚拟的是以太网设备,这两种设备针对网络包实施不同的封装

tun/tap设备到底是什么?从Linux文件系统的角度看,它是用户可以用文件句柄操作的字符设备;从网络虚拟化角度看,它是虚拟网卡,一端连着网络协议栈,另一端连着用户态程序

tun/tap设备有什么作用呢?tun/tap设备可以将TCP/IP协议栈处理好的网络包发送给任何一个使用tun/tap驱动的进程,由进程重新处理后发到物理链路中。tun/tap设备就像是埋在用户空间的一个钩子,我们可以很方便地将对网络包的处理程序挂载这个钩子上

1)tun/tap设备的工作原理

上图是通过Socket调用实现用户态和内核态数据交互的过程。物理网卡从网线接收数据后送达网络协议栈,而进程通过Socket创建网络套接字,从网络协议栈读取数据

tun/tap设备其实就是利用Linux的设备文件实现内核态和用户态的数据交互,而访问设备文件则会调用设备驱动相应的例程,要知道设备驱动也是内核态和用户态的一个接口

普通的物理网卡通过网线收发数据包,而tun设备通过一个设备文件(/dev/tunX)收发数据包。所有对这个文件的写操作会通过tun设备转换成一个数据包传送给内核网络协议栈。当内核发送一个包给tun设备时,用户态的进程通过读取这个文件可以拿到包的内容。当然,用户态的程序也可以通过写这个文件向tun设备发送数据

tap设备与tun设备的工作原理完全相同,区别在于:

  • tun设备的/dev/tunX文件收发的是IP包,因此只能工作在三层,无法与物理网卡做桥接,但可以通过三层交换(例如ip_forward)与物理网卡连通
  • tap设备的/dev/tapX文件收发的是链路层数据包,可以与物理网卡做桥接

2)利用tun设备部署一个VPN

tun/tap设备的用处是将协议栈中的部分数据包转发给用户空间的应用程序,给用户空间的程序一个处理数据包的机会。常见的tun/tap设备使用场景有数据压缩、加密等,最常见的是VPN,包括tunnel及应用层的IPSec等

如上图所示,数据包的流转过程包括:

  1. App1是一个普通的程序,通过Socket API发送了一个数据包,假设这个数据包的目的IP地址是192.168.1.3(和tun0在同一个网段)
  2. 程序A的数据包到达网络协议栈后,协议栈根据数据包的目的IP地址匹配到这个数据包应该由tun0网口出去,于是将数据包发送给tun0网卡
  3. tun0网卡收到数据包之后,发现网卡的另一端被App2打开了(这也是tun/tap设备的特点,一端连着协议栈,另一端连着用户态程序),于是将数据包发送给App2
  4. App2收到数据包之后,通过报文封装(将原来的数据包封装在新的数据报文中,假设新报文的源地址是eth0的地址,目的地址是和eth0在同一个网段的VPN对端IP地址,例如100.89.104.22)构造出一个新的数据包。App2通过同样的Socket API将数据包发送给协议栈
  5. 协议栈根据本地路由,发现这个数据包应该通过eth0发送出去,于是将数据包交给eth0,最后eth0通过物理网络将数据包发送给VPN的对端

综上所述,发到192.168.1.0/24网络的数据首先通过监听在tun0设备上的App2进行封包,利用eth0这块物理网卡发到远端网络的物理网卡上,从而实现VPN

VPN网络的报文真正从物理网卡出去要经过网络协议栈两次,因此会有一定的性能损耗。另外,经过用户态程序的处理,数据包可能已经加密,包头进行了封装,所以第二次通过网络栈内核看到的是截然不同的网络包

2)、VXLAN

VXLAN(虚拟可扩展的局域网)是一种虚拟化隧道通信技术。它是一种overlay(覆盖网络)技术,通过三层的网络搭建虚拟的二层网络

VXLAN是在底层物理网络(underlay)之上使用隧道技术,依托UDP层构建的overlay的逻辑网络,使逻辑网络与物理网络解耦,实现灵活的组网需求

1)VXLAN协议原理简介

如上图所示为VXLAN的工作模型,它创建在原来的IP网络(三层)上,只要是三层可达(能够通过IP互相通信)的网络就能部署VXLAN

在VXLAN网络的每个端点都有一个VTEP设备,负责VXLAN协议报文的封包和解包,也就是在虚拟报文上封装VTEP通信的报文头部。物理网络上可以创建多个VXLAN网络,可以将这些VXLAN看作一个隧道,不同节点上的虚拟机/容器能够通过隧道直连。通过VNI标识不同的VXLAN网络,使得不同的VXLAN可以相互隔离

VXLAN的几个重要概念如下:

  • VTEP(VXLAN Tunnel Endpoints):VXLAN网络的边缘设备,用来进行VXLAN报文的处理(封包和解包)。VTEP可以是网络设备(例如交换机),也可以是一台机器(例如虚拟机集群中的宿主机)
  • VNI(VXLAN Network Identifier):VNI是每个VXLAN的标识,是个24位整数,因此最大值是 2 24 2^{24} 224。如果一个VNI对应一个租户,那么理论上VXLAN可以支撑千万级别的租户
  • tunnel:隧道是一个逻辑上的概念,在VXLAN模型中并没有具体的物理实体相对应。隧道可以看作一种虚拟通道,VXLAN通信双方都认为自己在直接通信,并不知道底层网络的存在。从整体看,每个VXLAN网络像是为通信的虚拟机搭建了一个单独的通信通道,也就是隧道

VXLAN其实是在三层网络上构建出来的一个二层网络的隧道。VNI相同的机器逻辑上处理同一个二层网络中。VXLAN封包格式如下图:

在这里插入图片描述

VXLAN的报文就是MAC in UDP,即在三层网络的基础上构建一个虚拟的二层网络。VXLAN的封包格式显示原来的二层以太网帧(包含MAC头部、IP头部和传输层头部的报文),被放在VXLAN包头里进行封装,再套到标准的UDP头部(UDP头部、IP头部和MAC头部)用来在底层网络上传输报文

UDP目的端口是接收方VTEP设备使用的端口,IANA(互联网号码分配局)分配了4789作为VXLAN的目的UDP端口

2)VXLAN组网必要信息

VXLAN报文的转发过程就是:原始报文经过VTEP,被Linux内核添加上VXLAN包头及外层的UDP头部,再发送出去,对端VTEP接收到VXLAN报文后拆除外层UDP头部,并根据VXLAN头部的VNI把原始报文发送到目的的服务器

一个完整的VXLAN报文需要哪些信息?

  • 内层报文:通信双方的IP地址已经确定,需要VXLAN填充的是对方的MAC地址,VXLAN需要一个机制实现ARP的功能
  • VXLAN头部:只需要知道VNI。它一般是直接配置在VTEP上的,即要么是提前规划的,要么是根据内部报文自动生成的
  • UDP头部:最重要的是源地址和目的地址的端口,源地址端口是由系统自动生成并管理的,目的端口一般固定位IANA分配的4789端口
  • IP头部:IP头部关系的是对端VTEP的IP地址,源地址可以用简单的方式确定,目的地址是虚拟机所在地址宿主机VTEP的IP地址,需要由某种方式来确定
  • MAC头部:确定了VTEP的IP地址,MAC地址可以通过ARP方式获取,毕竟VTEP在同一个三层网络内

总结一下,一个VXLAN报文需要确定两个地址信息:内层报文(对应目的虚拟机/容器)的MAC地址和外层报文(对应目的虚拟机/容器所在宿主机的VTEP)IP地址

3)点对点的VXLAN

点对点的VXLAN即两台机器构成一个VXLAN网络,每台机器上有一个VTEP,VTEP之间通过它们的IP地址进行通信。点对点VXLAN网络拓扑如下图:

使用ip link命令创建VXLAN接口:

[root@aliyun ~]# ip link add vxlan0 type vxlan id 42 dstport 4789 remote 172.19.216.112 local 172.19.216.111 dev eth0

上面这条命令创建了一个名为vxlan0,类型为vxlan的网络接口,一些重要的参数如下:

  • id 42:指定VNI的值,有效值在1到 2 24 2^{24} 224之间
  • dstport:VTEP通信的端口,IANA分配的端口是4789
  • remote 172.19.216.112:对端VTEP的地址
  • local 172.19.216.111:当前节点VTEP要使用的IP地址,即当前节点隧道口的IP地址
  • dev eth0:当前节点用于VTEP通信的网卡设备,用来获取VTEP IP地址

它的详细信息:

[root@aliyun ~]# ip -d link show dev vxlan0
3: vxlan0: <BROADCAST,MULTICAST> mtu 1450 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 26:1a:4d:9b:a3:ed brd ff:ff:ff:ff:ff:ff promiscuity 0 minmtu 68 maxmtu 65535 
    vxlan id 42 remote 172.19.216.112 local 172.19.216.111 dev eth0 srcport 0 0 dstport 4789 ttl auto ageing 300 udpcsum noudp6zerocsumtx noudp6zerocsumrx addrgenmode eui64 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535

为刚创建的VXLAN网卡配置IP地址并启用它:

[root@aliyun ~]# ip addr add 172.17.1.2/24 dev vxlan0
[root@aliyun ~]# ip link set vxlan0 up

[root@aliyun ~]# ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.19.216.111  netmask 255.255.240.0  broadcast 172.19.223.255
        inet6 fe80::216:3eff:fe29:7a94  prefixlen 64  scopeid 0x20<link>
        ether 00:16:3e:29:7a:94  txqueuelen 1000  (Ethernet)
        RX packets 3146  bytes 780076 (761.7 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 3495  bytes 549248 (536.3 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 2  bytes 140 (140.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 2  bytes 140 (140.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

vxlan0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1450
        inet 172.17.1.2  netmask 255.255.255.0  broadcast 0.0.0.0
        inet6 fe80::241a:4dff:fe9b:a3ed  prefixlen 64  scopeid 0x20<link>
        ether 26:1a:4d:9b:a3:ed  txqueuelen 1000  (Ethernet)
        RX packets 45  bytes 3254 (3.1 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 45  bytes 3258 (3.1 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

执行成功后会发现路由表项多了下面的内容,所有目的地址是172.17.1.0/24网段的包要通过vxlan0转发:

[root@aliyun ~]# ip route
172.17.1.0/24 dev vxlan0 proto kernel scope link src 172.17.1.2

同时,vxlan0的FDB表项中标的内容如下:

[root@aliyun ~]# bridge fdb
00:00:00:00:00:00 dev vxlan0 dst 172.19.216.112 via eth0 self permanent

这个表项的意思是,默认的VTEP对端地址为172.19.216.112。换句话说,原始报文经过vxlan0后会被内核添加上VXLAN头部,而外部UDP头的目的IP地址会被带上172.19.216.112

在另外一台机器上(172.19.216.112)进行配置:

# 创建VXLAN接口
[root@aliyun ~]# ip link add vxlan0 type vxlan id 42 dstport 4789 remote 172.19.216.111 local 172.19.216.112 dev eth0
# 为刚创建的VXLAN网卡配置IP地址并启用它
[root@aliyun ~]# ip addr add 172.17.1.3/24 dev vxlan0
[root@aliyun ~]# ip link set vxlan0 up

测试两个VTEP的连通性:

[root@aliyun ~]# ping 172.17.1.3
PING 172.17.1.3 (172.17.1.3) 56(84) bytes of data.
64 bytes from 172.17.1.3: icmp_seq=1 ttl=64 time=0.720 ms
64 bytes from 172.17.1.3: icmp_seq=2 ttl=64 time=0.346 ms
64 bytes from 172.17.1.3: icmp_seq=3 ttl=64 time=0.343 ms

2、flannel backend详解

Flannel通过在每一个节点上启动一个叫做flanneld的进程,负责每一个节点上的子网划分,并将相关的配置信息(如各个节点的子网网段、外部IP等)保存到etcd中,而具体的网络包转发交给具体的backend实现

flanneld可以在启动时通过配置文件指定不同的backend进行网络通信,Flannel支持三种backend:UDP、VXLAN、host-gw。目前VXLAN是官方最推崇的一种backend实现方式;host-gw一般用于对网络性能要求比较高的场景,但需要基础网络架构的支持;UDP则用于测试及一些比较老的不支持VXLAN的Linux内核

1)、UDP

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

在这个例子中,有两台宿主机:

  • 宿主机Node1的IP地址是172.19.216.114,上面有一个容器A,它的IP地址是10.244.1.2,对应的cni0网桥的地址是10.244.1.1/24
  • 宿主机Node2的IP地址是172.19.216.115,上面有一个容器B,它的IP地址是10.244.2.3,对应的cni0网桥的地址是10.244.2.1/24

我们来逐步分析UDP模式下容器A访问容器B的整个过程

1)容器A里的进程发起的IP包,其源地址是10.244.1.2,目的地址是10.244.2.3

容器A路由表如下:

# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         10.244.1.1      0.0.0.0         UG    0      0        0 eth0
10.244.0.0      10.244.1.1      255.255.0.0     UG    0      0        0 eth0
10.244.1.0      0.0.0.0         255.255.255.0   U     0      0        0 eth0

匹配到10.244.0.0/16对应的这条路由规则,应该将IP包发送到网关10.244.1.1(cni0网桥),通过容器的网关进入cni0网桥从而出现在宿主机上

2)这时候,这个IP包的下一个目的地,就取决于宿主机上的路由规则了。此时,Flannel已经在宿主机上创建出了一系列的路由规则,Node1路由表如下:

[root@k8s-node1 ~]# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         172.19.223.253  0.0.0.0         UG    100    0        0 eth0
10.244.0.0      0.0.0.0         255.255.0.0     U     0      0        0 flannel0
10.244.1.0      0.0.0.0         255.255.255.0   U     0      0        0 cni0
172.17.0.0      0.0.0.0         255.255.0.0     U     0      0        0 docker0
172.19.208.0    0.0.0.0         255.255.240.0   U     100    0        0 eth0

由于我们的IP包目的地址是10.244.2.3,它匹配不到本地cni0网桥对应的10.244.1.0/24网段,只能匹配到10.244.0.0/16对应的这条路由规则,从而进入一个叫作flannel0的设备中

3)flannel0为tun设备,tun设备是一种工作在三层的虚拟网络设备,它的功能就是在操作系统内核和用户应用程序之间传递IP包

以flannel0设备为例:像上面提到的情况,当操作系统将一个IP包发送给flannel0设备之后,flannel0就会把这个IP包交给创建这个设备的应用程序,也就是Flannel进程。这是一个从内核向用户态的流动方向

反之,如果Flannel进程向flannel0设备发送了一个IP包,那么这个IP包就会出现在宿主机网路栈中,然后根据宿主机的路由表进行下一步处理。这是一个从用户态向内核态的流动方向

所以,当IP包从容器经过cni0出现在宿主机,然后又根据路由表进入flannel0设备后,宿主机上的flanneld进程(Flannel项目在每个宿主机上的主进程)就会收到这个IP包。然后,flanneld看到了这个IP包的目的地址是10.244.2.3,就把它发送给了Node2宿主机

4)flanneld又是如何知道这个IP地址对应的容器,是运行在Node2上的呢?

在由Flannel管理的容器网络里,一台宿主机上的所有容器都属于该宿主机被分配的一个子网。在我们的例子中,Node1的子网是10.244.1.0/24(cni0网桥的地址范围),容器A的IP地址是10.244.1.2;Node2的子网是10.244.2.0/24,容器B的IP地址是10.244.2.3

这些子网与宿主机的对应关系保存在Etcd中:

# etcdctl ls /coreos.com/network/subnets
/coreos.com/network/subnets/10.244.1.0-24
/coreos.com/network/subnets/10.244.2.0-24

所以,flanneld进程在处理由flannel0传入的IP包时,就可以根据目的IP的地址(比如10.244.2.3),匹配到对应的子网(10.244.2.0/24),从Etcd中找到这个子网对应的宿主机的IP地址是172.19.216.115

# etcdctl get /coreos.com/network/subnets/10.244.2.0-24
{
    
    "PublicIP":"172.19.216.115"}

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

5)flanneld在收到容器A发给容器B的IP包之后,就会把这个IP包直接封装在一个UDP包里,然后发送给Node2。这个UDP包的源地址就是flanneld所在的Node1的地址,而目的地址就是容器B所在的宿主机Node2的地址

每台宿主机上的flanneld都监听着一个8285端口,所以flanneld只要把UDP包发往Node2的8285端口即可

6)通过这样一个普通的、宿主机之间的UDP通信,一个UDP包就从Node1到达了Node2。而Node2上监听8285端口的进程也是flanneld,所以这时候,flanneld就可以从这个UDP包里解析出封装在里面的、容器A发来的原IP包

7)Node2上的flanneld会直接把这个IP包发送给它所管理的tun设备,即flannel0设备

这是一个从用户态向内核态的流动方向(Flannel进程向tun设备发送数据包),所以Linux内核网络栈就会通过本机的路由表来寻找这个IP包的下一步流向

Node2路由表如下:

[root@k8s-node2 ~]# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         172.19.223.253  0.0.0.0         UG    100    0        0 eth0
10.244.0.0      0.0.0.0         255.255.0.0     U     0      0        0 flannel0
10.244.2.0      0.0.0.0         255.255.255.0   U     0      0        0 cni0
172.17.0.0      0.0.0.0         255.255.0.0     U     0      0        0 docker0
172.19.208.0    0.0.0.0         255.255.240.0   U     100    0        0 eth0

由于这个IP包的目的地址是10.244.2.3,匹配到10.244.2.0/24对应的这条路由规则,把这个IP包转发给cni0网桥

8)接下来cni0网桥会扮演二层交换机的角色,将数据包发送给正确的端口,进而通过Veth Pair设备进入容器B的Network Namespace里(和主机内容器之间通信流程相同)

而容器B返回给容器A的数据包,则会经过与上述过程完全相反地路由回到容器A中

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

在这里插入图片描述

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

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

  1. 用户态的容器进程发出的IP包经过cni0网桥进入内核态
  2. IP包根据路由表进入flannel0(tun)设备,从而回到用户态的flanneld进程
  3. flanneld进行UDP封包之后重新进入内核态,将UDP包通过宿主机的eth0发出去

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

2)、VXLAN

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

VXLAN的覆盖网络的设计思想是:在现有的三层网络之上,覆盖一层虚拟的、由内核VXLAN模块负责维护的二层网络,使得连接在这个VXLAN二层网络上的主机之间可以像在同一个局域网(LAN)里那样自由通信

而为了能够在二层网络上打通隧道,VXLAN会在宿主机上设置一个特殊的网络设备作为隧道的两端,这个设备叫作VTEP。VTEP的作用其实跟前面的flanneld进程非常相似。只不过,它进行封装和解封装的对象是二层数据帧;而且这个工作的执行流程,全部是在内核里完成的(因为VXLAN本身就是Linux内核里的一个模块)

还是上面这个例子,宿主机Node1(IP地址是172.19.216.114)上的容器A的IP地址是10.244.1.2,要访问宿主机Node2(IP地址是172.19.216.115)上的容器B的IP地址是10.244.2.3

我们来逐步分析VXLAN模式下容器A访问容器B的整个过程

与前面UDP模式的流程类似,当容器A发出请求之后,这个目的地址是10.244.2.3的IP包,通过容器的网关进入cni0网桥,然后被路由到本机flannel.1设备进行处理。也就是说,来到了隧道的入口。为了方便叙述,接下来会把这个IP包称为原始IP包

当Node2启动并加入Flannel网络之后,在Node1以及所有其他节点上,flanneld就会添加一条如下所示的路由规则:

[root@k8s-node1 ~]# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
...
10.244.2.0      10.244.2.0      255.255.255.0   UG    0      0        0 flannel.1

这条规则的意思是:凡是发往10.244.2.0/24网段的IP包,都需要经过flannel.1设备发出,并且,它最后被发往的网关地址是:10.244.2.0,也就是Node2上的VTEP设备(也就是flannel.1设备)的IP地址。为了方便叙述,接下来会把Node1和Node2上的flannel.1设备分别称为源VTEP设备和目的VTEP设备

这些VTEP设备之间,就需要想办法组成一个虚拟的二层网络,即:通过二层数据帧进行通信。所以在我们的例子中,源VTEP设备收到原始IP包后,就要想办法把原始IP包加上一个目的MAC地址,封装成一个二层数据帧,然后发送给目的VTEP设备

这里需要解决的问题就是:目的VTEP设备的MAC地址是什么?此时,根据前面的路由记录,我们已经知道了目的VTEP设备的IP地址。而要根据三层IP地址查询对应的二层MAC地址,这正是ARP表的功能

而这里要用到的ARP记录,也是flanneld进程在Node2节点启动时,自动添加在Node1上的

[root@k8s-node1 ~]# ip neigh show dev flannel.1
10.244.2.0 lladdr 82:9e:ca:29:46:96 PERMANENT
10.244.0.0 lladdr 16:76:50:b4:c3:a5 PERMANENT

IP地址10.244.2.0对应的MAC地址是82:9e:ca:29:46:96

最新版本的Flannel并不依赖L3 MISS事件和ARP学习,而会在每台节点启动时把它的VTEP设备对应的ARP记录,直接下放到其他每台宿主机上

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

可以看到,Linux内核会把目的VTEP设备的MAC地址,填写在图中的Inner Ethernet Header字段,得到一个二层数据帧。上述封包过程只是加一个二层头,不会改变原始IP包的内容。所以图中的Inner IP Header字段,依然是容器B的IP地址,即:10.244.2.3

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

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

为了实现这个搭便车的机制,Linux内核会在内部数据帧前面,加上一个特殊的VXLAN头,用来表示这个乘客实际上是一个VXLAN要使用的数据帧。而在这个VXLAN头里有一个重要的标志叫作VNI,它是VTEP设备识别某个数据帧是不是应该归自己处理的重要标识。而在Flannel中,VNI的默认值是1,这也是为何,宿主机上的VTEP设备都叫作flannel.1的原因,这里的1其实就是VNI的值

然后,Linux内核会把这个数据帧封装进一个UDP包里发出去。跟UDP模式类似,在宿主机看来,它会以为自己的flannel.1设备只是在向另一台宿主机的flannel.1设备,发起一次普通的UDP链接

一个flannel.1设备只知道另一端的flannel.1设备的MAC地址,却不知道对应的宿主机地址是什么。也就是说,这个UDP包应该发给哪台宿主机呢?

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

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

[root@k8s-node1 ~]# bridge fdb show flannel.1 | grep 82:9e:ca:29:46:96
82:9e:ca:29:46:96 dev flannel.1 dst 172.19.216.115 self permanent

在上面这条FDB记录里,指定了这样一条规则:发往目的VTEP设备(MAC地址是82:9e:ca:29:46:96)的二层数据帧,应该通过flannel.1设备,发往IP地址为172.19.216.115的主机。这台主机正是Node2,UDP包要发往的目的地就找到了

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

UDP包是一个四层数据包,所以Linux内核会在它前面加一个IP头(Outer IP Header),组成一个IP包。并且,在这个IP头里,会填上通过FDB查询出来的目的主机的IP地址,即Node2的IP地址172.19.216.115

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

在这里插入图片描述

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

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

而flannel.1设备则会进一步拆包,取出原始IP包。接下来和主机内容器之间通信流程相同,最终,IP包进入到了容器B的Network Namespace里

以上就是基于Flannel VXLAN模式的跨主通信的基本原理了,总结成了一幅原理图,如下所示:

在这里插入图片描述

3)、host-gw

Flannel的host-gw模式是一种纯三层网络方案,它的工作原理如下图所示:

在这里插入图片描述

还是上面这个例子,宿主机Node1(IP地址是172.19.216.114)上的容器A的IP地址是10.244.1.2,要访问宿主机Node2(IP地址是172.19.216.115)上的容器B的IP地址是10.244.2.3

当设置Flannel使用host-gw模式之后,flanneld会在宿主机上创建这样一条规则,以Node1为例:

[root@k8s-node1 ~]# route -n
...
10.244.2.0      172.19.216.115  255.255.255.0   UG    0      0        0 eth0

这条路由规则的含义是:目的IP地址属于10.244.2.0/24网段的IP包。应该经过本机的eth0设备发出去;并且,它的下一跳(next-hop)是172.19.216.115

所谓下一跳地址就是:如果IP包从主机A发到主机B,需要经过路由设备X的中转。那么X的IP地址就应该配置为主机A的下一跳地址

一旦配置了下一跳地址,那么接下来,当IP包从网络层进入链路层封装成帧的时候,eth0设备就会使用下一跳地址对应的MAC地址,作为该数据帧的目的MAC地址。显然,这个MAC地址正是Node2的MAC地址

这样,这个数据帧就会从Node1通过宿主机的二层网络顺利到达Node2上

而Node2的内核网络栈从二层数据帧里拿到IP包后,会看到这个IP包的目的IP地址是10.244.2.3,即容器B的IP地址。这时候,根据Node2上的路由表,目的地址会匹配到10.244.2.0对应的路由规则,从而进入cni0网桥,进而进入到容器B中

host-gw模式的工作原理其实就是将每个Flannel子网的下一跳设置成了该子网对应的宿主机的IP地址。这个主机会充当这条容器通信路径里的网关,这也正是host-gw的含义

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

而在这种模式下,容器通信的过程就免除了额外的封包和解包带来的性能损耗

host-gw模式能够正常工作的核心,就在于IP包在封装成帧发出去的时候,会使用路由表的下一跳设置目的MAC地址。这样,它就会经过二层网络到达目的宿主机。所以说,Flannel host-gw模式必须要求集群宿主机之间是二层连通的

宿主机之间二层不连通的情况也是广泛存在的。比如,宿主机分布在了不同的子网(VLAN)里。但是,在一个Kubernetes集群里,宿主机之间必须可以通过IP地址进行通信,也就是说至少是三层可达的。三层可达也可以通过为几个子网设置三层转发来实现

参考

《Kubernetes网络权威指南:基础、原理与实践》

深入解析容器跨主机网络

解读Kubernetes三层网络方案

猜你喜欢

转载自blog.csdn.net/qq_40378034/article/details/123964705