Atlantis
GitHub 切换暗/亮/自动模式 切换暗/亮/自动模式 切换暗/亮/自动模式 返回首页

7. CNI笔记

Created At 2024-10-23 Updated on 2025-10-25

1. 前言

最初在开发边缘机房容器实例时,遇到将容器网络接入 VXLAN 需求,当时我们使用 Containerd 的 CRI 接口实现容器实例,对应的网络插件要用 CNI 框架开发,不过当时由另一位同事负责开发的网络插件,我只了解一个大概。

后续在前司的工作中,我也遇到了 CNI 相关的问题,一位客户有部署大规模集群的需求,且要求 VPC 内集群外虚机能与 Pod 网络直连,不凑巧的是这个需求还是由另外一个同事开发,再次失去学习 CNI 的机会。

这次就好好记录下最近关于 CNI 的测试,看看它到底在做什么,为什么有这么多种类,每一种 CNI 又适用于哪一种场景。

2. 什么是CNI

CNI 全称为 Container Network Interface,它最初是由 K8s 定义的容器网络接口标准,用于配置容器网络,包括配置网络接口、分配 IP 地址、设置路由规则等。

CNI 主要定义了以下四个接口:

  1. ADD:将容器添加到网络,或应用修改
  2. DEL:从网络中删除容器,或取消应用修改
  3. CHECK:检查容器的网络是否符合预期
  4. VERSION:输出版本支持

相比 Docker 网络插件,CNI 更加简洁,也更易于实现。

CNI 相关的资源如下:

  1. Specification:https://www.cni.dev/docs/spec
  2. Repository:https://github.com/containernetworking/cni
  3. Plugins:https://github.com/containernetworking/plugins

接下来会针对最简单的网络插件:Flannel,进行代码分析,Calico 及 Cilium 只做简单分析和测试,VPC-CNI 则从各云厂家的帮助文档参考总结。

3. 测试环境

构造测试环境确实比较烧钱,为了方便切换 CNI 和网络模式,我主要在 PVE 上创建虚拟机进行测试,部分云上的场景使用 UCloud 的虚拟机验证,具体配置如下:

  1. Kubernetes 版本:v1.30.5,对应的 K3s 版本为 v1.30.5+k3s1
  2. Containerd 版本:v1.6.36
  3. CNI 插件版本:Flannel v0.25.7、Calico v3.28.2、Cilium 1.16.3
  4. 操作系统: Debian 12,使用默认内核参数,并配置以下内核参数
    1. net.ipv4.ip_forward = 1
    2. net.ipv6.conf.all.forwarding = 1
    3. net.bridge.bridge-nf-call-iptables = 1
    4. net.bridge.bridge-nf-call-ip6tables = 1
  5. 云上环境:Master 与两个 Worker 都使用 2C4G
  6. 本地环境:宿主机 CPU 为 E5-2676 V4,虚拟机开启绑核,并在宿主机上将对应核心调整为性能模式,Master 与 Worker 的配置分别为 4C8G 和 8C32G
  7. 在 Worker 之间使用 iperf3 进行网络测试,每次测试时长为 100 秒,不限制速率,客户端命令如下:
    1. TCP 测试:iperf3 -c x.x.x.x -t 100
    2. UDP 测试:iperf3 -c x.x.x.x -u -b 0 -t 100

为了减少误差,最好在测试前将虚拟机绑定核心,并且将宿主机的 CPU 功耗调整到 performance 模式,命令如下:

cpupower frequency-set -g performance

宿主机上使用 localhost 测试 iperf3 结果如下,虚拟机以及容器之间的网络测试结果应该会比这个值更低。

➜  ~ iperf3 -c 127.0.0.1 -t 100
Connecting to host 127.0.0.1, port 5201
[  5] local 127.0.0.1 port 53600 connected to 127.0.0.1 port 5201
...
[ ID] Interval           Transfer     Bitrate         Retr
[  5]   0.00-100.00 sec   317 GBytes  27.2 Gbits/sec    0             sender
[  5]   0.00-100.00 sec   317 GBytes  27.2 Gbits/sec                  receiver

我尝试过调整内核参数与网卡多队列,没有明显效果,目前排查下来应该是内存通道问题,基于 B85 魔改的 X99 主板最多双通道,无法发挥 DDR3 1866Mhz x 4 的理论数值。

3.1 K8s

搭建本地环境 K8s 涉及的操作如下:

# 安装与配置 Containerd
wget -c https://github.com/containerd/containerd/releases/download/v1.6.36/cri-containerd-1.6.36-linux-amd64.tar.gz
tar -zxvf cri-containerd-1.6.36-linux-amd64.tar.gz -C /
containerd config default > /etc/containerd/config.toml
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/g' /etc/containerd/config.toml
systemctl enable --now containerd
# 安装 kubelet、kubeadm、kubectl
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.30/deb/ /' | tee /etc/apt/sources.list.d/kubernetes.list
apt update
apt install -y kubelet kubeadm kubectl
apt-mark hold kubelet kubeadm kubectl
systemctl enable --now kubelet
# 初始化 Master
kubeadm init --service-cidr=10.88.0.0/16 --pod-network-cidr=10.89.0.0/16
# 添加 Worker
kubeadm join myserver:6443 --token xxxxxxxx --discovery-token-ca-cert-hash xxxxxxx

如果忘记 token 与 discovery-token-ca-cert-hash,可以通过以下方法查询:

  1. 使用通过 kubeadm token list 命令查询 USAGESauthentication,signing 类型的 token,如果不存在可以手动创建 kubeadm token create –ttl 24h
  2. 使用 openssl 命令计算文件 /etc/kubernetes/pki/ca.crt 的哈希值:openssl x509 -in /etc/kubernetes/pki/ca.crt -noout -pubkey | openssl rsa -pubin -outform DER 2>/dev/null | sha256sum | cut -d' ' -f1

3.2 K3s

搭建云上环境 K3s 涉及的操作如下:

# 手动下载容器镜像包
wget -c "https://github.com/k3s-io/k3s/releases/download/v1.30.5+k3s1/k3s-airgap-images-amd64.tar.zst"
# 拷贝容器镜像包
mkdir -p /var/lib/rancher/k3s/agent/images && cp k3s-airgap-images-amd64.tar.zst /var/lib/rancher/k3s/agent/images/k3s-airgap-images-amd64.tar.zst
# 下载二进制文件
wget -c "https://github.com/k3s-io/k3s/releases/download/v1.30.5+k3s1/k3s" && chmod a+x k3s && cp k3s /usr/local/bin/k3s
# 初始化 Master sh -s - 后携带 k3s server 命令参数,flannel-backend 控制跨节点通信模式,测试中会使用到 vxlan 与 host-gw
curl -sfL https://get.k3s.io | INSTALL_K3S_SKIP_DOWNLOAD=true INSTALL_K3S_VERSION="v1.30.5+k3s1" INSTALL_K3S_EXEC="server" sh -s - --cluster-cidr 10.88.0.0/16 --service-cidr 10.89.0.0/16 --flannel-backend=host-gw --kube-proxy-arg proxy-mode=ipvs
# 添加 Worker
curl -sfL https://get.k3s.io | INSTALL_K3S_SKIP_DOWNLOAD=true INSTALL_K3S_VERSION="v1.30.5+k3s1" K3S_URL=https://myserver:6443 K3S_TOKEN=mynodetoken sh -

在国内云厂家的环境下进行测试需要考虑网络问题,上面的脚本中提前下载了离线安装使用的二进制文件和容器镜像,并在安装时指定跳过下载 K3s 二进制文件的步骤。

4. 测试结果

4.1 Flannel

PVE 上使用 iperf3 测试虚拟机和容器的跨节点通信能力,结果如下:

TCP

# 测试命令
iperf3 -c 10.244.1.41 -t 100
# 虚拟机
[ ID] Interval           Transfer     Bitrate         Retr
[  5]   0.00-100.00 sec   293 GBytes  25.2 Gbits/sec    0             sender
[  5]   0.00-100.00 sec   293 GBytes  25.2 Gbits/sec                  receiver
# 容器 flannel host-gw
[ ID] Interval           Transfer     Bitrate         Retr
[  5]   0.00-100.00 sec   238 GBytes  20.4 Gbits/sec  504             sender
[  5]   0.00-100.00 sec   238 GBytes  20.4 Gbits/sec                  receiver
# 容器 flannel vxlan
[ ID] Interval           Transfer     Bitrate         Retr
[  5]   0.00-100.00 sec  69.9 GBytes  6.00 Gbits/sec  10220             sender
[  5]   0.00-100.00 sec  69.9 GBytes  6.00 Gbits/sec                  receiver

UDP

# 测试命令 不限制 bitrate 默认是 1 Mbit/sec for UDP unlimited for TCP
iperf3 -c 10.244.1.41 -u -b 0 -t 100
# 虚拟机
[ ID] Interval           Transfer     Bitrate         Jitter    Lost/Total Datagrams
[  5]   0.00-100.00 sec  26.5 GBytes  2.28 Gbits/sec  0.000 ms  0/19680970 (0%)  sender
[  5]   0.00-100.00 sec  25.6 GBytes  2.20 Gbits/sec  0.003 ms  698117/19680942 (3.5%)  receiver
# 容器 flannel host-gw
[ ID] Interval           Transfer     Bitrate         Jitter    Lost/Total Datagrams
[  5]   0.00-100.00 sec  16.2 GBytes  1.39 Gbits/sec  0.000 ms  0/12041760 (0%)  sender
[  5]   0.00-100.00 sec  16.2 GBytes  1.39 Gbits/sec  0.006 ms  41355/12041760 (0.34%)  receiver
# 容器 flannel vxlan
[ ID] Interval           Transfer     Bitrate         Jitter    Lost/Total Datagrams
[  5]   0.00-100.00 sec  11.8 GBytes  1.02 Gbits/sec  0.000 ms  0/9092300 (0%)  sender
[  5]   0.00-100.00 sec  11.8 GBytes  1.01 Gbits/sec  0.007 ms  26642/9092300 (0.29%)  receiver

并发连接

上面的两个测试都是单线程测试,使用 -P 参数可以设置并发数,如下:

# 测试命令
iperf3 -c 192.168.123.6 -t 100 -P 4
# 测试结果
[ ID] Interval           Transfer     Bitrate         Retr
[  5]   0.00-100.00 sec  71.3 GBytes  6.12 Gbits/sec    0             sender
[  5]   0.00-100.00 sec  71.3 GBytes  6.12 Gbits/sec                  receiver
[  7]   0.00-100.00 sec  71.3 GBytes  6.12 Gbits/sec    0             sender
[  7]   0.00-100.00 sec  71.3 GBytes  6.12 Gbits/sec                  receiver
[  9]   0.00-100.00 sec  71.3 GBytes  6.12 Gbits/sec    0             sender
[  9]   0.00-100.00 sec  71.3 GBytes  6.12 Gbits/sec                  receiver
[ 11]   0.00-100.00 sec  71.2 GBytes  6.12 Gbits/sec    0             sender
[ 11]   0.00-100.00 sec  71.2 GBytes  6.12 Gbits/sec                  receiver
[SUM]   0.00-100.00 sec   285 GBytes  24.5 Gbits/sec    0             sender
[SUM]   0.00-100.00 sec   285 GBytes  24.5 Gbits/sec                  receiver
# 测试命令
iperf3 -c 192.168.123.6 -u -b 0 -t 100 -P 4
# 测试结果
[ ID] Interval           Transfer     Bitrate         Jitter    Lost/Total Datagrams
[  5]   0.00-100.00 sec  6.97 GBytes   599 Mbits/sec  0.000 ms  0/5170910 (0%)  sender
[  5]   0.00-100.00 sec  6.96 GBytes   598 Mbits/sec  0.011 ms  9926/5170910 (0.19%)  receiver
[  7]   0.00-100.00 sec  6.97 GBytes   599 Mbits/sec  0.000 ms  0/5170870 (0%)  sender
[  7]   0.00-100.00 sec  6.96 GBytes   598 Mbits/sec  0.012 ms  9930/5170870 (0.19%)  receiver
[  9]   0.00-100.00 sec  6.97 GBytes   599 Mbits/sec  0.000 ms  0/5170850 (0%)  sender
[  9]   0.00-100.00 sec  6.96 GBytes   598 Mbits/sec  0.013 ms  9939/5170850 (0.19%)  receiver
[ 11]   0.00-100.00 sec  6.97 GBytes   599 Mbits/sec  0.000 ms  0/5170810 (0%)  sender
[ 11]   0.00-100.00 sec  6.96 GBytes   598 Mbits/sec  0.012 ms  9941/5170810 (0.19%)  receiver
[SUM]   0.00-100.00 sec  27.9 GBytes  2.40 Gbits/sec  0.000 ms  0/20683440 (0%)  sender
[SUM]   0.00-100.00 sec  27.8 GBytes  2.39 Gbits/sec  0.012 ms  39736/20683440 (0.19%)  receiver

从日志看,虚拟机上 TCP 最高达到 28Gbps 左右,稳定在 25Gbps 左右,上限差不多在这里,而 UDP 性能还比较糟糕,感觉有很大优化空间,容器网络下就不再做并发测试了。

4.2 Calico

下面是直接路由模式下的 Calico 测试结果,测试时设置了 ipipModevxlanMode 为 Never。

# TCP
[ ID] Interval           Transfer     Bitrate         Retr
[  5]   0.00-100.00 sec   207 GBytes  17.8 Gbits/sec  838             sender
[  5]   0.00-100.00 sec   207 GBytes  17.8 Gbits/sec                  receiver
# UDP
[ ID] Interval           Transfer     Bitrate         Jitter    Lost/Total Datagrams
[  5]   0.00-100.00 sec  12.0 GBytes  1.03 Gbits/sec  0.000 ms  0/8902310 (0%)  sender
[  5]   0.00-100.00 sec  12.0 GBytes  1.03 Gbits/sec  0.009 ms  5427/8902310 (0.061%)  receiver

测试结果让人有点意外,即使在直接路由模式,也没赶上 host-gw 模式的 Flannel,这里还没做绑定核心操作,可以认为是在误差内。

Calico 的默认配置下会设置 ippools 的 ipipMode 为 Alaways,vxlanMode 为 Never,也就是使用 IPIP 作为跨节点和跨子网通信模式,性能更差。

我们可以修改 ippools 配置,设置 ipipMode 为 CrossSubnet,只在跨子网时使用 IPIP 封装数据包,同子网下使用直接路由,若需要跨子网直接路由得底层网络支持,在云上通常可以通过配置 VPC 路由表实现,不过单表规则条数有限制,一般在 50 条。

4.3 Flannel on VPC

这里使用 UCloud 的虚拟机来摸一摸 VPC 网络中的容器网络性能,测试结果如下:

# 虚拟机 TCP
[ ID] Interval           Transfer     Bitrate         Retr
[  5]   0.00-100.00 sec  73.0 GBytes  6.27 Gbits/sec  5493             sender
[  5]   0.00-100.04 sec  73.0 GBytes  6.27 Gbits/sec                  receiver

# 虚拟机 UDP
[ ID] Interval           Transfer     Bitrate         Jitter    Lost/Total Datagrams
[  5]   0.00-100.00 sec  34.7 GBytes  2.98 Gbits/sec  0.000 ms  0/26640670 (0%)  sender
[  5]   0.00-100.05 sec  20.3 GBytes  1.75 Gbits/sec  0.008 ms  11046518/26640498 (41%)  receiver

# Flannel host-gw TCP
[ ID] Interval           Transfer     Bitrate         Retr
[  5]   0.00-100.00 sec  15.7 GBytes  1.35 Gbits/sec  6243             sender
[  5]   0.00-100.06 sec  15.7 GBytes  1.35 Gbits/sec                  receiver

# Flannel host-gw UDP
[ ID] Interval           Transfer     Bitrate         Jitter    Lost/Total Datagrams
[  5]   0.00-100.00 sec  29.8 GBytes  2.56 Gbits/sec  0.000 ms  0/22871500 (0%)  sender
[  5]   0.00-100.25 sec  4.01 GBytes   344 Mbits/sec  0.002 ms  19791910/22869147 (87%)  receiver

我在一个 VPC 中创建了三个子网,每个子网内创建一个虚拟机,然后配置了 VPC 的路由表,将节点关联的 PodCIDR 网段下一跳设置为该节点 IP,这样就可以跨子网使用 host-gw 模式的 Flannel。

可以看到跨子网直接路由网络性能不到 VM 直连的三分之一也,如果切换到 vxlan 模式,恐怕还会下降更多。

VPC 是通过 SDN 实现的,能支持跨子网节点间的直接路由,但无法支持 Overlay 网络流量的直接路由,这是受限于 ARP 包无法跨二层,我们需要配置路由表让 SDN 允许转发 Overlay 网络流量,而这里又有一个限制,单路由表最多 50 条规则(腾讯云的 global router 可能是做了特殊处理,没看到关于路由表 50 条规则的限制)。

我们最好选择创建一个大的子网网段来创建节点,因为每个子网只能绑定一个路由表。

4.4 Cilium

最后是新起之秀 Cilium,Cilium 默认使用 vxlan 模式,开启直接路由的配置如下:

# 安装 Cilium
cilium install --version 1.16.3
# 配置 Cilium 参数,以下命令会修改 Cilium 使用的 configmap
cilium config set routing-mode native
cilium config set auto-direct-node-routes true
# 这里的网段选择宿主机所在网段,与 Calico 的 CrossSubnet 策略有点差异,似乎只能设置一个网段
cilium config set ipv4-native-routing-cidr 192.168.123.0/24

下面是测试结果:

# Cilium vxlan TCP
[ ID] Interval           Transfer     Bitrate         Retr
[  5]   0.00-100.00 sec  67.2 GBytes  5.77 Gbits/sec  14838             sender
[  5]   0.00-100.00 sec  67.2 GBytes  5.77 Gbits/sec                  receiver
# Cilium vxlan UDP
[ ID] Interval           Transfer     Bitrate         Jitter    Lost/Total Datagrams
[  5]   0.00-100.00 sec  10.9 GBytes   938 Mbits/sec  0.000 ms  0/8384400 (0%)  sender
[  5]   0.00-100.00 sec  10.8 GBytes   930 Mbits/sec  0.011 ms  70779/8384400 (0.84%)  receiver
# Cilium native TCP
[ ID] Interval           Transfer     Bitrate         Retr
[  5]   0.00-100.00 sec   214 GBytes  18.3 Gbits/sec  738             sender
[  5]   0.00-100.00 sec   214 GBytes  18.3 Gbits/sec                  receiver
# Cilium native UDP
[ ID] Interval           Transfer     Bitrate         Jitter    Lost/Total Datagrams
[  5]   0.00-100.00 sec  14.2 GBytes  1.22 Gbits/sec  0.000 ms  0/10554910 (0%)  sender
[  5]   0.00-100.00 sec  14.2 GBytes  1.22 Gbits/sec  0.010 ms  1948/10554910 (0.018%)  receiver

性能还是略差于 host-gw 模式的 Flannel,下面是将虚拟机绑定核心并开启性能模式后的二次测试结果,绑核与睿频确实能提升一部分性能。

# Cilium native TCP
[ ID] Interval           Transfer     Bitrate         Retr
[  5]   0.00-100.00 sec   229 GBytes  19.7 Gbits/sec   73             sender
[  5]   0.00-100.00 sec   229 GBytes  19.7 Gbits/sec                  receiver
# Cilium native UDP
[ ID] Interval           Transfer     Bitrate         Jitter    Lost/Total Datagrams
[  5]   0.00-100.00 sec  14.3 GBytes  1.23 Gbits/sec  0.000 ms  0/10634750 (0%)  sender
[  5]   0.00-100.00 sec  14.3 GBytes  1.23 Gbits/sec  0.013 ms  271/10634738 (0.0025%)  receiver

4.5 Calico with eBPF

Calico 也支持使用 eBPF 代替标准 Linux 数据面(基于 iptables 或 nftables),这里补一个安装与测试结果,总体感觉比 Cilium 的部署流程要顺利,测试结果也让人有点意外。

安装步骤如下:

# 禁用 kube-proxy,也可以在安装时直接禁用 kubeadm init --skip-phases=addon/kube-proxy
kubectl patch ds -n kube-system kube-proxy -p '{"spec":{"template":{"spec":{"nodeSelector":{"non-calico": "true"}}}}}'
# 安装 operator
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.29.0/manifests/tigera-operator.yaml
# 创建 configmap,填写 kube-apiserver 的内网地址与端口,用于在集群内支持通过虚 IP 访问 kube-apiserver
kubectl apply -f - <<EOF
kind: ConfigMap
apiVersion: v1
metadata:
  name: kubernetes-services-endpoint
  namespace: tigera-operator
data:
  KUBERNETES_SERVICE_HOST: "<API server host>"
  KUBERNETES_SERVICE_PORT: "<API server port>"
EOF
# 创建 CR,注意这个 CR 中的 cidr 需要修改成与当前集群 PodCIDR 一致
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.29.0/manifests/custom-resources.yaml
# 等待组件就绪
kubectl get tigerastatus -w

然后我们检查下 master 节点环境:

# 可以看到同子网直接路由
➜  ~ ip route
default via 192.168.123.1 dev eth0 onlink
10.76.43.0/26 via 192.168.123.6 dev eth0 proto 80 onlink
blackhole 10.76.92.192/26 proto 80
10.76.92.198 dev calib5fac6d9571 scope link
10.80.226.64/26 via 192.168.123.7 dev eth0 proto 80 onlink
169.254.1.1 dev bpfin.cali scope link
172.31.0.10 via 169.254.1.1 dev bpfin.cali src 192.168.123.5
192.168.123.0/24 dev eth0 proto kernel scope link src 192.168.123.5
# 进入 calico-node 容器使用 -bpf 工具检查状态,感觉工具链还不太完善
➜  ~ nerdctl -n k8s.io ps|grep calico-node
11295cb2c381    registry.k8s.io/pause:3.6                          "/pause"                  3 hours ago    Up                 k8s://calico-system/calico-node-64s2l
e318f2732d54    docker.io/calico/node:v3.29.0                      "start_runit"             3 hours ago    Up                 k8s://calico-system/calico-node-64s2l/calico-node
➜  ~ nerdctl -n k8s.io exec -it e318f2732d54 bash
[root@master-0 /]# calico-node -bpf help
tool for interrogating Calico BPF state

Usage:
  calico-bpf [command]

Available Commands:
  arp          Manipulates arp
  completion   Generate the autocompletion script for the specified shell
  connect-time Manipulates connect-time load balancing programs
  conntrack    Manipulates connection tracking
  counters     Show and reset counters
  help         Help about any command
  ifstate      Manipulates ifstate
  ipsets       Manipulates ipsets
  nat          Manipulates network address translation (nat)
  policy       Dump policy attached to interface
  routes       Manipulates routes
  version      Prints the version and exits

Flags:
      --config string      config file (default is $HOME/.calico-bpf.yaml)
  -h, --help               help for calico-bpf
  -6, --ipv6               Use IPv6 instead of IPv4
      --log-level string   Set log level (default "warn")

Use "calico-bpf [command] --help" for more information about a command.
[root@master-0 /]# calico-node -bpf routes dump
     10.0.0.0/8: remote in-pool nat-out
  10.76.43.0/26: remote workload in-pool nat-out same-subnet tunneled nh 192.168.123.6
  10.76.43.0/32: remote host in-pool nat-out same-subnet tunneled
10.76.92.192/32: local host
10.76.92.198/32: local workload in-pool nat-out same-subnet idx 3
10.80.226.64/26: remote workload in-pool nat-out same-subnet tunneled nh 192.168.123.7
10.80.226.64/32: remote host in-pool nat-out same-subnet tunneled
192.168.123.5/32: local host
192.168.123.6/32: remote host
192.168.123.7/32: remote host

我们可以看到:

  1. 当前使用 VXLAN 的 CrossSubnet 模式,同子网直接路由,跨子网 Overlay,与之前保持了一致
  2. Calico 使用 eBPF 代替了 kube-proxy 的功能,现在从集群外通过 NodePort 访问服务,后端 Pod 也能获取到来源 IP
  3. 由于使用 eBPF 代替了标准 Linux 数据面,容器网络通信的整体性能与稳定性都有所提升,包括:数据吞吐量、首包延迟、DSR、连接跟踪效率、路由性能等

eBPF 数据面是为 40 Gbits/sec 的网络设计的,而标准 Linux 数据面是针对 10 Gbits/sec 设计的,更多关于 eBPF 的信息可以查看:About eBPF

下面是测试数据:

# 跨节点 Pod 间 TCP 测试
[ ID] Interval           Transfer     Bitrate         Retr
[  5]   0.00-100.00 sec   233 GBytes  20.0 Gbits/sec   99             sender
[  5]   0.00-100.00 sec   232 GBytes  20.0 Gbits/sec                  receiver
# 跨节点 Pod 间 UDP 测试
[ ID] Interval           Transfer     Bitrate         Jitter    Lost/Total Datagrams
[  5]   0.00-100.00 sec  21.5 GBytes  1.84 Gbits/sec  0.000 ms  0/16475010 (0%)  sender
[  5]   0.00-99.99  sec  21.4 GBytes  1.84 Gbits/sec  0.005 ms  10986/16475010 (0.067%)  receiver
# 下面是集群外节点使用 NodePort 做 TCP 测试
# 访问 Pod 所在节点的 NodePort
[ ID] Interval           Transfer     Bitrate         Retr
[  5]   0.00-100.00 sec   366 GBytes  31.4 Gbits/sec    0             sender
[  5]   0.00-100.00 sec   366 GBytes  31.4 Gbits/sec                  receiver
# 开启 DSR 时访问其他节点的 NodePort
[ ID] Interval           Transfer     Bitrate         Retr
[  5]   0.00-100.00 sec  80.2 GBytes  6.89 Gbits/sec  1413             sender
[  5]   0.00-100.00 sec  80.2 GBytes  6.89 Gbits/sec                  receiver
# 使用 NodePort 时在容器内仍旧可以看到来源 IP,注意 iperf3 无法使用负载均衡模式,Service 只能有只能有一个后端 iperf3 服务端 Pod
root@debian-5596568bc9-f6vg9:~# ss -atupn
Netid              State               Recv-Q               Send-Q                                     Local Address:Port                                       Peer Address:Port               Process
tcp                LISTEN              0                    4096                                                   *:5201                                                  *:*                   users:(("iperf3",pid=1,fd=3))
tcp                ESTAB               0                    0                                  [::ffff:10.80.226.72]:5201                             [::ffff:192.168.123.3]:37300               users:(("iperf3",pid=1,fd=5))
tcp                ESTAB               0                    0                                  [::ffff:10.80.226.72]:5201                             [::ffff:192.168.123.3]:37288               users:(("iperf3",pid=1,fd=4))

5. 测试总结

从上述的测试结果不难看出:

  1. 在条件允许的情况下,直接路由模式的性能最高,再想提升性能的话,就需要使用云厂商提供的 VPC CNI 方案,实际性能取决于 SDN 优化
  2. 云上环境要跨子网使用直接路由模式的话,需要配置子网路由表,而路由表条目有限,限制了跨子网部署节点的数量,感觉只适合单子网场景
  3. 本地环境(或者物理环境)要跨子网使用直接路由模式的话,需要配置交换机转发规则,目前缺少这方面的测试条件,但我推测物理交换机也会存在类似限制
  4. Calico 能适应复杂的网络场景,可配置项又多又繁琐,云上的小规模集群无法展现它的优势,只有大规模集群环境和物理环境才能发挥它的能力
  5. Cilium 作为后起之秀,包含了 Calico 该有的所有功能,但似乎更强调可观测能力,它依赖较新版本的内核能力,落地会有一些困难

这时回顾下前司的开发工作,当时对 VPC CNI 的定义是有些不恰当,只满足了两个常见招标需求:

  1. 提供接近虚拟机的网络性能:这里使用 Calico 的直接路由模式,不过 VPC 内虚拟机内网互联带宽并不高,没有达到万兆,因此衰减不明显
  2. 支持集群外的虚拟机直接访问 Pod:这里用到了 VPC 的路由能力,会从 VPC 中预占网段,然后将节点关联的 PodCIDR 下一跳设置为节点 IP

实际上客户的需求只有第一个,容器网络性能达到或者接近虚拟机水平,第二点没有配合静态 IP 使用的话,无实际意义,每次容器重建 IP 都会发生变更。

当时网络团队给出的建议是:只调用 VPC 接口预占和释放网段接口,然后自行同步路由信息,剩余工作在 Calico 内闭环~听起来和标准 VPC CNI 方案相差甚远,没有做到网卡级别的联动,所有容器仍共享宿主机的内网网卡性能。

我认为即使将这里的 Calico 替换为 host-gw 模式的 Flannel 也没有问题,在 UCloud 的虚拟机上就测试了这种玩法,甚至将 ServiceCIDR 一起添加到路由表中,集群外同 VPC 内的虚拟机也能正常访问到服务。

此外还记起一个小问题,之前容器产品使用 Calico 的 Overlay 模式时,继续使用 ipipMode 默认值 Always 而不是 CrossSubnet,这意味着同子网节点之间也无法直接路由,性能只有三分之一。

6. 网络插件分析

kubelet 不直接调用 CNI 插件配置容器网络,而是在创建和删除 Pod 对应的 Sandbox 时,通过 CRI 服务间接调用 setupPodNetwork 与 teardownPodNetwork 触发 CNI 插件配置容器网络。

接下来分析下常用的网络插件架构与实现,网络插件通常至少有两个组成部分:

  1. node-daemon:运行在每个节点上,负责生成节点网络配置文件、配置节点网络接口、维护集群跨节点网络通信等,例如 flanneld
  2. cni-plugin:实现 CNI 接口的二进制文件,在 CRI 服务创建/删除沙箱时会被直接调用,如 flannel、loopback 等

复杂的网络插件还有更多组件,比如 Calico-Typha、Hubble-UI 等。

6.1 Flannel

alt text

特性 支持选项 说明
Policy 不支持网络策略 需要配合其他组件(如 Calico)实现网络策略功能
IPAM host-local 本地 IP 地址分配,支持配置网段范围和子网划分
CNI flannel 基础 CNI 插件,配置路径: /etc/cni/net.d/10-flannel.conflist
Overlay vxlan、host-gw、udp、wireguard vxlan: 默认模式,overlay网络
host-gw: 直接路由模式,性能较好
udp: 调试用途
wireguard: 加密通信
Routing flanneld 由 flanneld 同步路由规则
Datastore kubernetes 使用 K8s API 存储配置和状态

由于 ARP 包无法跨二层,一般 host-gw 只能在所有节点接入同一个二层的情况下使用,因此 flannel 的默认使用 vxlan 组网转发数据,但我们可以看到它的性能损失较大。

Flannel 关联的项目有两个:

  1. cni-plugin:github.com/flannel-io/cni-plugin
  2. flannel:github.com/flannel-io/flannel

cni-plugin 项目的 flannel.go 文件中可以看到 flannel 实现的 CNI 接口:

func main() {
	fullVer := fmt.Sprintf("CNI Plugin %s version %s (%s/%s) commit %s built on %s", Program, Version, runtime.GOOS, runtime.GOARCH, Commit, buildDate)
	skel.PluginMain(cmdAdd, cmdCheck, cmdDel, cni.All, fullVer)
}

func cmdAdd(args *skel.CmdArgs) error {
	n, err := loadFlannelNetConf(args.StdinData)
	if err != nil {
		return fmt.Errorf("loadFlannelNetConf failed: %w", err)
	}
	fenv, err := loadFlannelSubnetEnv(n.SubnetFile)
	if err != nil {
		return fmt.Errorf("loadFlannelSubnetEnv failed: %w", err)
	}

	if n.Delegate == nil {
		n.Delegate = make(map[string]interface{})
	} else {
		if hasKey(n.Delegate, "type") && !isString(n.Delegate["type"]) {
			return fmt.Errorf("'delegate' dictionary, if present, must have (string) 'type' field")
		}
		if hasKey(n.Delegate, "name") {
			return fmt.Errorf("'delegate' dictionary must not have 'name' field, it'll be set by flannel")
		}
		if hasKey(n.Delegate, "ipam") {
			return fmt.Errorf("'delegate' dictionary must not have 'ipam' field, it'll be set by flannel")
		}
	}

	if n.RuntimeConfig != nil {
		n.Delegate["runtimeConfig"] = n.RuntimeConfig
	}

	return doCmdAdd(args, n, fenv)
}

func cmdDel(args *skel.CmdArgs) error {
	nc, err := loadFlannelNetConf(args.StdinData)
	if err != nil {
		return err
	}

	if nc.RuntimeConfig != nil {
		if nc.Delegate == nil {
			nc.Delegate = make(map[string]interface{})
		}
		nc.Delegate["runtimeConfig"] = nc.RuntimeConfig
	}

	return doCmdDel(args, nc)
}

func cmdCheck(args *skel.CmdArgs) error {
	// TODO: implement
	return nil
}

分析代码可以发现它主要做配置文件解析,以及调用 CNI 框架,整体逻辑比较简单,flannel 项目略微复杂一些,除了分配子网、同步路由外,还支持了多种 backend:

alt text

如下,切换 backend 后可以看到区别只是 PodCIDR 网段下一跳发生了变化:

# flannel host-gw
➜  ~ ip route
default via 192.168.123.1 dev eth0 onlink
10.0.1.0/24 via 192.168.123.6 dev eth0
10.0.2.0/24 via 192.168.123.7 dev eth0
192.168.123.0/24 dev eth0 proto kernel scope link src 192.168.123.5
# flannel vxlan
➜  ~ ip route
default via 192.168.123.1 dev eth0 onlink
10.0.1.0/24 via 10.0.1.0 dev flannel.1 onlink
10.0.2.0/24 via 10.0.2.0 dev flannel.1 onlink
192.168.123.0/24 dev eth0 proto kernel scope link src 192.168.123.5
# flannel ipip
➜  ~ ip route
default via 192.168.123.1 dev eth0 onlink
10.0.1.0/24 via 192.168.123.6 dev flannel.ipip onlink
10.0.2.0/24 via 192.168.123.7 dev flannel.ipip onlink
192.168.123.0/24 dev eth0 proto kernel scope link src 192.168.123.5

CNI 配置不变,容器都是插入到 cni0 网桥:

➜  ~ brctl show
bridge name	bridge id		STP enabled	interfaces
cni0		8000.7e5d8326e49a	no		veth5312acde
➜  ~ cat /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
      }
    }
  ]
}

变化的只有跨节点通信方案,即使我们动态修改 backend,修改前创建的 Pod 跨节点通信仍旧正常,不过切换后会多出一些网络设备:

➜  ~ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether bc:24:11:ee:e5:1a brd ff:ff:ff:ff:ff:ff
    altname enp6s18
    inet 192.168.123.5/24 brd 192.168.123.255 scope global eth0
       valid_lft forever preferred_lft forever
3: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN group default
    link/ether 62:8c:8e:c8:ea:04 brd ff:ff:ff:ff:ff:ff
    inet 10.0.0.0/32 scope global flannel.1
       valid_lft forever preferred_lft forever
4: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000
    link/ipip 0.0.0.0 brd 0.0.0.0
5: flannel.ipip@NONE: <NOARP,UP,LOWER_UP> mtu 1480 qdisc noqueue state UNKNOWN group default
    link/ipip 192.168.123.5 brd 0.0.0.0
    inet 10.0.0.0/32 scope global flannel.ipip
       valid_lft forever preferred_lft forever

6.2 Calico

首先是 calico-kube-conrtollers,包含多个控制器用于监听 Calico 相关资源:

graph TD
   A[kube-apiserver] -->|配置/状态| CKC[calico-kube-controllers]

   CKC --> PC[Policy Controller]
   CKC --> NC[Node Controller]
   CKC --> NSC[Namespace Controller]
   CKC --> SAC[ServiceAccount Controller]
   CKC --> WEC[WorkloadEndpoint Controller]

   PC -->|同步| PO[策略对象]
   NC -->|维护| NO[节点状态]
   NSC -->|处理| NSO[命名空间变更]
   SAC -->|处理| SAO[服务账号变更]
   WEC -->|同步| WEO[工作负载端点]

   classDef k8s fill:#326ce5,stroke:#fff,stroke-width:4px,color:#fff
   class A k8s
   class CKC k8s

其次是 calico-node,包含多个组件,用于维护集群网络:

graph TD
    A[kube-apiserver] -->|配置/状态| CN-01[calico-node]
    A[kube-apiserver] -->|配置/状态| CN-02[calico-node]
    subgraph Node-02
        CN-01 --> F-01[Felix]
        CN-01 --> B-01[BIRD]
        CN-01 --> C-01[confd]
        
        F-01 -->|配置| N-01[网络接口]
        F-01 -->|维护| R-01[路由表]
        F-01 -->|执行| P-01[网络策略]
        
        C-01 -->|生成| BC-01[BIRD配置]
        BC-01 --> B-01
    end
    subgraph Node-01
        CN-02 --> F-02[Felix]
        CN-02 --> B-02[BIRD]
        CN-02 --> C-02[confd]
        
        F-02 -->|配置| N-02[网络接口]
        F-02 -->|维护| R-02[路由表]
        F-02 -->|执行| P-02[网络策略]
        
        C-02 -->|生成| BC-02[BIRD配置]
        BC-02 --> B-02
    end
    B-01 <-.-> BGP
    B-02 <-.-> BGP
   classDef k8s fill:#326ce5,stroke:#fff,stroke-width:4px,color:#fff
   class A k8s
   class CN-01 k8s
   class CN-02 k8s
   class BGP k8s
特性 支持选项 说明
Policy NetworkPolicy
GlobalNetworkPolicy
支持标准 K8s NetworkPolicy
支持 Calico 自定义策略
支持集群级全局策略
IPAM host-local
calico-ipam
支持 IP 池管理
支持 IP 块分配
CNI calico 支持动态配置更新
Overlay IPIP
VXLAN
None
IPIP: 适用于跨子网通信
VXLAN: 支持跨网络通信
None: 原生路由,最佳性能
Routing BGP 支持完整 BGP 协议
支持大规模集群路由
支持自定义 AS 号
Datastore kubernetes 支持使用 K8s API 存储

Calico 关联的主要项目是:github.com/projectcalico/calico,官方站点为:About Calico,有商业公司兜底,文档齐全。

Calico 适合在可控网络环境下,对网络性能和安全性有较高要求的场景,比如私有云/IDC、混合云以及裸金属,最适合与物理网络联动。

7. 确定最佳网络选项

关于如何选择部署选项,Calico 这一篇文档就比较有意思:Determine best networking option,下面简单解读一下。

Calico 灵活的模块化架构支持广泛的部署选项,因此可以选择适合特定环境和需求的最佳网络方法,这包括能够以非覆盖或覆盖模式(non-overlay or overlay)、带或不带 BGP 运行各种 CNI 和 IPAM 插件以及底层网络类型。

7.1 概念

K8s 网络基础

  1. 每个 Pod 都有自己的 IP 地址
  2. 任何节点上的 Pod 都可以与所有其他节点上的所有 Pod 通信,无需 NAT

Overlay 网络

Overlay 网络是一个建立在另一个网络之上的网络,在 K8s 中,Overlay 网络可用于处理节点之间的 Pod 到 Pod 流量,底层网络无需关心 Pod 的 IP 地址或 Pod 运行在哪些节点上,两种常用于构建 Overlay 网络的协议是 VXLAN 和 IP-in-IP。

使用 Overlay 网络的优点是减少了对底层网络的依赖,例如几乎可以在任何底层网络上运行 VXLAN Overlay 网络,而主要缺点是:

  1. 对性能有影响,封装数据包的过程需要占用少量 CPU,并且数据包中需要额外字节来对封装进行编码
  2. Pod IP 地址在集群外部不可路由

跨子网 Overlay

除了标准 VXLAN 或 IP-in-IP 覆盖之外,Calico 还支持 VXLAN 和 IP-in-IP 的 CrossSubnet 模式,这种模式下,每个子网内使用底层网络充当 L2 网络,同子网内发送的数据包不会被封装,从而获得 non-overlay 网络的性能,而跨子网发送的数据包会像 Overlay 网络一样被封装,减少对底层网络的依赖(这一点我认为是 Calico 区别于 Flannel 的关键)。

和标准的 Overlay 网络一样,跨子网 Overlay 模式下,底层网络无法感知 Pod IP 地址,并且 Pod IP 地址在集群外不可路由。

Pod IP 在集群外的可路由性

不同Kubernetes网络实现的一个重要区别在于:Pod IP 地址是否可以在集群外部网络中路由。

如果 Pod IP 在集群外部不可路由,那么 Pod 尝试与集群外部的 IP 地址建立网络连接时,K8s 将使用 SNAT 来更改源 IP 地址:从 Pod 的 IP 地址转换为托管 Pod 的节点 IP 地址,同样对于入向连接,集群外部的任何服务都无法直接连接到 Pod IP 地址,需要通过 Service 或者 Ingress 来完成。

如果 Pod IP 地址在集群外部可路由,则 Pod 可以在没有 SNAT 的情况下连接到集群外部,并且集群外部服务可以直接连接到 Pod,无需 Service 或者 Ingress,以下简称直接路由。

直接路由的优点是:

  1. 避免 SNAT,简化调试和操作复杂度,更容易通过来源 IP 定位问题
  2. 可以直接访问 Pod,相比使用 hostNetwork 方案来暴露服务更简单

直接路由的缺点是 Pod IP 在集群外部网络中必须是唯一的,如果运行多个集群,需要为每个集群中的 PodCIDR 预分配地址段,在大规模环境下可能遇到 IP 地址不足问题。

那什么决定了可路由性?如果使用 Overlay 网络,通常 Pod IP 无法在集群外部路由,若不使用 Overlay 网络,Pod IP 的可路由性取决于所使用的 CNI 插件、云提供商集成或与物理网络的 BGP 对等互连的组合。

BGP

BGP (Border Gateway Protocol) 是互联网核心路由协议,主要用于不同自治系统(AS)之间的路由选择。它是互联网的基本构建块之一,具有出色的扩展特性。

Calico 具备内置的 BGP 支持。在本地部署中,允许 Calico 与物理网络(通常是机架顶部路由器)对等,以交换路由,从而创建一个 non-overlay 网络,使 Pod 的 IP 地址可以跨越更广泛的网络路由,就像网络上任意其他工作负载一样。

7.2 网络选项

最佳选项

Calico 在本地的最常见网络设置是 non-overlay 模式,使用 BGP 与物理网络(通常是机架顶部路由器)对等,以使 Pod IP 可以在集群外部可路由,如果有需要的话,还可以配置其余的本地网络以限制 Pod IP 在集群外部的路由范围。

这种设置提供了一系列丰富的高级功能,包括能够宣告 Kubernetes Service IP(Cluster IP 或 External IP),以及能够在 Pod、命名空间或节点级别控制 IP 地址管理,以支持与现有企业网络和安全要求集成的广泛可能性。

跨子网 Overlay

在云上环境通常无法利用 BGP 实现 non-overlay 模式,但可以利用 CrossSubnet 模式实现同子网下的直接路由与跨子网 Overlay 网络来优化每个 L2 子网内的性能。

Overlay 网络

如果无法确定适合当前环境的网络模式,可以利用 VXLAN 构建 Overlay 网络,它几乎可以在任何环境中使用。