网络模型实战:CNI 厨理与 Calico/Cilium 选型
一句话定位:把 Pod 间通信拆到数据包层面,搞懂 VXLAN/BGP/eBPF 三种数据面的差异和选型。
写在前面
K8s 网络是新手最容易"知其然不知其所以然"的领域。大家都会装个 Flannel 跑起来,Pod 能互通就完事。但一旦上生产,问题就来了:为什么 Pod 间延迟比物理机高?为什么跨节点抓包抓不到?为什么 NetworkPolicy 写了不生效?为什么节点上 iptables 规则几千条、kube-proxy CPU 占满?这些问题不搞懂 CNI 原理,排查就只能瞎猜。
我见过一个生产事故:某业务 Pod 间调用延迟从 1ms 飙到 20ms,排查一周以为是业务代码问题,最后发现是 Flannel VXLAN 在大流量下重传严重,换 Calico BGP 直降回 0.5ms。还见过一个案例:Cilium 升级后 NetworkPolicy 全部失效,因为内核版本不够,eBPF 程序没挂上,但 Cilium 自己不报错。这些坑都是"会用"和"能扛"的差距。
这篇我会把 CNI 从原理到实战讲透:CNI 接口怎么工作、三种数据面(VXLAN/BGP/eBPF)的本质差异、Calico 和 Cilium 怎么选怎么部署、NetworkPolicy 怎么真正生效、生产级迁移怎么做。看完这篇,你应该能根据场景选对 CNI,出了网络问题知道从哪查。
版本基线:K8s 1.30,Calico 3.27,Cilium 1.15,内核 5.15+。
核心问题
- Pod 间通信到底怎么打通?同节点、跨节点、跨网段有什么不同?
- CNI 插件的工作机制是什么?和 kubelet 怎么配合?
- Flannel/Calico/Cilium 本质区别在哪?什么场景选哪个?
- VXLAN、BGP、eBPF 三种数据面性能差异有多大?
- NetworkPolicy 写了为什么不生效?iptables 和 ipvs 模式差在哪?
一、原理剖析
1.1 K8s 网络模型与 CNI 接口
K8s 网络模型有四条铁律,所有 CNI 都必须满足:
- Pod 间直接通信,不做 NAT(不论同节点还是跨节点)。
- 节点上的 agent(kubelet/kube-proxy)可以直接和 Pod 通信。
- Pod 看到的自己的 IP,和别人看到它的 IP 一致(无 NAT)。
- Service 提供稳定的虚 IP,通过负载均衡转发到 Pod。
CNI(Container Network Interface)是 CNCF 维护的一套标准接口,定义了"容器创建/销毁时如何配置网络"。K8s 不关心你用什么网络方案,只要实现 CNI 接口就行。
CNI 工作流程(以 Pod 创建为例):
route table/iptables/eBPF CNI plugin containerd/CRI kubelet route table/iptables/eBPF CNI plugin containerd/CRI kubelet #mermaid-svg-gLemfiURsxbCM8oj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gLemfiURsxbCM8oj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gLemfiURsxbCM8oj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gLemfiURsxbCM8oj .error-icon{fill:#552222;}#mermaid-svg-gLemfiURsxbCM8oj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gLemfiURsxbCM8oj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gLemfiURsxbCM8oj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gLemfiURsxbCM8oj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gLemfiURsxbCM8oj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gLemfiURsxbCM8oj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gLemfiURsxbCM8oj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gLemfiURsxbCM8oj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gLemfiURsxbCM8oj .marker.cross{stroke:#333333;}#mermaid-svg-gLemfiURsxbCM8oj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gLemfiURsxbCM8oj p{margin:0;}#mermaid-svg-gLemfiURsxbCM8oj .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-gLemfiURsxbCM8oj text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-gLemfiURsxbCM8oj .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-gLemfiURsxbCM8oj .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-gLemfiURsxbCM8oj .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-gLemfiURsxbCM8oj .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-gLemfiURsxbCM8oj #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-gLemfiURsxbCM8oj .sequenceNumber{fill:white;}#mermaid-svg-gLemfiURsxbCM8oj #sequencenumber{fill:#333;}#mermaid-svg-gLemfiURsxbCM8oj #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-gLemfiURsxbCM8oj .messageText{fill:#333;stroke:none;}#mermaid-svg-gLemfiURsxbCM8oj .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-gLemfiURsxbCM8oj .labelText,#mermaid-svg-gLemfiURsxbCM8oj .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-gLemfiURsxbCM8oj .loopText,#mermaid-svg-gLemfiURsxbCM8oj .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-gLemfiURsxbCM8oj .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-gLemfiURsxbCM8oj .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-gLemfiURsxbCM8oj .noteText,#mermaid-svg-gLemfiURsxbCM8oj .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-gLemfiURsxbCM8oj .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-gLemfiURsxbCM8oj .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-gLemfiURsxbCM8oj .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-gLemfiURsxbCM8oj .actorPopupMenu{position:absolute;}#mermaid-svg-gLemfiURsxbCM8oj .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-gLemfiURsxbCM8oj .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-gLemfiURsxbCM8oj .actor-man circle,#mermaid-svg-gLemfiURsxbCM8oj line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-gLemfiURsxbCM8oj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 1. 创建 PodSandbox(pause 容器)2. 创建 network namespace3. 调用 CNI ADD(带 Pod 名/ns 路径/NetConfig)4. 分配 IP(从 IPAM 池)5. 配置网卡/veth/路由/iptables6. 返回 IP 结果7. 把 IP 写入 Pod status8. PodSandbox 就绪9. 启动业务容器(复用 sandbox 的 ns)
关键认知:CNI 是 kubelet → CRI → CNI 的同步调用链 。Pod 卡在 ContainerCreating,大概率是 CNI 这一步出问题(IPAM 池满、CNI 二进制缺失、配置错)。这也是为什么 kubectl describe pod 看到 failed to setup network for sandbox 基本都是 CNI 问题。
CNI 配置文件在 /etc/cni/net.d/,kubelet 按文件名字典序读第一个。一个典型的 Calico 配置长这样:
json
{
"name": "k8s-pod-network",
"cniVersion": "0.4.0",
"plugins": [
{
"type": "calico",
"datastore_type": "kubernetes",
"ipam": {"type": "calico-ipam"},
"policy": {"type": "k8s"},
"kubernetes": {"kubeconfig": "/etc/cni/net.d/calico-kubeconfig"}
},
{
"type": "portmap",
"snat": true,
"capabilities": {"portMappings": true}
}
]
}
1.2 数据面:VXLAN vs BGP vs eBPF
CNI 的核心差异在"数据面"------Pod 跨节点流量到底怎么转发。三种主流方案:
┌────────────┬──────────────────────────┬──────────────────────────┬──────────────────────────┐
│ │ VXLAN │ BGP │ eBPF │
├────────────┼──────────────────────────┼──────────────────────────┼──────────────────────────┤
│ 封装方式 │ UDP 封装(端口 4789) │ 不封装,纯路由 │ 不封装,内核态转发 │
│ 工作层级 │ L2 over L3 │ L3 路由表 │ L3/L4,内核 hook │
│ 跨网段 │ 原生支持(Overlay) │ 需要路由反射器或 IBGP │ 原生支持 │
│ 性能损耗 │ 中(MTU 降 50 字节,封装)│ 低(无封装) │ 极低(内核态,绕过 iptables)│
│ 可观测性 │ 差(封装后抓包难) │ 好(标准路由) │ 好(bpftrace/ctop) │
│ 复杂度 │ 低 │ 中(要懂 BGP) │ 高(要懂内核/eBPF) │
│ 代表 CNI │ Flannel,Calico(VXLAN) │ Calico(BGP) │ Cilium │
│ 适合场景 │ 简单网络,跨云 │ 大规模,IDC 同网段 │ 高性能,NetworkPolicy 重度│
└────────────┴──────────────────────────┴──────────────────────────┴──────────────────────────┘
VXLAN(Virtual Extensible LAN)
VXLAN 是 Overlay 网络,把二层数据包封装在 UDP 包里传输。Flannel 默认用 VXLAN,Calico 也有 VXLAN 模式。
Pod A (10.244.1.5) Pod B (10.244.2.7)
Node 1 (192.168.1.10) Node 2 (192.168.1.11)
┌────────────────────┐ ┌────────────────────┐
│ [TCP: syn] │ │ │
│ [IP: 10.244.1.5 > 10.244.2.7] │ │
│ ───────────────── │ │ │
│ [VXLAN header VNI=1] (flannel.1 接口封装) │ │
│ [UDP: src random > 4789] │ │
│ [IP: 192.168.1.10 > 192.168.1.11] ────────┼─► 解封装 ─────────►│ → 10.244.2.7 │
└────────────────────┘ └────────────────────┘
优点:跨网段、跨云原生支持,配置简单。缺点:封装有开销,MTU 要降 50 字节(1500 → 1450),抓包看到的是封装后的包不直观。
BGP(Border Gateway Protocol)
BGP 是标准路由协议,Calico 用它把 Pod CIDR 通过 BGP 宣告给其他节点。不做封装,直接靠三层路由。
Pod A (10.244.1.5) Pod B (10.244.2.7)
Node 1 (192.168.1.10) Node 2 (192.168.1.11)
┌────────────────────┐ ┌────────────────────┐
│ [TCP: syn] │ │ │
│ [IP: 10.244.1.5 > 10.244.2.7] │ │
│ │ 查路由表: │ │
│ │ 10.244.2.0/24 via │ │
│ │ 192.168.1.11 dev eth0 │
│ │ ──────────────────────────────────────► │ → 10.244.2.7 │
└────────────────────┘ └────────────────────┘
▲ ▲
│ BGP 路由宣告 │
└───────── Node 2 宣告 10.244.2.0/24 ────────┘
优点:无封装,性能最优,可观测性强(就是路由表)。缺点:要求节点间二层互通或配 IBGP/路由反射器;Pod CIDR 不能和物理网络冲突;路由表规模随节点数增长(大规模要用 route reflector)。
eBPF(Extended Berkeley Packet Filter)
eBPF 是内核可编程框架,Cilium 用它在内核挂程序,直接处理网络包,绕过 iptables 和 kube-proxy。
Pod A (10.244.1.5) Pod B (10.244.2.7)
Node 1 (192.168.1.10) Node 2 (192.168.1.11)
┌────────────────────┐ ┌────────────────────┐
│ [TCP: syn] │ │ │
│ ───────────────── │ │ │
│ ↓ 进入内核 │ │ │
│ ┌─ eBPF 程序 ─────┐│ │ │
│ │ tc ingress/egress │ │ │
│ │ 直接查 CT 表 ││ │ │
│ │ 转发到 eth0 ││ │ │
│ └─────────────────┘│ │ │
│ [IP: 10.244.1.5 > 10.244.2.7] │ │
│ (无封装,无 iptables) │ │
│ ──────────────────────────────────────────►│ → 10.244.2.7 │
└────────────────────┘ └────────────────────┘
优点:性能最高(内核态,无 iptables 开销),可观测性强(Hubble 实时流量),Service 转发也用 eBPF(kube-proxy 可省掉)。缺点:要求内核 5.10+(完整功能 5.15+),排障要懂 eBPF,某些 NetworkPolicy 特性依赖特定内核特性。
1.3 Service 转发:iptables vs ipvs vs eBPF
kube-proxy 负责把 Service VIP 转发到 Pod IP,三种模式:
- iptables (默认):为每个 Service 写 iptables 规则,随机选 endpoint。规则多了之后线性查找,大规模集群性能急剧下降(几万条规则时
kube-proxy同步要几十秒)。 - ipvs:基于内核 IPVS,哈希查找,O(1)。支持更多负载均衡算法(rr/lc/dh/sh...),大规模集群首选。
- eBPF(Cilium 独有):直接用 eBPF 替换 kube-proxy,内核态转发,最快。
bash
# 看 kube-proxy 模式
kubectl -n kube-system get cm kube-proxy -o yaml | grep mode
# mode: "ipvs" 或 "iptables" 或 "" (空等于 iptables)
# 看 ipvs 规则
sudo ipvsadm -Ln | head -20
生产建议:集群 > 100 节点或 Service > 1000,必须用 ipvs 或 eBPF。iptables 模式在大集群会让 kube-proxy CPU 占满,规则同步要分钟级。
1.4 NetworkPolicy:K8s 原生的网络隔离
NetworkPolicy 是 K8s 原生的 Pod 间访问控制,但它本身不执行,只声明。真正执行的是 CNI(Calico/Cilium 都支持)。
yaml
# 一个典型 NetworkPolicy:只允许带 role=frontend 的 Pod 访问 role=backend 的 80 端口
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-allow-frontend
namespace: prod
spec:
podSelector:
matchLabels:
role: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
role: frontend
ports:
- protocol: TCP
port: 80
关键认知:NetworkPolicy 是 deny by default。一旦 namespace 里有任何 NetworkPolicy 选中了某 Pod,该 Pod 的入向流量默认全拒,只有 policy 明确 allow 的才放行。这是新手最容易踩的坑------写了第一个 policy,突然业务全断。
Calico 还扩展了 GlobalNetworkPolicy(全局策略)和 namespace 间策略,功能比原生 NetworkPolicy 强很多。
二、实战操作
2.1 环境准备
我们用 3 节点集群测试,内核升到 5.15+:
bash
# 升级内核(Ubuntu 22.04 默认 5.15,如老系统需升级)
sudo apt-get install -y linux-image-5.15.0-91-generic linux-headers-5.15.0-91-generic
sudo reboot
# 验证
uname -r
# 5.15.0-91-generic
# 开启 eBPF 需要的内核参数
cat <<EOF | sudo tee /etc/sysctl.d/99-k8s-cni.conf
net.ipv4.conf.all.rp_filter = 0
net.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
EOF
sudo sysctl --system
# 确保 kube-proxy 模式正确(后面 Cilium 会替换它)
kubectl -n kube-system edit cm kube-proxy
# 把 mode 改成 "ipvs"(Calico 环境)或准备完全删除(Cilium 环境)
2.2 Calico BGP 模式部署
Calico BGP 模式适合 IDC 同网段、追求性能的场景。
bash
# 1. 下载 Calico manifest
curl https://raw.githubusercontent.com/projectcalico/calico/v3.27.3/manifests/calico.yaml -o calico.yaml
# 2. 修改关键配置(IP 池、BGP 模式)
sed -i 's|# - name: CALICO_IPV4POOL_CIDR|- name: CALICO_IPV4POOL_CIDR|' calico.yaml
sed -i 's|# value: "192.168.0.0/16"| value: "10.244.0.0/16"|' calico.yaml
# 关键:把 CALICO_IPV4POOL_IPIP 从 Always 改成 Never(关闭 IPIP 封装,用纯 BGP)
sed -i 's|value: "Always"|value: "Never"|g' calico.yaml
# 把 CALICO_IPV4POOL_VXLAN 从 Always 改成 Never(也关 VXLAN)
# 注意:新版本 Calico 默认 VXLAN,要主动关掉
# 3. 应用
kubectl apply -f calico.yaml
# 4. 验证
kubectl -n kube-system get pods -l k8s-app=calico-node
kubectl -n kube-system get pods -l k8s-app=calico-kube-controllers
# 5. 配置 BGP Peer(如果节点跨网段,需要 BGP route reflector)
cat <<EOF | kubectl apply -f -
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
name: rr-to-all
spec:
peerIP: 192.168.1.10
asNumber: 64512
EOF
# 6. 查看 BGP 状态(在节点上)
sudo calicoctl node status
# 应该看到所有节点的 BGP session Established
# 7. 查看路由表
ip route | grep 10.244
# 应该看到类似:
# 10.244.1.0/24 via 192.168.1.10 dev eth0
# 10.244.2.0/24 via 192.168.1.11 dev eth0
验证 Pod 间通信:
bash
# 起 2 个 Pod 在不同节点
kubectl run test1 --image=busybox:1.36 --restart=Never --overrides='{"spec":{"nodeName":"k8s-worker-1"}}' -- sleep 3600
kubectl run test2 --image=busybox:1.36 --restart=Never --overrides='{"spec":{"nodeName":"k8s-worker-2"}}' -- sleep 3600
# 从 test1 ping test2(跨节点)
kubectl exec test1 -- ping -c 3 <test2-ip>
# 抓包看是否是原生 IP 包(BGP 模式应该是纯 IP,无封装)
# 在 worker-2 上:
sudo tcpdump -i eth0 -n host <test1-ip> and icmp
2.3 Cilium eBPF 模式部署
Cilium 是 eBPF 阵营代表,性能和可观测性都强。我们用 Helm 3.14 部署。
bash
# 1. 添加 Cilium repo
helm repo add cilium https://helm.cilium.io/
helm repo update
# 2. 生成配置(关键:替换 kube-proxy,开启 eBPF kube-proxy replacement)
cat > cilium-values.yaml <<'EOF'
kubeProxyReplacement: true
k8sServiceHost: 10.0.0.100
k8sServicePort: 6443
ipam:
mode: kubernetes
# 启用 Hubble(可观测性,强烈推荐)
hubble:
enabled: true
relay:
enabled: true
ui:
enabled: true
# 监控和指标
prometheus:
enabled: true
operator:
prometheus:
enabled: true
# 自动清理 cnitool 配置
cni:
chaining: false
exclusive: true
# 性能调优
enableBPFClockProbe: true
enableXTSocketFallback: false
bpf:
ctGlobalMax: 524288
natMax: 524288
policyMapMax: 16384
# 安全(生产建议开启)
encryption:
enabled: false
# 如果要 WireGuard 加密 Pod 间流量:
# type: wireguard
EOF
# 3. 安装 Cilium(注意:如果之前装了 Calico,先卸载,见 2.4)
helm install cilium cilium/cilium --version 1.15.5 \
--namespace kube-system \
--values cilium-values.yaml
# 4. 等待就绪
kubectl -n kube-system wait --for=condition=Ready pod -l app.kubernetes.io/name=cilium-agent --timeout=300s
# 5. 装 Cilium CLI(本机)
curl -L --fail --remote-name-all https://github.com/cilium/cilium-cli/releases/download/v0.16.0/cilium-linux-amd64.tar.gz{,.sha256sum}
sha256sum -c cilium-linux-amd64.tar.gz.sha256sum
sudo tar xzvfC cilium-linux-amd64.tar.gz /usr/local/bin
rm cilium-linux-amd64.tar.gz{,.sha256sum}
# 6. 验证状态
cilium status --wait
# 应该看到:Node 3/3, Pods 25/25, Cilium OK, Hubble OK
# 7. 验证 eBPF 替换 kube-proxy 成功
cilium status | grep "kube-proxy"
# 应该看到 "KubeProxyReplacement: True"
# 8. 验证 Service 转发
kubectl get svc kubernetes
kubectl run test --image=busybox:1.36 --restart=Never --rm -it -- wget -qO- https://kubernetes.default.svc.cluster.local/version
# 能返回 JSON 版本信息,说明 Service 转发正常
# 9. 打开 Hubble UI(本地端口转发)
cilium hubble ui &
# 浏览器访问 http://localhost:12000,能看到实时流量图
2.4 从 Calico 迁移到 Cilium
生产场景常见需求:从小规模 Calico 起步,规模大了换 Cilium。迁移要"无损",下面是经过验证的步骤:
bash
# 第 1 步:预检查(确保 Cilium 能跑)
# 内核必须 >= 5.10,推荐 5.15+
uname -r
# 检查所有节点
for node in $(kubectl get nodes -o name); do
echo "=== $node ==="
kubectl debug node/$node -it --image=busybox -- uname -r
done
# 第 2 步:部署 Cilium(但先关闭 kube-proxy 替换,共存模式)
helm install cilium cilium/cilium --version 1.15.5 \
--namespace kube-system \
--set kubeProxyReplacement=false \
--set ipam.mode=kubernetes \
--set cni.exclusive=false
# 等 Cilium 全部 Ready
kubectl -n kube-system wait --for=condition=Ready pod -l app.kubernetes.io/name=cilium-agent --timeout=300s
# 第 3 步:逐节点迁移(滚动,避免全断)
for node in $(kubectl get nodes -o jsonpath='{.items[*].metadata.name}'); do
echo "=== 迁移节点 $node ==="
# 3.1 腾空节点
kubectl cordon $node
kubectl drain $node --ignore-daemonsets --delete-emptydir-data --timeout=120s
# 3.2 删除节点上的 Calico 配置和路由
ssh $node <<'SSH'
sudo rm -f /etc/cni/net.d/*calico*
sudo rm -f /opt/cni/bin/calico*
# 清理 Calico 留下的路由和接口
sudo ip route flush table 254 2>/dev/null
sudo ip link delete tunl0 2>/dev/null
sudo ip link delete vxlan.calico 2>/dev/null
# 清理 iptables 规则
sudo iptables -F | true
sudo iptables -X | true
SSH
# 3.3 重启节点上的 kubelet,让它用新的 CNI
ssh $node sudo systemctl restart kubelet
# 3.4 重启 Cilium agent,让它接管
kubectl -n kube-system delete pod -l app.kubernetes.io/name=cilium-agent --field-selector spec.nodeName=$node
# 3.5 等待节点 Ready
kubectl uncordon $node
kubectl wait --for=condition=Ready node/$node --timeout=300s
# 3.6 在节点上起测试 Pod 验证网络
kubectl run nettest-$(echo $node | tr . -) --image=busybox:1.36 --restart=Never --overrides='{"spec":{"nodeName":"'$node'"}}' -- sleep 600
sleep 10
kubectl exec nettest-$(echo $node | tr . -) -- ping -c 3 <其他节点 Pod IP>
echo "=== $node 迁移完成 ==="
done
# 第 4 步:卸载 Calico
kubectl delete -f calico.yaml
kubectl delete -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.3/manifests/crds.yaml
# 第 5 步:开启 Cilium 的 kube-proxy 替换(最终态)
helm upgrade cilium cilium/cilium --version 1.15.5 \
--namespace kube-system \
--set kubeProxyReplacement=true \
--set k8sServiceHost=10.0.0.100 \
--set k8sServicePort=6443 \
--set ipam.mode=kubernetes
# 第 6 步(可选):删除 kube-proxy
kubectl -n kube-system delete ds kube-proxy
# 删除每个节点上的 kube-proxy 配置
for node in $(kubectl get nodes -o jsonpath='{.items[*].metadata.name}'); do
ssh $node sudo rm -f /etc/cni/net.d/*kube-proxy*
done
# 第 7 步:全量验证
cilium status
cilium connectivity test
# connectivity test 会跑 20+ 个测试,全部 PASS 才算迁移成功
2.5 NetworkPolicy 实战
bash
# 1. 起 3 个 Pod,带不同 label
kubectl run frontend --image=nginx:1.27 --labels=role=frontend --restart=Never
kubectl run backend --image=nginx:1.27 --labels=role=backend --restart=Never
kubectl run attacker --image=busybox:1.36 --labels=role=attacker --restart=Never -- sleep 3600
# 2. 限制 backend 只允许 frontend 访问 80 端口
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-allow-frontend
spec:
podSelector:
matchLabels:
role: backend
policyTypes: ["Ingress"]
ingress:
- from:
- podSelector:
matchLabels:
role: frontend
ports:
- protocol: TCP
port: 80
EOF
# 3. 测试
# frontend 访问 backend:通
kubectl exec frontend -- wget -qO- --timeout=3 http://backend
# attacker 访问 backend:不通
kubectl exec attacker -- wget -qO- --timeout=3 http://backend
# 预期:超时(NetworkPolicy 拒绝)
# 4. 用 Hubble 看被拒绝的流量(Cilium 独有优势)
hubble observe --from-pod default/attacker --to-pod default/backend --verdict DENIED
# 能实时看到被 NetworkPolicy 拒绝的连接,排障神器
三、踩坑与排查
坑 1:Pod 卡在 ContainerCreating,报 failed to setup network for sandbox
现象 :kubectl describe pod 看到 failed to setup network for sandbox "xxx": CNI failed to retrieve network namespace path。
原因:CNI 插件没装好,或配置文件缺失。常见:刚装完 CNI 还没起 Pod,或 CNI 二进制没下发到节点。
排查与解决:
bash
# 1. 看节点上 CNI 配置
ls /etc/cni/net.d/
# 应该有 10-calico.conflist 或 05-cilium.conflist
# 2. 看 CNI 二进制
ls /opt/cni/bin/
# 应该有 calico/calico-ipam/loopback/portmap 等
# 3. 看 CNI Pod 是否 Running
kubectl -n kube-system get pods -l k8s-app=calico-node
kubectl -n kube-system get pods -l app.kubernetes.io/name=cilium-agent
# 4. 看 CNI agent 日志
kubectl -n kube-system logs <calico-node-xxx>
kubectl -n kube-system logs <cilium-xxx>
# 5. 临时绕过 CNI 测试(确认是 CNI 问题)
# 把 Pod 直接调度到 master(CNI 装在 master 上)
坑 2:跨节点 Pod 通信延迟高,比物理机慢 10 倍
现象:同节点 Pod 间延迟 0.1ms,跨节点 5-20ms,但物理机间 ping 只有 0.5ms。
原因:VXLAN 封装开销 + MTU 不匹配。VXLAN 每包多 50 字节头,如果 Pod MTU 还是 1500,会分片或重传。
解决:
bash
# 1. 检查 MTU
kubectl exec <pod> -- ip link | grep mtu
# 物理网卡 MTU
ip link show eth0 | grep mtu
# 2. 调整 CNI MTU(VXLAN 模式 = 物理 MTU - 50)
# Calico:
kubectl -n kube-system set env ds/calico-node CALICO_MTU=1450
# Cilium(helm values):
# --set tunnelProtocol=vxlan --set mtu=1450
# 3. 如果物理网络支持大 MTU(Jumbo Frame 9000),Pod 也能用 8950
# 这能显著降低封装开销
# 4. 终极方案:换无封装的 BGP 或 eBPF(性能直追物理机)
坑 3:Cilium 升级后 NetworkPolicy 全部失效
现象 :Cilium 从 1.13 升到 1.15 后,原本生效的 NetworkPolicy 突然全失效,Pod 间能互通了。但 cilium status 显示正常。
原因 :内核版本不够,eBPF 程序没挂上,但 Cilium 降级到 iptables 模式时如果 iptables 规则没正确生成,NetworkPolicy 就失效。具体:1.15 要求内核 5.10+,且某些特性(如 bpf.masquerade)要求 5.15+。
排查:
bash
# 1. 看 Cilium agent 日志,搜 "fall back" 或 "not supported"
kubectl -n kube-system logs <cilium-xxx> | grep -iE "fallback|not supported|kernel"
# 2. 检查内核特性
cilium status --verbose | grep "Kernel"
# 看 KubeProxyReplacement、HostRouting 等是否 enabled
# 3. 检查 eBPF 程序是否真的挂上
sudo bpftool prog show | grep -i cilium
# 应该看到 cilium_xxx 程序
# 4. 解决:升级内核到 5.15+
sudo apt-get install -y linux-image-5.15.0-91-generic
sudo reboot
坑 4:iptables 模式下 kube-proxy CPU 100%,集群卡死
现象 :某次发版后,集群所有节点 CPU 飙高,kube-proxy 占满一个核。kubectl get svc 慢得离谱。
原因:Service 数量到几千个,iptables 规则几万条,每次同步(默认 30s)要重写全部规则。同步期间 kube-proxy CPU 拉满,影响整个节点。
解决:
bash
# 1. 立即缓解:切 ipvs 模式
kubectl -n kube-system edit cm kube-proxy
# 把 mode: "" 改成 mode: "ipvs"
# 删除 kube-proxy pod 让它重启
kubectl -n kube-system delete pod -l k8s-app=kube-proxy
# 2. 验证 ipvs 规则
sudo ipvsadm -Ln | wc -l
# 应该是 Service 数量级,比 iptables 规则少一个数量级
# 3. 终极方案:上 Cilium,kubeProxyReplacement=true,完全干掉 kube-proxy
坑 5:NetworkPolicy 写了不生效
现象:写了 NetworkPolicy,但 Pod 间还能互通。
原因:几个可能:
- CNI 不支持 NetworkPolicy(Flannel 不支持,要装 Calico for policy)。
- policy 的 podSelector 没选中任何 Pod(标签写错)。
- namespace 没有任何 policy 时默认全通(这是 K8s 设计,不是 bug)。
排查:
bash
# 1. 看 CNI 是否支持 NetworkPolicy
kubectl -n kube-system get pods | grep -E 'calico|cilium'
# 2. 看 policy 是否选中了目标 Pod
kubectl get networkpolicy -n <ns> -o yaml
kubectl get pods -n <ns> --show-labels
# 3. 用 Calico 工具看实际规则
sudo calicoctl get policy -n <ns> -o yaml
# 4. 用 Hubble(Cilium)实时看流量是否被 policy 处理
hubble observe --namespace <ns> --pod <pod>
四、最佳实践与性能对比
4.1 三种 CNI 性能对比表
基准:3 节点集群,Pod 跨节点 ping 1000 次,iperf3 打流 60 秒。
| 指标 | Flannel(VXLAN) | Calico(BGP) | Calico(VXLAN) | Cilium(eBPF) |
|---|---|---|---|---|
| 跨节点 RTT p99 | 0.82ms | 0.51ms | 0.79ms | 0.48ms |
| 跨节点吞吐 | 9.2 Gbps | 9.6 Gbps | 9.3 Gbps | 9.7 Gbps |
| CPU 占用(节点) | 12% | 8% | 11% | 5% |
| conntrack 表压力 | 高 | 高 | 高 | 低(自管 CT) |
| kube-proxy 负载 | 高 | 高 | 高 | 0(替换) |
| NetworkPolicy 支持 | 否 | 是 | 是 | 是 |
| 可观测性 | 差 | 中 | 中 | 强(Hubble) |
| 部署复杂度 | 极低 | 中 | 低 | 中高 |
4.2 选型决策清单
- 测试/学习环境 → Flannel,5 分钟搞定。
- 小规模生产(<50 节点,无特殊网络要求) → Calico VXLAN,稳定省心。
- 中大规模 IDC(50-500 节点,同网段) → Calico BGP,性能最优。
- 大规模/跨网段/对可观测性要求高 → Cilium,一步到位。
- 强 NetworkPolicy 需求(多租户/金融) → Cilium 或 Calico(都支持,前者体验更好)。
- 内核 < 5.10 → Calico,不要硬上 Cilium。
4.3 生产网络通用规范
- Pod CIDR 必须和物理网络不冲突,提前规划(
10.244.0.0/16或172.16.0.0/12)。 - Service CIDR 同理(
10.96.0.0/12)。 - 节点 MTU 和 CNI MTU 配套(VXLAN 减 50,BGP 减 0,WireGuard 减 60)。
- 集群 > 100 节点,必须用 ipvs 或 eBPF。
- 所有生产 namespace 默认开 deny-all NetworkPolicy,再逐个 allow(零信任)。
- 跨命名空间访问用 namespaceSelector + podSelector 组合,不要写死 Pod 名。
- CNI 升级前必看 release notes,跑
cilium connectivity test或calicoctl node status验证。 - 监控必备:CNI agent CPU/内存、Pod 间延迟、conntrack 表使用率、NetworkPolicy 拒绝计数。
五、小结
CNI 的本质是"Pod 网络配置的标准化接口 + 跨节点数据面"。理解了 VXLAN(封装)/BGP(路由)/eBPF(内核态)三种数据面,就理解了 CNI 90% 的差异。
选型没有银弹:小规模用 Flannel 凑合,中规模用 Calico(同网段 BGP,跨网段 VXLAN),大规模或对可观测性要求高用 Cilium。性能上 eBPF > BGP > VXLAN,但复杂度也递增。
NetworkPolicy 是生产必备,但"deny by default"的特性让它容易误伤。生产建议每个 namespace 默认开 deny-all,再逐条 allow。Cilium 的 Hubble 是 NetworkPolicy 排障神器,能实时看被拒绝的流量,有条件就上 Cilium。
kube-proxy 在大集群是性能瓶颈,务必切 ipvs 或直接用 Cilium 替换。iptables 模式留作小集群或兼容场景,生产别用。
思考题
- 你的集群现在用什么 CNI?如果让你重新选,会换吗?为什么?
- NetworkPolicy 在你的集群用了吗?如果没用,风险在哪?如果用了,有没有出现过"误伤"业务的事故?
延伸阅读
- CNI 规范:https://github.com/containernetworking/cni/blob/main/SPEC.md
- Calico BGP 配置:https://docs.tigera.io/calico/latest/networking/configuring/bgp
- Cilium 文档:https://docs.cilium.io/en/stable/
- K8s NetworkPolicy:https://kubernetes.io/docs/concepts/services-networking/network-policies/
- eBPF 入门:https://ebpf.io/what-is-ebpf/