CNI 网络:Calico 与 Cilium¶
kubeadm 只负责把节点拉起来,真正让 Pod 拿到地址、跨节点互通的是 CNI。本章拆开 CNI 的调用链,对比 Flannel、Calico、Cilium 三种数据平面,然后用 cilium install 部署一套无 kube-proxy 的 eBPF 网络,最后把 Calico 的 full-mesh 改造成 Route Reflector 架构。
你将学到¶
- CNI 规范的调用时机与
/etc/cni/net.d的作用,Flannel、Calico、Cilium 的数据平面差异与选型指标。 - Calico 的 IPIP、VXLAN、BGP 三种转发模式,full-mesh 为什么会在大规模集群失效。
- 用 BGPConfiguration 与 BGPPeer 把 master 改造成 Route Reflector,并关闭 node-to-node mesh。
- Cilium 的 eBPF、kube-proxy replacement、tunnel/native 路由、Hubble,以及两种插件下 NetworkPolicy 的实现差异。
为什么需要它¶
Kubernetes 只规定 Pod 网络必须满足三条:Pod 之间可以直接用 IP 通信且不经 NAT、节点访问 Pod 不经 NAT、Pod 看到的自己的 IP 就是别人看到的 IP。至于怎么实现,全部交给 CNI 插件。没有装 CNI 时,节点会停在 NotReady,Reason 是 NetworkPluginNotReady,这是新集群最常见的第一道坎。
| 维度 | Flannel | Calico | Cilium |
|---|---|---|---|
| 数据平面 | 内核 VXLAN / host-gw | iptables 或 eBPF | eBPF(TC/XDP) |
| 封装模式 | VXLAN、host-gw | IPIP、VXLAN、BGP 无封装 | VXLAN、Geneve、native routing |
| 网络策略 | 不支持(需另装) | NetworkPolicy、GlobalNetworkPolicy | NetworkPolicy、CiliumNetworkPolicy(L7/DNS) |
| 服务代理 | 依赖 kube-proxy | 默认依赖 kube-proxy,eBPF 模式可替代 | 可完全替换 kube-proxy |
| 可观测性 | 弱 | 中等(flow log) | 强(Hubble 逐流) |
| 典型场景 | 小集群、教学 | 传统 IDC、需要 BGP 对接 | 高性能、云原生、需要 L7 策略 |
核心概念¶
Pod 网络模型与 CNI 规范¶
CNI 是一个很薄的契约:容器运行时创建 network namespace 之后,按顺序执行 /etc/cni/net.d/ 下的 *.conflist 中列出的插件,把网络配置以 JSON 从 stdin 传给插件二进制(位于 /opt/cni/bin),插件完成网卡、IP、路由配置后把结果以 JSON 从 stdout 返回。核心动作只有 ADD、DEL、CHECK,VERSION 用于协商规范版本。
一个常见的 calico conflist 会把 calico 与 portmap 串成链:前者负责分配 IP 与写路由,后者负责 hostPort 的 DNAT。Cilium 则只注册一个 cilium-cni,把大部分工作交给常驻的 agent,插件本身只是一次 gRPC 调用。排查 CNI 问题时,ls /etc/cni/net.d/ 与 crictl info 比看 Pod 事件更快。
v1.31 的运行时前提
v1.24 起 dockershim 已移除,kubelet 通过 CRI 调用 containerd 或 CRI-O,CNI 配置由运行时读取。仓库 README 的 kubeadm 示例写的是 v1.29.2,而 ansible-k8s-install/install-k8s-*.yaml 里的变量已经到 v1.32.8;本书统一按 v1.31 讲,CNI 接口与这些版本完全兼容,差异只在镜像 tag。
Calico 的三条数据路径¶
Calico 把 Pod 地址池建模成 IPPool,仓库里的 default-ipv4-ippool.yaml 同时给出了两个开关:ipipMode: Never 与 vxlanMode: Always,也就是走 VXLAN 封装;blockSize: 24 表示每个节点从池中切一个 /24;natOutgoing: true 让 Pod 访问集群外时做 SNAT。
- IPIP:把三层包再包一层 IP,跨网段时
CrossSubnet只在不同子网间封装,同子网直接路由。开销比 VXLAN 小,但部分云网络会丢弃 IPIP 协议号 4。 - VXLAN:UDP 4789 封装,能穿过绝大多数网络,是跨子网最稳的选择,代价是 MTU 要减 50。
- BGP:不封装,节点把 Pod 网段通过 BGP 通告给交换机或其它节点,纯三层转发,性能最好,但要求底层网络可控。
默认情况下 Calico 在节点间建立 full-mesh:每个节点与其它所有节点建 iBGP 会话,ASN 默认 64512。N 个节点需要 N×(N-1)/2 条会话,100 节点就是 4950 条,会话震荡和收敛时间都不可接受。规模上去后标准做法是选几个节点做 Route Reflector(RR),普通节点只与 RR 建会话,RR 之间再互相同步。
flowchart LR
A["Pod A 发出报文"] --> B["veth 进入宿主机"]
B --> C{"同节点?"}
C -->|是| D["查路由直接转发到 veth"]
C -->|否| E{"封装模式"}
E -->|IPIP 或 VXLAN| F["隧道到达对端节点"]
E -->|BGP| G["按 BGP 学到的下一跳转发"]
F --> H["对端 veth"]
G --> H
D --> H
H --> I["Pod B"]
图:一个 Pod 到另一个 Pod 的报文在 Calico 中的三条可能路径,分叉点是同节点判断与封装模式。
Cilium 的 eBPF 数据平面¶
Cilium 不用 iptables,而是把 service 负载均衡、策略、NAT、可见性都编译成 eBPF 程序,挂在 socket、TC ingress/egress 或 XDP 钩子上。因为钩子点更靠前,它可以在 connect() 系统调用返回前就完成 Service 的 DNAT,省掉一条经过协议栈的路径。
kubeProxyReplacement=strict 表示把所有 Service 处理都交给 Cilium,此时集群里不应再有 kube-proxy,kubeadm init 必须加 --skip-phases=addon/kube-proxy。routingMode=tunnel 是默认值,用 VXLAN 封装;routingMode=native 关闭隧道,要求底层网络能路由 Pod 网段,因此要同时设置 ipv4NativeRoutingCIDR,并常用 autoDirectNodeRoutes=true 自动写对端路由。
IPAM 有两种:ipam.mode=kubernetes 直接使用 kube-controller-manager 分配给节点的 PodCIDR;cluster-pool 由 operator 从 clusterPoolIPv4PodCIDRList 里给每个节点切块。仓库示例统一用前者,因为 kubeadm init --pod-network-cidr=10.244.0.0/16 已经分配好了。
仓库里的大小写坑
Cilium/README.md 的示例写的是 --set ipam.Operator.ClusterPoolIPv4MaskSize=24,Helm values 的键名实际是小写的 ipam.operator.clusterPoolIPv4MaskSize。Helm 对大小写敏感,照抄不会报错但不会生效,按本节命令写。
NetworkPolicy 的实现差异¶
Calico 的 Felix 把策略翻译成 iptables 或 eBPF 规则,挂在 veth 的主机侧,除 Kubernetes NetworkPolicy 外还支持 GlobalNetworkPolicy 与 order 排序。Cilium 基于 identity 匹配,在 eBPF 里执行,除 L3/L4 外还能写 L7 HTTP/gRPC 与 DNS 规则。两者默认都是全放通,只有被策略选中的 Pod 才受限。
动手实验¶
实验一:用 cilium install 部署无 kube-proxy 的集群
目标:在 v1.31 集群上以 VXLAN + Kubernetes IPAM 部署 Cilium,开启 kube-proxy replacement 与 Hubble,并能逐流观察。
步骤 1 · 初始化时跳过 kube-proxy
sudo kubeadm init \
--control-plane-endpoint=kubeapi.magedu.com \
--kubernetes-version=v1.31.0 \
--pod-network-cidr=10.244.0.0/16 \
--image-repository=registry.aliyuncs.com/google_containers \
--skip-phases=addon/kube-proxy
加入工作节点后先不要执行 kubectl apply 任何 CNI。此时节点是 NotReady,符合预期。
步骤 2 · 安装 Cilium
cilium install \
--version v1.15.6 \
--namespace kube-system \
--set k8sServiceHost=kubeapi.magedu.com \
--set k8sServicePort=6443 \
--set kubeProxyReplacement=strict \
--set ipam.mode=kubernetes \
--set routingMode=tunnel \
--set tunnelProtocol=vxlan \
--set ipam.operator.clusterPoolIPv4PodCIDRList=10.244.0.0/16 \
--set ipam.operator.clusterPoolIPv4MaskSize=24 \
--set hubble.enabled=true \
--set hubble.relay.enabled=true \
--set hubble.ui.enabled=true
--version 对齐仓库 playbook 中的 cilium_version: v1.15.6。k8sServiceHost 必须填控制面可达地址,否则 agent 起不来。若用 routingMode=native,追加 --set ipv4NativeRoutingCIDR=10.244.0.0/16 --set autoDirectNodeRoutes=true,并确认节点间能直路由该网段。ipam.mode=kubernetes 时 clusterPool 参数不会生效,地址仍由 kubeadm 分配的 PodCIDR 决定。
步骤 3 · 检查状态
预期输出(版本与 Pod 哈希随环境变化):
Cilium: OK
Operator: OK
Hubble Relay: OK
Cluster health: 4/4 reachable
NAME READY STATUS RESTARTS AGE
cilium-2x7kq 1/1 Running 0 3m
步骤 4 · 用 Hubble 看流量
预期输出类似:
default demoapp-6f9c8b7d4-abcde 10.244.1.7:41234 -> 10.244.2.9:80 http-request FORWARDED
default demoapp-6f9c8b7d4-abcde 10.244.2.9:80 -> 10.244.1.7:41234 http-response FORWARDED
--last 只回看最近若干条,持续观察用 --follow;只看被拒绝的流用 hubble observe --verdict DROPPED,UI 用 cilium hubble ui 打开。
实验二:把 Calico master 改造成 Route Reflector
目标:在 4 节点 Calico 集群上关闭 full-mesh,让普通节点只与 master 建 BGP 会话。
步骤 1 · 给 RR 节点打 Cluster ID 与标签
kubectl annotate node k8s-master01.magedu.com \
projectcalico.org/RouteReflectorClusterID=224.0.99.1
kubectl label node k8s-master01.magedu.com route-reflector=true
Cluster ID 只需是集群内唯一、所有 RR 一致的标识(Calico 官方示例用 224.0.0.1)。仓库正文写 224.0.99.1、命令却写成 244.0.99.1;本实验统一用 224.0.99.1,多 RR 时必须完全相同。
步骤 2 · 创建指向 RR 的 BGPPeer
apiVersion: crd.projectcalico.org/v1
kind: BGPPeer
metadata:
name: peer-with-route-reflector
spec:
nodeSelector: all()
peerSelector: route-reflector == 'true'
nodeSelector 决定这条 peer 配置作用在哪些节点上,peerSelector 决定它们去找谁建会话。两条选择器配合,就能表达「所有节点都连 RR」。
步骤 3 · 关闭 full-mesh 并统一 ASN
apiVersion: crd.projectcalico.org/v1
kind: BGPConfiguration
metadata:
name: default
spec:
logSeverityScreen: Info
nodeToNodeMeshEnabled: false
nodeMeshMaxRestartTime: 120s
asNumber: 65009
serviceClusterIPs:
- cidr: 10.96.0.0/12
在 RR 上应看到与所有节点均为 node specific / Established,普通节点上只剩指向 RR 的一条:
+--------------+---------------+-------+-------------+
| PEER ADDRESS | PEER TYPE | STATE | INFO |
+--------------+---------------+-------+-------------+
| 172.29.7.1 | node specific | up | Established |
+--------------+---------------+-------+-------------+
需要冗余时,再给第二个节点打同样的注解和标签即可,普通节点会自动与两个 RR 各建一条会话。配置过程中 BGP 会短暂中断,生产环境放在维护窗口做。
验证清单¶
-
kubectl get nodes全部Ready,且kubectl -n kube-system get pods -l k8s-app=kube-proxy为空。 -
cilium status全 OK,跨节点 Pod 互 ping 通,hubble observe能看到 FORWARDED。 - Calico 集群
calicoctl node status不再出现node-to-node mesh,普通节点只连 RR。
常见问题与排错¶
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
节点 NotReady,事件 NetworkPluginNotReady |
未安装 CNI 或 conflist 不在 /etc/cni/net.d |
安装插件;ls /etc/cni/net.d、crictl info 确认运行时已识别 |
| Pod 跨节点不通、同节点正常 | VXLAN/IPIP 被防火墙拦截,或 MTU 过大 | 放行 UDP 4789 或 IPIP 协议号 4;按封装开销下调 MTU |
Cilium 装完 cilium status 报 kube-proxy 仍存在 |
init 时没加 --skip-phases=addon/kube-proxy |
删除 kube-proxy DaemonSet 与 ConfigMap 后重启 cilium agent |
calicoctl node status 会话一直 Idle |
两端 ASN 不一致、179 端口不通、RR 标签没生效 | 核对 asNumber、防火墙放行 TCP 179、kubectl get node --show-labels |
| native routing 下跨节点丢包 | 底层网络没有 Pod 网段路由 | 加 ipv4NativeRoutingCIDR 与 autoDirectNodeRoutes,或在交换机上配静态路由 |
进阶挑战¶
- 在同一集群对比 Calico 的 iptables 与 eBPF dataplane:切换
linuxDataplane后用calicoctl node status和延迟测试说明差异。 - 给 Cilium 加一条
CiliumNetworkPolicy,只允许带特定标签的 Pod 访问demoapp的/api路径,并用hubble observe --verdict DROPPED验证。
小结¶
- CNI 只负责 IP 与路由;Flannel 解决连通性,Calico 用 BGP 把网络变成可编程路由,Cilium 用 eBPF 收敛转发、策略与 Service。
- Calico 上百节点通常要把 full-mesh 换成 RR,核心是三件事:Cluster ID 注解、RR 标签与 BGPPeer、
nodeToNodeMeshEnabled: false。 - Cilium 的
kubeProxyReplacement与routingMode是两个独立维度,前者决定谁做 Service,后者决定报文是否封装;选型还要看是否需要 L7 策略与现有 BGP 网络。
延伸阅读¶
- Calico BGP 配置文档
- Cilium 官方文档
- 原仓库 Calico 示例
- 原仓库 Cilium 示例
- 下一步:阅读负载均衡与 Ingress 流量入口,把集群南北向流量接进来。