​Flannel 介绍

Flannel 是一个非常简单的 overlay 网络(VXLAN),是 Kubernetes 网络 CNI 的解决方案之一。Flannel 在每台主机上运行一个简单的轻量级 agentflanneld​来监听集群中节点的变更,并对地址空间进行预配置。Flannel 还会在每台主机上安装 vtepflannel.1(VXLAN tunnel endpoints),与其他主机通过 VXLAN 隧道相连。

flanneld 监听在8472端口,通过 UDP 与其他节点的 vtep 进行数据传输。到达 vtep 的二层包会被原封不动地通过 UDP 的方式发送到对端的 vtep,然后拆出二层包进行处理。简单说就是用四层的 UDP 传输二层的数据帧。

vxlan-tunnel


(资料图片)

在 Kubernetes 发行版K3S[1]中将 Flannel 作为默认的 CNI 实现。K3S 集成了 flannel,在启动后 flannel 以 go routine 的方式运行。

环境搭建

Kubernetes 集群使用 k3s 发行版,但在安装集群的时候,禁用 k3s 集成的 flannel,使用独立安装的 flannel 进行验证。

安装 CNI 的 plugin,需要在所有的 node 节点上执行下面的命令,下载 CNI 的官方 bin。

sudo mkdir -p /opt/cni/bincurl -sSL https://github.com/containernetworking/plugins/releases/download/v1.1.1/cni-plugins-linux-amd64-v1.1.1.tgz | sudo tar -zxf - -C /opt/cni/bin

安装 k3s 的控制平面。

export INSTALL_K3S_VERSION=v1.23.8+k3s2curl -sfL https://get.k3s.io | sh -s - --disable traefik --flannel-backend=none --write-kubeconfig-mode 644 --write-kubeconfig ~/.kube/config

安装 Flannel。这里注意,Flannel 默认的 Pod CIRD 是10.244.0.0/16​,我们将其修改为 k3s 默认的10.42.0.0/16。

curl -s https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml | sed "s|10.244.0.0/16|10.42.0.0/16|g" | kubectl apply -f -

添加另一个节点到集群。

export INSTALL_K3S_VERSION=v1.23.8+k3s2export MASTER_IP=export NODE_TOKEN=curl -sfL https://get.k3s.io | K3S_URL=https://${MASTER_IP}:6443 K3S_TOKEN=${NODE_TOKEN} sh -

查看节点状态。

kubectl get nodeNAME          STATUS   ROLES                  AGE   VERSIONubuntu-dev3   Ready                     13m   v1.23.8+k3s2ubuntu-dev2   Ready    control-plane,master   17m   v1.23.8+k3s2

运行两个 pod:curl​和httpbin,为了探寻

NODE1=ubuntu-dev2NODE2=ubuntu-dev3kubectl apply -n default -f - <网络配置

接下来,一起看下 CNI 插件如何配置 pod 网络。

初始化

Flannel 是通过Daemonset​的方式部署的,每台节点上都会运行一个 flannel 的 pod。通过挂载本地磁盘的方式,在 Pod 启动时会通过初始化容器将二进制文件和 CNI 的配置复制到本地磁盘中,分别位于/opt/cni/bin/flannel​和/etc/cni/net.d/10-flannel.conflist。

通过查看kube-flannel.yml[2]中的ConfigMap​,可以找到 CNI 配置,flannel 默认委托(见flannel-cni 源码 `flannel_linux.go#L78`[3])给bridge 插件[4]进行网络配置,网络名称为cbr0;IP 地址的管理,默认委托(见flannel-cni 源码 `flannel_linux.go#L40`[5])host-local 插件[6]完成。

#cni-conf.json 复制到 /etc/cni/net.d/10-flannel.conflist{  "name": "cbr0",  "cniVersion": "0.3.1",  "plugins": [    {      "type": "flannel",      "delegate": {        "hairpinMode": true,        "isDefaultGateway": true      }    },    {      "type": "portmap",      "capabilities": {        "portMappings": true      }    }  ]}

还有 Flannel 的网络配置,配置中有我们设置的 Pod CIDR10.42.0.0/16​以及后端(backend)的类型vxlan​。这也是 flannel 默认的类型,此外还有多种后端类型[7]可选,如host-gw、wireguard、udp、Alloc、IPIP、IPSec。

#net-conf.json 挂载到 pod 的 /etc/kube-flannel/net-conf.json{  "Network": "10.42.0.0/16",  "Backend": {    "Type": "vxlan"  }}

Flannel Pod 运行启动flanneld​进程,指定了参数--ip-masq​和--kube-subnet-mgr​,后者开启了kube subnet manager模式。

运行

集群初始化时使用了默认的 Pod CIDR10.42.0.0/16​,当有节点加入集群,集群会从该网段上为节点分配属于节点的 Pod CIDR10.42.X.1/24。

flannel 在kube subnet manager模式下,连接到 apiserver 监听节点更新的事件,从节点信息中获取节点的 Pod CIDR。

kubectl get no ubuntu-dev2 -o jsnotallow={.spec} | jq{  "podCIDR": "10.42.0.0/24",  "podCIDRs": [    "10.42.0.0/24"  ],  "providerID": "k3s://ubuntu-dev2"}

然后在主机上写子网配置文件,下面展示的是其中一个节点的子网配置文件的内容。另一个节点的内容差异在FLANNEL_SUBNET=10.42.1.1/24,使用的是对应节点的 Pod CIDR。

#node 192.168.1.12cat /run/flannel/subnet.envFLANNEL_NETWORK=10.42.0.0/16FLANNEL_SUBNET=10.42.0.1/24FLANNEL_MTU=1450FLANNEL_IPMASQ=true
CNI 插件执行

CNI 插件的执行是由容器运行时触发的,具体细节可以看上一篇《源码解析:从 kubelet、容器运行时看 CNI 的使用》。

Flannel Plugin Flow

flannel 插件

flannel​CNI 插件(/opt/cni/bin/flannel​)执行的时候,接收传入的cni-conf.json​,读取上面初始化好的subnet.env​的配置,输出结果,委托给bridge进行下一步。

cat /var/lib/cni/flannel/e4239ab2706ed9191543a5c7f1ef06fc1f0a56346b0c3f2c742d52607ea271f0 | jq{  "cniVersion": "0.3.1",  "hairpinMode": true,  "ipMasq": false,  "ipam": {    "ranges": [      [        {          "subnet": "10.42.0.0/24"        }      ]    ],    "routes": [      {        "dst": "10.42.0.0/16"      }    ],    "type": "host-local"  },  "isDefaultGateway": true,  "isGateway": true,  "mtu": 1450,  "name": "cbr0",  "type": "bridge"}
bridge 插件

bridge使用上面的输出连同参数一起作为输入,根据配置完成如下操作:

创建网桥cni0(节点的根网络命名空间)创建容器网络接口eth0( pod 网络命名空间)创建主机上的虚拟网络接口vethX(节点的根网络命名空间)将vethX​连接到网桥cni0委托 ipam 插件分配 IP 地址、DNS、路由将 IP 地址绑定到 pod 网络命名空间的接口eth0上检查网桥状态设置路由设置 DNS

最后输出如下的结果:

cat /var/li/cni/results/cbr0-a34bb3dc268e99e6e1ef83c732f5619ca89924b646766d1ef352de90dbd1c750-eth0 | jq .result{  "cniVersion": "0.3.1",  "dns": {},  "interfaces": [    {      "mac": "6a:0f:94:28:9b:e7",      "name": "cni0"    },    {      "mac": "ca:b4:a9:83:0f:d4",      "name": "veth38b50fb4"    },    {      "mac": "0a:01:c5:6f:57:67",      "name": "eth0",      "sandbox": "/var/run/netns/cni-44bb41bd-7c41-4860-3c55-4323bc279628"    }  ],  "ips": [    {      "address": "10.42.0.5/24",      "gateway": "10.42.0.1",      "interface": 2,      "version": "4"    }  ],  "routes": [    {      "dst": "10.42.0.0/16"    },    {      "dst": "0.0.0.0/0",      "gw": "10.42.0.1"    }  ]}

port-mapping 插件

该插件会将来自主机上一个或多个端口的流量转发到容器。

Debug

让我们在第一个节点上,使用tcpdump​对接口cni0进行抓包。

tcpdump -i cni0 port 80 -vvv

从 podcurl​中使用 podhttpbin​的 IP 地址10.42.1.2发送请求:

kubectl exec curl -n default -- curl -s 10.42.1.2/get

cni0

从在cni0上的抓包结果来看,第三层的 IP 地址均为 Pod 的 IP 地址,看起来就像是两个 pod 都在同一个网段。

tcpdump-on-cni0

host eth0

文章开头提到 flanneld 监听 udp 8472 端口。

netstat -tupln | grep 8472udp        0      0 0.0.0.0:8472            0.0.0.0:*                           -

我们直接在以太网接口上抓取 UDP 的包:

tcpdump -i eth0 port 8472 -vvv

再次发送请求,可以看到抓取到 UDP 数据包,传输的负载是二层的封包。

tcpdump-on-host-eth0

Overlay 网络下的跨节点通信

在系列的第一篇中,我们研究 pod 间的通信时提到不同 CNI 插件的处理方式不同,这次我们探索了 flannel 插件的工作原理。希望通过下面的图可以对 overlay 网络处理跨节点的网络通信有个比较直观的认识。

当发送到10.42.1.2​流量到达节点 A 的网桥cni0​,由于目标 IP 并不属于当前阶段的网段。根据系统的路由规则,进入到接口flannel.1​,也就是 VXLAN 的 vtep。这里的路由规则也由flanneld来维护,当节点上线或者下线时,都会更新路由规则。

#192.168.1.12Destination     Gateway         Genmask         Flags Metric Ref    Use Ifacedefault         _gateway        0.0.0.0         UG    0      0        0 eth010.42.0.0       0.0.0.0         255.255.255.0   U     0      0        0 cni010.42.1.0       10.42.1.0       255.255.255.0   UG    0      0        0 flannel.1192.168.1.0     0.0.0.0         255.255.255.0   U     0      0        0 eth0#192.168.1.13Destination     Gateway         Genmask         Flags Metric Ref    Use Ifacedefault         _gateway        0.0.0.0         UG    0      0        0 eth010.42.0.0       10.42.0.0       255.255.255.0   UG    0      0        0 flannel.110.42.1.0       0.0.0.0         255.255.255.0   U     0      0        0 cni0192.168.1.0     0.0.0.0         255.255.255.0   U     0      0        0 eth0

flannel.1​将原始的以太封包使用 UDP 协议重新封装,将其发送到目标地址10.42.1.0​(目标的 MAC 地址通过 ARP 获取)。对端的 vtep 也就是flannel.1​的 UDP 端口 8472 收到消息,解帧出以太封包,然后对以太封包进行路由处理,发送到接口cni0,最终到达目标 pod 中。

响应的数据传输与请求的处理也是类似,只是源地址和目的地址调换。

参考资料

[1]K3S:https://k3s.io/

[2]kube-flannel.yml:https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml

[3]flannel-cni 源码flannel_linux.go#L78​:https://github.com/flannel-io/cni-plugin/blob/v1.1.0/flannel_linux.go#L78

[4]bridge 插件:https://www.cni.dev/plugins/current/main/bridge/

[5]flannel-cni 源码flannel_linux.go#L40​:https://github.com/flannel-io/cni-plugin/blob/v1.1.0/flannel_linux.go#L40

[6]host-local 插件:https://www.cni.dev/plugins/current/ipam/host-local/

[7]多种后端类型:https://github.com/flannel-io/flannel/blob/master/Documentation/backends.md

标签: Flannel