Flannel 是 CoreOS(Etcd 的公司)推出的 Overlay 类型的容器网络插件,为解决容器跨主机通信。支持 UDP、VXLAN、host-gw 三种后端实现。

kubernetes配置了cni网络插件后,其容器网络创建流程:

  • kubelet先创建pause容器生成对应的network namespace
  • 调用网络driver,因为配置的是CNI,会调用CNI相关代码,识别CNI的配置目录为/etc/cni/net.d
  • CNI driver根据配置调用具体的CNI插件,二进制调用,可执行文件目录为/opt/cni/bin
  • CNI插件给pause容器配置正确的网络,pod中其他的容器都是用pause的网络

UDP方式(低效)

实现

在 Flannel 的配置文件中指定 Backend type 为 UDP,使用 UDP 模式

  1. $ kubectl edit cm kube-flannel-cfg -n kube-system
  2. apiVersion: v1
  3. data:
  4. cni-conf.json: |
  5. {
  6. "cniVersion": "0.2.0",
  7. "name": "cbr0",
  8. "plugins": [
  9. {
  10. "type": "flannel",
  11. "delegate": {
  12. "hairpinMode": true,
  13. "isDefaultGateway": true
  14. }
  15. },
  16. {
  17. "type": "portmap",
  18. "capabilities": {
  19. "portMappings": true
  20. }
  21. }
  22. ]
  23. }
  24. net-conf.json: |
  25. {
  26. {
  27. "Network": "10.244.0.0/16",
  28. "Backend": {
  29. "Type": "udp" # 修改后端类型为 UDP
  30. }
  31. }
  32. kind: ConfigMap
  33. ......


采用 UDP 模式时后端默认为端口为 8285,即 Flanneld 的监听端口。当采用 UDP 模式时,Flanneld 进程在启动时会通过打开 /dev/net/tun 的方式生成一个 TUN 设备,TUN 设备可以简单理解为 Linux 当中提供的一种内核网络与用户空间通信的一种机制,即应用可以通过直接读写 TUN 设备的方式收发 RAW IP 包。所以要将宿主机的 /dev/net/tun 文件挂载到容器中

$ kubectl edit ds kube-flannel-ds-amd64 -n kube-system
......
  volumeMounts:
    - mountPath: /run/flannel
    name: run
    - mountPath: /etc/kube-flannel/
    name: flannel-cfg
    - mountPath: /dev/net
    name: tun
......
volumes:
- hostPath:
    path: /run/flannel
    type: ""
  name: run
- hostPath:
    path: /etc/cni/net.d
    type: ""
  name: cni
- hostPath:
    path: /dev/net  # 挂载宿主机的 /dev/net/tun 文件
    type: ""
  name: tun
......


Flanneld 的 Pod 自动重建完成后,随机查看某个 Pod 的日志

$ kubectl logs -f kube-flannel-ds-amd64-5bk4dmd64 -n kube-system
I1128 08:26:49.663566       1 main.go:527] Using interface with name eth0 and address 10.151.30.11
I1128 08:26:49.663838       1 main.go:544] Defaulting external address to interface address (10.151.30.11)
I1128 08:26:49.857634       1 kube.go:126] Waiting 10m0s for node controller to sync
I1128 08:26:49.857805       1 kube.go:309] Starting kube subnet manager
I1128 08:26:50.858137       1 kube.go:133] Node controller sync successful
I1128 08:26:50.858324       1 main.go:244] Created subnet manager: Kubernetes Subnet Manager - ydzs-master
I1128 08:26:50.858357       1 main.go:247] Installing signal handlers
I1128 08:26:50.858933       1 main.go:386] Found network config - Backend type: udp
I1128 08:26:51.089114       1 main.go:317] Wrote subnet file to /run/flannel/subnet.env
I1128 08:26:51.089177       1 main.go:321] Running backend.

信息:Found network config - Backend type: udp 证明现在已经变成了 UDP 模式。

Flanneld 进程启动后节点当中多了个 flannel0 的网络设备

$ ip -d link show flannel0
210: flannel0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1472 qdisc pfifo_fast state UNKNOWN mode DEFAULT group default qlen 500
    link/none  promiscuity 0
    tun addrgenmode random numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535

$ netstat -ulnp | grep flanneld
udp        0      0 10.151.30.11:8285       0.0.0.0:*      24844/flanneld

原理

需求
在节点 k8s-node1 和节点 k8s-node2 上分别有一个 Pod,现在让 pod-a(10.244.1.236)向 pod-b(10.244.2.123)发送一个请求报文(ping),分析报文是如何从 pod-a 到达 pod-b

分析过程

$ kubectl get pods -o wide
NAME             READY   STATUS    RESTARTS   AGE     IP             NODE         NOMINATED NODE   READINESS GATES
pod-a            1/1     Running   0          73s     10.244.1.236   k8s-node1              
pod-b            1/1     Running   0          38s     10.244.2.123   k8s-node2
    <br />1、在 pod-a 当中发出 ICMP 请求报文,其源地址就是 10.244.1.236,目标地址是 10.244.2.123,此时通过 pod-a 内的路由表匹配到应该将该 IP 包发送到 k8s-node1 节点上网关 10.244.1.1(cni0网桥)。
$ kubectl exec pod-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

# 在 k8s-node1 节点上可以查看到 pod-a 中的网关 10.244.1.1 就是节点上的 cni0 网桥
[root@k8s-node1  ~]# ifconfig -a 
cni0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1450
inet 10.244.1.1  netmask 255.255.255.0  broadcast 0.0.0.0
inet6 fe80::64c2:65ff:fe15:3669  prefixlen 64  scopeid 0x20
ether f6:f9:99:71:81:a2  txqueuelen 1000  (Ethernet)
RX packets 33385207  bytes 24883992070 (23.1 GiB)
RX errors 0  dropped 0  overruns 0  frame 0
TX packets 31653673  bytes 19703556786 (18.3 GiB)
TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
......


2、此时 IP 包的下一个目的地取决于宿主机上面的路由规则,Flanneld 进程已经在宿主机上面创建了一系列的路由规则

[root@k8s-node1  ~]# ip route 
default via 10.151.30.11 dev eth0  proto static  metric 100
10.151.30.0/24 dev eth0  proto kernel  scope link  src 10.151.30.22  metric 100
10.244.0.0/16 dev flannel0
10.244.1.0/24 dev cni0  proto kernel  scope link  src 10.244.1.1
172.17.0.0/16 dev docker0  proto kernel  scope link  src 172.17.0.1

此时到达 cni0 的 IP 包目标地址 10.244.2.123 匹配不到本机的 cni0 网桥对应的 10.244.1.0/24 网段,只能匹配到第三条 10.244.0.0/16 对应的这条路由规则,此刻内核将 RAW IP 包发送给 flannel0 设备。

flannel0 设备它是一个 TUN 设备(Tunnel 设备)。在 Linux 中,TUN 设备是一种工作在三层(Network Layer)的虚拟网络设备,TUN 设备的功能非常简单,即:在操作系统内核和用户应用程序之间传递 IP 包。

3、由于 flannel0 是一个 TUN 设备,发送给 flannel0 接口的 RAW IP 包将被 Flanneld 进程接收到,然后在原有的基础上进行 UDP 封包,然后发送到 k8s-node2 节点上的 Flanneld 进行解包。

UDP 封包的形式为:10.151.30.22:src port -> 10.151.30.23:8285。

#UDP 封包发送到目标 IP 10.244.2.123 这个容器所在的节点,是如何知道这个节点的呢?
在 Flannel 管理的容器网络中,一台宿主机上的所有容器,都属于该宿主机被分配的一个子网,比如 k8s-node1 节点的子网是 10.244.1.0/24(10.244.1.1-10.244.1.254),pod-a 的 IP 地址是 10.244.1.236;k8s-node2 节点的子网是 10.244.2.0/24(10.244.2.1-10.244.2.254),pod-b 的 IP 地址是 10.244.2.123,这些子网信息是当 Flanneld 进程在启动时通过 api-server 保存到 etcd 当中,所以在发送报文时可以通过目的地址 10.244.2.123 匹配到对应的子网是 10.244.2.0/24,再查询 etcd 得到这个子网对应的宿主机的 IP 地址 10.151.30.23,也就是 k8s-node2 节点。

4、k8s-node2 节点收到 UDP 报文过后经过 Linux 内核通过 UDP 端口 8285 将包交给节点上的 Flanneld 进程。

5、然后 k8s-node2 节点上的 Flanneld 进程将接收到的 UDP 包解包后得到 RAW IP 包:10.244.1.236 -> 10.244.2.123。

6、解包后的 RAW IP 包匹配到 k8s-node2 节点上的路由规则(10.244.2.0/24),内核将 RAW IP 包发送给 cni0 设备

[root@k8s-node2  ~]# ip route 
default via 10.151.30.11 dev eth0  proto static  metric 100
10.151.30.0/24 dev eth0  proto kernel  scope link  src 10.151.30.23  metric 100
10.244.0.0/16 dev flannel0
10.244.2.0/24 dev cni0  proto kernel  scope link  src 10.244.2.1
172.17.0.0/16 dev docker0  proto kernel  scope link  src 172.17.0.1


7、cni0 将 IP 包转发给连接在 cni0 网桥上的 pod-b,完成整个通信过程:

$ kubectl exec pod-a ping 10.244.2.123
PING 10.244.2.123 (10.244.2.123): 56 data bytes
64 bytes from 10.244.2.123: seq=0 ttl=62 time=1.452 ms
64 bytes from 10.244.2.123: seq=1 ttl=62 time=1.160 ms
64 bytes from 10.244.2.123: seq=2 ttl=62 time=0.853 ms


以上是基于 Flannel UDP 模式的跨主通信的基本流程,Flanneld 主要有两方面的功能:

  • UDP 封包解包
  • 节点上的路由表的动态更新

VXLAN 方式(普遍应用)

UDP 方式的数据包是通过 tun 设备从内核态复制到用户态的应用中的,然后再通过用户态复制到内核态,仅一次网络传输就进行了两次用户态和内核态的切换,显然效率是不高的,要提高效率最简单的方式就是把封包解包这些事情都交给内核去干,事实上 Linux 内核本身也提供了比较成熟的网络封包解包(隧道传输)实现方案 VXLAN,Flanneld 也实现了基于 VXLAN 的方案,该方案是最普遍实用的。

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

实现

Flannel 默认使用vxlan技术作为Backend,查看方式如下:

$ kubectl -n kube-system exec  kube-flannel-ds-amd64-cb7hs cat /etc/kube-flannel/net-conf.json
{
  "Network": "10.244.0.0/16",
  "Backend": {
    "Type": "vxlan"
  }
}

若要修改 Flanneld 的 Backend 类型修改为 vxlan,操作如下:

$ kubectl edit cm kube-flannel-cfg -n kube-system
apiVersion: v1
data:
  cni-conf.json: |
    {
      "cniVersion": "0.2.0",
      "name": "cbr0",
      "plugins": [
        {
          "type": "flannel",
          "delegate": {
            "hairpinMode": true,
            "isDefaultGateway": true
          }
        },
        {
          "type": "portmap",
          "capabilities": {
            "portMappings": true
          }
        }
      ]
    }
  net-conf.json: |
    {
        {
      "Network": "10.244.0.0/16",
      "Backend": {
        "Type": "vxlan"  # 修改后端类型为 vxlan
      }
    }
kind: ConfigMap
......

# 删除使其自动重新创建加载配置
$ kubectl delete pod -n kube-system -l app=flannel

# 日志查看配置是否成功
$ kubectl logs -f kube-flannel-ds-amd64-pfb8h -n kube-system
I1129 03:33:56.588549       1 main.go:527] Using interface with name eth0 and address 10.151.30.23
I1129 03:33:56.588893       1 main.go:544] Defaulting external address to interface address (10.151.30.23)
I1129 03:33:56.698562       1 kube.go:126] Waiting 10m0s for node controller to sync
I1129 03:33:56.698726       1 kube.go:309] Starting kube subnet manager
I1129 03:33:57.698910       1 kube.go:133] Node controller sync successful
I1129 03:33:57.698980       1 main.go:244] Created subnet manager: Kubernetes Subnet Manager - ydzs-node2
I1129 03:33:57.699000       1 main.go:247] Installing signal handlers
I1129 03:33:57.699375       1 main.go:386] Found network config - Backend type: vxlan


Flanneld 在启动时会通过 Netlink 机制与 Linux 内核通信,建立一个 VTEP(Virtual Tunnel Access End Point)设备 flannel.1(命名规则为flannel.[VNI],VNI 默认为1),类似于交换机当中的一个网口。

# 查看,VTEP 的 local IP 为 10.151.30.22,destination port 为 8472
$ ip -d link show flannel.1
6: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN mode DEFAULT
link/ether 72:f7:e9:40:97:1e brd ff:ff:ff:ff:ff:ff promiscuity 0
vxlan id 1 local 10.151.30.22 dev eth0 srcport 0 0 dstport 8472 nolearning ageing 300 addrgenmode eui64

# 在节点上查看进程监听情况
$ netstat -ulnp | grep 8472
udp   0   0 0.0.0.0:8472      0.0.0.0:*             -

说明:
VXLAN 模式下查看 Flannel 监听的端口,最后一栏显示的不是进程的 ID 和名称,而是一个破折号“-”,这说明 UDP 的8472端口不是由用户态的进程在监听的,也证实了VXLAN模块工作在内核态模式下。

原理

在 UDP 模式下由 Flanneld 进程进行网络包的封包和解包的工作,而在 VXLAN 模式下解封包的事情交由内核处理。

Flanneld 后端的具体工具流程:
当 Flanneld 启动时将创建 VTEP 设备 flannel.1,并将 VTEP 设备的相关信息上报到 etcd 当中,而当在 Flannel 网络中有新的节点发现时,各个节点上的 Flanneld 进程将依次执行以下流程:

1、在节点当中创建一条该节点所属网段的路由表,主要是能让 Pod 当中的流量路由到 flannel.1 接口。

#查看节点当前已有的四条 flannel.1 接口的路由
$ route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         10.151.30.11    0.0.0.0         UG    100    0        0 eth0
10.151.30.0     0.0.0.0         255.255.255.0   U     100    0        0 eth0
10.244.0.0      10.244.0.0      255.255.255.0   UG    0      0        0 flannel.1
10.244.1.0      0.0.0.0         255.255.255.0   U     0      0        0 cni0
10.244.2.0      10.244.2.0      255.255.255.0   UG    0      0        0 flannel.1
10.244.3.0      10.244.3.0      255.255.255.0   UG    0      0        0 flannel.1
10.244.4.0      10.244.4.0      255.255.255.0   UG    0      0        0 flannel.1
172.17.0.0      0.0.0.0         255.255.0.0     U     0      0        0 docker0


比如 10.244.2.0 这条路由规则,意思是发往 10.244.2.0/24 网段的 IP 包,都需要经过 flannel.1 设备发出,而且最后被发送到的网关地址是 10.244.2.0。其就是 k8s-node2 节点上的 VTEP 设备(也就是 flannel.1)的 IP 地址:

[root@k8s-node2  ~]# ifconfig 
flannel.1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1450
inet 10.244.2.0  netmask 255.255.255.255  broadcast 0.0.0.0
inet6 fe80::2467:52ff:fe6b:1bf9  prefixlen 64  scopeid 0x20
ether 26:67:52:6b:1b:f9  txqueuelen 0  (Ethernet)
RX packets 42050890  bytes 27691009839 (25.7 GiB)
RX errors 0  dropped 0  overruns 0  frame 0
TX packets 36287976  bytes 46894346975 (43.6 GiB)
......


2、上面知道了目的 VTEP 设备的 IP 地址了,这个时候就需要知道目的 MAC 地址,才能把数据包发送过去,此时 Flanneld 进程就会在节点当中维护所有节点的 IP 以及 VTEP 设备的静态 ARP 缓存。

# 通过 arp -n 命令查看到当前节点当中已经缓存了另外四个节点以及 VTEP 的 ARP 信息。
$ arp -n
Address                  HWtype  HWaddress           Flags Mask            Iface
10.244.3.0               ether   32:f1:e0:a9:97:ab   CM                    flannel.1
10.244.2.0               ether   26:67:52:6b:1b:f9   CM                    flannel.1
10.244.0.0               ether   0a:24:5e:40:ff:da   CM                    flannel.1
10.244.4.0               ether   4a:09:1f:42:ed:c1   CM                    flannel.1
......


IP 地址 10.244.2.0 对应的 MAC 地址是 26:67:52:6b:1b:f9,这样就知道了目的 VTEP 设备的 MAC 地址。

3、这个时候 Flanneld 进程还会在节点当中添加一条该节点的转发表,MAC 为 VTEP 设备即 flannel.1 的 MAC 地址,IP 为 VTEP 对应的对外 IP(可通过 Flanneld 的启动参数 —iface=eth0 指定,若不指定则按默认网关查找网络接口对应的 IP),可以看到已经有四条转发表。

# 通过 bridge 命令查看节点上的 VXLAN 转发表(FDB entry),
$ bridge fdb show dev flannel.1
32:f1:e0:a9:97:ab dst 10.151.30.57 self permanent
26:67:52:6b:1b:f9 dst 10.151.30.23 self permanent
4a:09:1f:42:ed:c1 dst 10.151.30.59 self permanent
0a:24:5e:40:ff:da dst 10.151.30.11 self permanent


这样就找到了上面目的 VTEP 设备的 MAC 地址对应的 IP 地址为 10.151.30.23 的主机,即 k8s-node2 节点,所以就找到了要发往的目的地址。

此时容器跨节点网络通信实现的完整流程为:

  • 和 UDP 模式一样,pod-a(10.244.1.236)当中的 IP 包通过 pod-a 内的路由表被发送到 cni0
  • 到达 cni0 当中的 IP 包通过匹配节点 k8s-node1 当中的路由表发现通往 10.244.2.13 的 IP 包应该交给 flannel.1 接口
  • flannel.1 作为一个 VTEP 设备,收到报文后将按照 VTEP 的配置进行封包,通过 k8s-node1 节点上的 arp 和转发表得知 10.244.2.123 属于节点 k8s-node2,并且会将 k8s-node2 节点对应的 VTEP 设备的 MAC 地址,根据 flannel.1 设备创建时的设置的参数(VNI、local IP、Port)进行 VXLAN 封包
  • 通过节点 k8s-node2 跟 k8s-node1 之间的网络连接,VXLAN 包到达 k8s-node2 的 eth0 接口
    通过端口 8472,VXLAN 包被转发给 VTEP 设备 flannel.1 进行解包
  • 解封装后的 IP 包匹配节点 k8s-node2 当中的路由表(10.244.2.0),内核将 IP 包转发给cni0
  • cni0将 IP 包转发给连接在 cni0 上的 pod-b

image.png

host-gw(高效)

vxlan模式适用于三层可达的网络环境,对集群的网络要求很宽松,其由于会通过VTEP设备进行额外封包和解包,因此给性能带来了额外的开销。

为什么三层可达的网络不直接利用网关转发流量?
内核当中的路由规则,网关必须在跟主机当中至少一个 IP 处于同一网段。由于k8s集群内部各节点均需要实现Pod互通,就意味着host-gw模式需要整个集群节点都在同一二层网络内。

网络插件的目的其实就是将本机的cni0网桥的流量送到目的主机的cni0网桥。实际上有很多集群是部署在同一二层网络环境下的,可以直接利用二层的主机当作流量转发的网关。这样的话,可以不用进行封包解包,直接通过路由表去转发流量。

实现

# 修改配置
$ kubectl edit cm kube-flannel-cfg -n kube-system
...
net-conf.json: |
    {
      "Network": "10.244.0.0/16",
      "Backend": {
        "Type": "host-gw"
      }
    }
kind: ConfigMap
...

# 重建 Pod
$ kubectl delete pod -n kube-system -l app=flannel

# 等待Pod新启动后,查看日志,出现 Backend type: host-gw 字样
$  kubectl -n kube-system logs -f kube-flannel-ds-amd64-4hjdw
I0704 01:18:11.916374       1 kube.go:126] Waiting 10m0s for node controller to sync
I0704 01:18:11.916579       1 kube.go:309] Starting kube subnet manager
I0704 01:18:12.917339       1 kube.go:133] Node controller sync successful
I0704 01:18:12.917848       1 main.go:247] Installing signal handlers
I0704 01:18:12.918569       1 main.go:386] Found network config - Backend type: host-gw
I0704 01:18:13.017841       1 main.go:317] Wrote subnet file to /run/flannel/subnet.env

原理

采用 host-gw 模式后 Flanneld 的唯一作用就是负责主机上路由表的动态更新,其实就是将每个 Flannel 子网(Flannel Subnet,比如:10.244.1.0/24)的“下一跳”,设置成了该子网对应的宿主机的 IP 地址。

Flannel 子网和主机的信息,都是保存在 etcd 当中的。Fanneld 只需要 WACTH 这些数据的变化,然后实时更新路由表即可。主要流程如下所示:

1、同 UDP、VXLAN 模式一致,通过容器A 的路由表 IP 包到达cni0

2、到达 cni0 的 IP 包匹配到 k8s-node1 当中的路由规则(10.244.2.0),并且网关为 10.151.30.23,即节点 k8s-node2,所以内核将 IP 包发送给节点 k8s-node2(10.151.30.23)

$ route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         10.151.30.11    0.0.0.0         UG    100    0        0 eth0
10.151.30.0     0.0.0.0         255.255.255.0   U     100    0        0 eth0
10.244.0.0      10.151.30.11    255.255.255.0   UG    0      0        0 eth0
10.244.1.0      0.0.0.0         255.255.255.0   U     0      0        0 cni0
10.244.2.0      10.151.30.23    255.255.255.0   UG    0      0        0 eth0
10.244.3.0      10.151.30.57    255.255.255.0   UG    0      0        0 eth0
10.244.4.0      10.151.30.59    255.255.255.0   UG    0      0        0 eth0
172.17.0.0      0.0.0.0         255.255.0.0     U     0      0        0 docker0

3、IP 包通过物理网络到达节点的 k8s-node2 的 eth0 设备

4、到达 k8s-node2 节点 eth0 的 IP 包匹配到节点当中的路由表(10.244.2.0/24),IP 包被转发给 cni0 设备

5、cni0 将 IP 包转发给连接在 cni0 上的 pod-b
image.png

这样就完成了整个跨主机通信流程,容器通信的过程还免除了额外的封包和解包带来的性能损耗,所以理论上性能肯定要更好。

该模式是通过节点上的路由表来实现各个节点之间的跨节点网络通信,那么就得保证两个节点是可以直接路由过去的。按照内核当中的路由规则,网关必须在跟主机当中至少一个 IP 处于同一网段,故造成的结果就是采用 host-gw 这种模式的时候,集群中所有的节点必须处于同一个网络当中,当集群规模比较大时,对节点进行网段划分会存在一定的局限性,同时随着集群当中节点规模的增大,Flanneld 需要维护主机上成千上万条路由表的动态更新也是一个不小的压力。