從Flannel 學習Kubernetes overlay 網絡

2022.12.19

從Flannel 學習Kubernetes overlay 網絡

Flannel 是通過Daemonset 的方式部署的,每台節點上都會運行一個flannel 的pod。通過掛載本地磁盤的方式,在Pod 啟動時會通過初始化容器將二進製文件和CNI 的配置複製到本地磁盤中,分別位於/opt/cni/bin/flannel 和/etc/cni/net.d/10-flannel.conflist。

​Flannel 介紹

Flannel 是一個非常簡單的overlay 網絡(VXLAN),是Kubernetes 網絡CNI 的解決方案之一。Flannel 在每台主機上運行一個簡單的輕量級agent flanneld​ 來監聽集群中節點的變更,並對地址空間進行預配置。Flannel 還會在每台主機上安裝vtep flannel.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/bin
curl -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
  • 1.
  • 2.

安裝k3s 的控制平面。

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

安裝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 -
  • 1.

添加另一個節點到集群。

export INSTALL_K3S_VERSION=v1.23.8+k3s2
export MASTER_IP=<MASTER_IP>
export NODE_TOKEN=<TOKEN>
curl -sfL https://get.k3s.io | K3S_URL=https://${MASTER_IP}:6443 K3S_TOKEN=${NODE_TOKEN} sh -
  • 1.
  • 2.
  • 3.
  • 4.

查看節點狀態。

kubectl get node
NAME          STATUS   ROLES                  AGE   VERSION
ubuntu-dev3   Ready    <none>                 13m   v1.23.8+k3s2
ubuntu-dev2   Ready    control-plane,master   17m   v1.23.8+k3s2
  • 1.
  • 2.
  • 3.
  • 4.

運行兩個pod:curl​ 和 httpbin,為了探尋

NODE1=ubuntu-dev2
NODE2=ubuntu-dev3
kubectl apply -n default -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
  labels:
    app: curl
  name: curl
spec:
  containers:
  - image: curlimages/curl
    name: curl
    command: ["sleep", "365d"]
  nodeName: $NODE1
---
apiVersion: v1
kind: Pod
metadata:
  labels:
    app: httpbin
  name: httpbin
spec:
  containers:
  - image: kennethreitz/httpbin
    name: httpbin
  nodeName: $NODE2
EOF
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.

網絡配置

接下來,一起看下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
      }
    }
  ]
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.

还有 Flannel 的网络配置,配置中有我们设置的 Pod CIDR 10.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"
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

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

运行

圖片

集群初始化时使用了默认的 Pod CIDR 10.42.0.0/16​,当有节点加入集群,集群会从该网段上为节点分配 属于节点的 Pod CIDR 10.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"
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

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

#node 192.168.1.12
cat /run/flannel/subnet.env
FLANNEL_NETWORK=10.42.0.0/16
FLANNEL_SUBNET=10.42.0.1/24
FLANNEL_MTU=1450
FLANNEL_IPMASQ=true
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

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"
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.

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"
    }
  ]
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.

port-mapping 插件

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

Debug

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

tcpdump -i cni0 port 80 -vvv
  • 1.

从 pod curl​ 中使用 pod httpbin​ 的 IP 地址 10.42.1.2 发送请求:

kubectl exec curl -n default -- curl -s 10.42.1.2/get
  • 1.

cni0

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

圖片

tcpdump-on-cni0

host eth0

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

netstat -tupln | grep 8472
udp        0      0 0.0.0.0:8472            0.0.0.0:*                           -
  • 1.
  • 2.

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

tcpdump -i eth0 port 8472 -vvv
  • 1.

再次发送请求,可以看到抓取到 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.12
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         _gateway        0.0.0.0         UG    0      0        0 eth0
10.42.0.0       0.0.0.0         255.255.255.0   U     0      0        0 cni0
10.42.1.0       10.42.1.0       255.255.255.0   UG    0      0        0 flannel.1
192.168.1.0     0.0.0.0         255.255.255.0   U     0      0        0 eth0
#192.168.1.13
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         _gateway        0.0.0.0         UG    0      0        0 eth0
10.42.0.0       10.42.0.0       255.255.255.0   UG    0      0        0 flannel.1
10.42.1.0       0.0.0.0         255.255.255.0   U     0      0        0 cni0
192.168.1.0     0.0.0.0         255.255.255.0   U     0      0        0 eth0
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

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