跳转至

CNI 网络:Calico 与 Cilium

kubeadm 只负责把节点拉起来,真正让 Pod 拿到地址、跨节点互通的是 CNI。本章拆开 CNI 的调用链,对比 Flannel、Calico、Cilium 三种数据平面,然后用 cilium install 部署一套无 kube-proxy 的 eBPF 网络,最后把 Calico 的 full-mesh 改造成 Route Reflector 架构。

难度 · 进阶 预计 90 分钟 前置:Service 与网络基础

你将学到

  • 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 · 检查状态

cilium status --wait
kubectl -n kube-system get pods -l k8s-app=cilium -o wide

预期输出(版本与 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 看流量

cilium hubble port-forward &
hubble observe --namespace default --last 20

预期输出类似:

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

bgppeer-with-rr.yaml
apiVersion: crd.projectcalico.org/v1
kind: BGPPeer
metadata:
  name: peer-with-route-reflector
spec:
  nodeSelector: all()
  peerSelector: route-reflector == 'true'
kubectl apply -f bgppeer-with-rr.yaml

nodeSelector 决定这条 peer 配置作用在哪些节点上,peerSelector 决定它们去找谁建会话。两条选择器配合,就能表达「所有节点都连 RR」。

步骤 3 · 关闭 full-mesh 并统一 ASN

bgpconfiguration-default.yaml
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
kubectl apply -f bgpconfiguration-default.yaml
calicoctl node status

在 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,或在交换机上配静态路由

进阶挑战

  1. 在同一集群对比 Calico 的 iptables 与 eBPF dataplane:切换 linuxDataplane 后用 calicoctl node status 和延迟测试说明差异。
  2. 给 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 网络。

延伸阅读