7. CNI笔记
最初在开发边缘机房容器实例时,遇到将容器网络接入 VXLAN 需求,当时我们使用 Containerd 的 CRI 接口实现容器实例,对应的网络插件要用 CNI 框架开发,不过当时由另一位同事负责开发的网络插件,我只了解一个大概。
后续在前司的工作中,我也遇到了 CNI 相关的问题,一位客户有部署大规模集群的需求,且要求 VPC 内集群外虚机能与 Pod 网络直连,不凑巧的是这个需求还是由另外一个同事开发,再次失去学习 CNI 的机会。
这次就好好记录下最近关于 CNI 的测试,看看它到底在做什么,为什么有这么多种类,每一种 CNI 又适用于哪一种场景。
CNI 全称为 Container Network Interface,它最初是由 K8s 定义的容器网络接口标准,用于配置容器网络,包括配置网络接口、分配 IP 地址、设置路由规则等。
CNI 主要定义了以下四个接口:
- ADD:将容器添加到网络,或应用修改
- DEL:从网络中删除容器,或取消应用修改
- CHECK:检查容器的网络是否符合预期
- VERSION:输出版本支持
相比 Docker 网络插件,CNI 更加简洁,也更易于实现。
CNI 相关的资源如下:
- Specification:https://www.cni.dev/docs/spec
- Repository:https://github.com/containernetworking/cni
- Plugins:https://github.com/containernetworking/plugins
接下来会针对最简单的网络插件:Flannel,进行代码分析,Calico 及 Cilium 只做简单分析和测试,VPC-CNI 则从各云厂家的帮助文档参考总结。
构造测试环境确实比较烧钱,为了方便切换 CNI 和网络模式,我主要在 PVE 上创建虚拟机进行测试,部分云上的场景使用 UCloud 的虚拟机验证,具体配置如下:
- Kubernetes 版本:v1.30.5,对应的 K3s 版本为 v1.30.5+k3s1
- Containerd 版本:v1.6.36
- CNI 插件版本:Flannel v0.25.7、Calico v3.28.2、Cilium 1.16.3
- 操作系统: Debian 12,使用默认内核参数,并配置以下内核参数
- net.ipv4.ip_forward = 1
- net.ipv6.conf.all.forwarding = 1
- net.bridge.bridge-nf-call-iptables = 1
- net.bridge.bridge-nf-call-ip6tables = 1
- 云上环境:Master 与两个 Worker 都使用 2C4G
- 本地环境:宿主机 CPU 为 E5-2676 V4,虚拟机开启绑核,并在宿主机上将对应核心调整为性能模式,Master 与 Worker 的配置分别为 4C8G 和 8C32G
- 在 Worker 之间使用 iperf3 进行网络测试,每次测试时长为 100 秒,不限制速率,客户端命令如下:
- TCP 测试:iperf3 -c x.x.x.x -t 100
- 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 的理论数值。
搭建本地环境 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,可以通过以下方法查询:
- 使用通过 kubeadm token list 命令查询 USAGES 为 authentication,signing 类型的 token,如果不存在可以手动创建 kubeadm token create –ttl 24h
- 使用 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
搭建云上环境 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 二进制文件的步骤。
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 性能还比较糟糕,感觉有很大优化空间,容器网络下就不再做并发测试了。
下面是直接路由模式下的 Calico 测试结果,测试时设置了 ipipMode 与 vxlanMode 为 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 条。
这里使用 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 条规则的限制)。
我们最好选择创建一个大的子网网段来创建节点,因为每个子网只能绑定一个路由表。
最后是新起之秀 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
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
我们可以看到:
- 当前使用 VXLAN 的 CrossSubnet 模式,同子网直接路由,跨子网 Overlay,与之前保持了一致
- Calico 使用 eBPF 代替了 kube-proxy 的功能,现在从集群外通过 NodePort 访问服务,后端 Pod 也能获取到来源 IP
- 由于使用 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))
从上述的测试结果不难看出:
- 在条件允许的情况下,直接路由模式的性能最高,再想提升性能的话,就需要使用云厂商提供的 VPC CNI 方案,实际性能取决于 SDN 优化
- 云上环境要跨子网使用直接路由模式的话,需要配置子网路由表,而路由表条目有限,限制了跨子网部署节点的数量,感觉只适合单子网场景
- 本地环境(或者物理环境)要跨子网使用直接路由模式的话,需要配置交换机转发规则,目前缺少这方面的测试条件,但我推测物理交换机也会存在类似限制
- Calico 能适应复杂的网络场景,可配置项又多又繁琐,云上的小规模集群无法展现它的优势,只有大规模集群环境和物理环境才能发挥它的能力
- Cilium 作为后起之秀,包含了 Calico 该有的所有功能,但似乎更强调可观测能力,它依赖较新版本的内核能力,落地会有一些困难
这时回顾下前司的开发工作,当时对 VPC CNI 的定义是有些不恰当,只满足了两个常见招标需求:
- 提供接近虚拟机的网络性能:这里使用 Calico 的直接路由模式,不过 VPC 内虚拟机内网互联带宽并不高,没有达到万兆,因此衰减不明显
- 支持集群外的虚拟机直接访问 Pod:这里用到了 VPC 的路由能力,会从 VPC 中预占网段,然后将节点关联的 PodCIDR 下一跳设置为节点 IP
实际上客户的需求只有第一个,容器网络性能达到或者接近虚拟机水平,第二点没有配合静态 IP 使用的话,无实际意义,每次容器重建 IP 都会发生变更。
当时网络团队给出的建议是:只调用 VPC 接口预占和释放网段接口,然后自行同步路由信息,剩余工作在 Calico 内闭环~听起来和标准 VPC CNI 方案相差甚远,没有做到网卡级别的联动,所有容器仍共享宿主机的内网网卡性能。
我认为即使将这里的 Calico 替换为 host-gw 模式的 Flannel 也没有问题,在 UCloud 的虚拟机上就测试了这种玩法,甚至将 ServiceCIDR 一起添加到路由表中,集群外同 VPC 内的虚拟机也能正常访问到服务。
此外还记起一个小问题,之前容器产品使用 Calico 的 Overlay 模式时,继续使用 ipipMode 默认值 Always 而不是 CrossSubnet,这意味着同子网节点之间也无法直接路由,性能只有三分之一。
kubelet 不直接调用 CNI 插件配置容器网络,而是在创建和删除 Pod 对应的 Sandbox 时,通过 CRI 服务间接调用 setupPodNetwork 与 teardownPodNetwork 触发 CNI 插件配置容器网络。
接下来分析下常用的网络插件架构与实现,网络插件通常至少有两个组成部分:
- node-daemon:运行在每个节点上,负责生成节点网络配置文件、配置节点网络接口、维护集群跨节点网络通信等,例如 flanneld
- cni-plugin:实现 CNI 接口的二进制文件,在 CRI 服务创建/删除沙箱时会被直接调用,如 flannel、loopback 等
复杂的网络插件还有更多组件,比如 Calico-Typha、Hubble-UI 等。

| 特性 | 支持选项 | 说明 |
|---|---|---|
| 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 关联的项目有两个:
- cni-plugin:github.com/flannel-io/cni-plugin
- 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:

如下,切换 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
首先是 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、混合云以及裸金属,最适合与物理网络联动。
关于如何选择部署选项,Calico 这一篇文档就比较有意思:Determine best networking option,下面简单解读一下。
Calico 灵活的模块化架构支持广泛的部署选项,因此可以选择适合特定环境和需求的最佳网络方法,这包括能够以非覆盖或覆盖模式(non-overlay or overlay)、带或不带 BGP 运行各种 CNI 和 IPAM 插件以及底层网络类型。
K8s 网络基础
- 每个 Pod 都有自己的 IP 地址
- 任何节点上的 Pod 都可以与所有其他节点上的所有 Pod 通信,无需 NAT
Overlay 网络
Overlay 网络是一个建立在另一个网络之上的网络,在 K8s 中,Overlay 网络可用于处理节点之间的 Pod 到 Pod 流量,底层网络无需关心 Pod 的 IP 地址或 Pod 运行在哪些节点上,两种常用于构建 Overlay 网络的协议是 VXLAN 和 IP-in-IP。
使用 Overlay 网络的优点是减少了对底层网络的依赖,例如几乎可以在任何底层网络上运行 VXLAN Overlay 网络,而主要缺点是:
- 对性能有影响,封装数据包的过程需要占用少量 CPU,并且数据包中需要额外字节来对封装进行编码
- 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,以下简称直接路由。
直接路由的优点是:
- 避免 SNAT,简化调试和操作复杂度,更容易通过来源 IP 定位问题
- 可以直接访问 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 地址可以跨越更广泛的网络路由,就像网络上任意其他工作负载一样。
最佳选项
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 网络,它几乎可以在任何环境中使用。