跳转到内容

Cilium 替换 Calico 全过程讲解(深度模式) ​

目标读者:已经完成替换、但想知道"到底发生了什么、为什么这么做"的运维同学。 环境:192.168.60.128 单节点 k8s v1.35.4,docker 29.7.2 + cri-dockerd,Ubuntu 26.04 本文所有结论都来自这台机器上的实测输出,不是抄文档。


开篇:一句话概括这次做了什么 ​

把集群"给 Pod 分配 IP、让 Pod 互相通信、并实现 K8s Service 负载均衡"这一整套底层机制, 从 Calico(路由/隧道 + iptables) 换成了 Cilium(eBPF 数据面), 并顺手把传统上负责 Service 转发的老组件 kube-proxy 停掉(它的活被 eBPF 接管了)。

要真正看懂这件事,先得有 3 个心智模型。


第 0 部分:三个必须先有的心智模型 ​

0.1 Pod 网络到底要解决什么问题 ​

K8s 里每个 Pod 都有自己的 IP,而且要在整个集群范围内可直接寻址(不用 NAT)。 但 Pod 是动态创建/销毁的,节点之间只有物理网络。于是需要有人来做这几件事:

  1. 给 Pod 分配 IP
  2. 让同节点的两个 Pod 能互相访问
  3. 让跨节点的两个 Pod 能互相访问(物理网络不认识 Pod 网段,需要隧道或路由)
  4. 让 Pod 能访问外网(SNAT)
  5. 实现 K8s 的 Service(ClusterIP / NodePort)—— 这是一组 Pod 的虚拟 IP,需要负载均衡
  6. 实现 NetworkPolicy(谁能访问谁)

负责 1~5 的那个组件叫 CNI 插件(网络插件),第 6 项通常也由它负责。Calico、Cilium、Flannel 都是 CNI 插件。

0.2 CNI 是"插件 + 配置文件",由容器运行时调用 ​

  • 插件二进制放在 /opt/cni/bin/(比如 calico、cilium-cni)
  • 配置放在 /etc/cni/net.d/(.conflist 文件)
  • 容器运行时(你这台是 docker + cri-dockerd)在创建 Pod 时会去读 /etc/cni/net.d/, 按配置调用插件二进制,给这个 Pod 的网络命名空间配好 IP 和路由。

所以"换 CNI"的本质之一就是:把 /etc/cni/net.d/ 里的配置和 /opt/cni/bin/ 里的插件换掉。 我们这次做的正是:删掉 10-calico.conflist 和 calico 二进制,换成 Cilium 的。

补充:Cilium 的 cilium-agent(DaemonSet)会自己往 /etc/cni/net.d/ 写 05-cilium.conflist。 它默认开启 cni.exclusive,会自动清掉别的 CNI 配置,避免两个 CNI 打架。

0.3 "控制面"与"数据面" ​

  • 控制面:谁该被分配哪个 IP、有哪些 Service、有哪些策略 —— 靠监听 K8s API 得到。
  • 数据面:真正转发/改写每个数据包的逻辑 —— 决定性能上限。

两条技术路线的本质差别就在数据面:

数据面实现代价
Calico(默认)Linux 内核路由表 + iptables规则条目多,逐条链式匹配
Cilium内核 eBPF 程序(挂在网卡/cgroup 的钩子上)哈希表直接查,可编程、可观测

(Calico 也支持 eBPF,但默认不用;Cilium 是 eBPF-first。)


第 1 部分:为什么 eBPF 比 iptables 好(这一段理解了,后面全通) ​

iptables 就像一本从上往下逐条念的规则册:一个包进来,要一条条比对,命中才处理。 Service 一多、端点一多,规则成百上千条,每次服务变更还要重写整本册子。

eBPF 则是在内核里挂一个"钩子函数":包到网卡时直接进你的程序, 程序里用 map(哈希表) 查一下"这个 ClusterIP 对应哪些后端",O(1) 命中,直接改写包。 好处:

  1. 快:常数级查找,不是线性遍历
  2. 不用重建整本规则册:更新 map 里一个条目即可,抖动小
  3. 能看:程序里可以顺手把"这次流量是什么"上报出去 → 这就是 Hubble
  4. 能做 L7:能在 eBPF 层面解析 HTTP,做七层策略

代价:需要有较新内核(一般 ≥5.10)、要理解的东西更多、排障手法和 iptables 时代不同。


第 2 部分:kube-proxy 是什么,为什么要停掉它 ​

2.1 kube-proxy 的活 ​

K8s 的 Service 有三层:

  • ClusterIP(如 10.96.x.x):集群内虚拟 IP,访问它要 DNAT 到某个后端 Pod
  • NodePort:在每个节点上开一个端口(30000-32767),访问它也要 DNAT 到后端
  • LoadBalancer:云上用

这些 DNAT/负载均衡规则,传统上由 kube-proxy 用 iptables 或 IPVS 写出来。kube-proxy 监听 Service/Endpoint 变化,把规则写进本机内核。

痛点:又是 iptables 那套,服务多了规则爆炸。

2.2 kubeProxyReplacement=true 是什么 ​

Cilium 的招牌功能:不用 kube-proxy 了。Service 的负载均衡直接由 eBPF 程序 + BPF map 实现。

  • 启动参数:--set kubeProxyReplacement=true
  • 效果:KubeProxyReplacement: True,Service 的 DNAT 全在 BPF 里做
  • 前提:停掉 kube-proxy;并且要告诉 Cilium API Server 的地址(因为 Pod 要访问 API Server 也是走 Service,没 kube-proxy 了得直连)
    • 所以有 --set k8sServiceHost=192.168.60.128 --set k8sServicePort=6443

2.3 ★ 这次踩的关键坑:kube-proxy 是 systemd 服务,不是 DaemonSet ​

  • kubeadm 装的集群,kube-proxy 是 DaemonSet(在 kube-system 里能看到 pod)
  • 你这套是二进制手工部署的,kube-proxy 是 systemd 服务(systemctl status kube-proxy)

后果:Cilium 靠"集群里有没有 kube-proxy 这个 DaemonSet"来判断要不要清理它的 iptables 规则。 你这台没有 DaemonSet,于是 Cilium 没去清,留下了 81 行 KUBE-SVC-* / KUBE-SEP-* / KUBE-POSTROUTING 规则。

为什么必须清:这些残留规则也在做 Service DNAT,和 Cilium 的 eBPF 重复转发; 而且它们是"旧快照",Pod 重建后旧端点就变成黑洞。

清理方法:

bash
systemctl disable --now kube-proxy
/usr/local/bin/kube-proxy --cleanup --kubeconfig=/etc/kubernetes/kube-proxy.conf
# 验收:只应剩下 kubelet 自带的 KUBE-FIREWALL / KUBE-KUBELET-CANARY / KUBE-IPTABLES-HINT
iptables-save | grep -c 'KUBE-SVC\|KUBE-SEP\|KUBE-POSTROUTING'   # 期望 0

第 3 部分:逐步复盘(我们实际做了什么、为什么) ​

Step 0 — 采集基线(只读) ​

bash
mkdir -p /root/cilium-migrate
iptables-save > /root/cilium-migrate/iptables-before.txt
ip route show > /root/cilium-migrate/routes-before.txt
kubectl get pods -A -o wide > /root/cilium-migrate/pods-before.txt
cp /etc/cni/net.d/10-calico.conflist /root/cilium-migrate/

为什么:没有基线就无法证明"改动带来了什么",也无法在出问题时对照。 后来我们拿它对比出 iptables 从 406 行降到 ~101 行。

Step 1 — 装 cilium CLI + 预拉镜像 ​

bash
CLI_VER=$(curl -s https://raw.githubusercontent.com/cilium/cilium-cli/main/stable.txt)
curl -L --fail --remote-name-all \
  https://github.com/cilium/cilium-cli/releases/download/${CLI_VER}/cilium-linux-amd64.tar.gz{,.sha256sum}
sha256sum --check cilium-linux-amd64.tar.gz.sha256sum
tar xzvfC cilium-linux-amd64.tar.gz /usr/local/bin

# 反查这次安装到底需要哪些镜像(避免安装时卡 ImagePull)
cilium install --version 1.20.2 --set ... --dry-run > render.yaml
grep -oE 'image: [^ ]+' render.yaml | sed 's/image: //' | sort -u
# 然后逐个 docker pull

要点:

  • cilium CLI 内置了 helm,不用另外装 helm
  • --dry-run 这版 CLI 不支持 -o yaml(这是个小坑)
  • 预拉镜像 = 变更窗口内不会因为拉镜像慢而卡住

Step 2 — 卸载 Calico ​

bash
kubectl -n kube-system delete ds calico-node
kubectl -n kube-system delete deploy calico-kube-controllers
rm -f /etc/cni/net.d/10-calico.conflist /etc/cni/net.d/calico-kubeconfig
rm -f /opt/cni/bin/calico /opt/cni/bin/calico-ipam
# 清 calico 自建的 iptables 链(cali-*):先删跳转,再 flush+delete
ip link del tunl0                      # Calico 的 IPIP 隧道口
ip route | awk '/172\.16\./{print $2}' | while read r; do ip route del "$r"; done
rm -rf /var/run/calico /var/lib/calico /var/log/calico

原理:Calico 由三部分组成 ——

  • calico-node(DaemonSet,跑 felix 写 iptables/路由,跑 bird 做 BGP)
  • calico-kube-controllers(Deployment,处理 CRD/同步)
  • 宿主上的 CNI 配置 + 二进制(供运行时调用)

三样都要清:K8s 对象、宿主文件、宿主内核残留(iptables/路由/隧道口)。

Step 3 — 停用 kube-proxy ​

见第 2.3 节。这一步和 Step 4 必须紧挨着做,因为停掉 kube-proxy 后、Cilium 起来前, ClusterIP 会短暂失效(分钟级)。

Step 4 — 安装 Cilium(深度模式) ​

bash
cilium install --version 1.20.2 \
  --set kubeProxyReplacement=true \
  --set k8sServiceHost=192.168.60.128 \
  --set k8sServicePort=6443 \
  --set ipam.mode=cluster-pool \
  --set ipam.operator.clusterPoolIPv4PodCIDRList=10.244.0.0/16 \
  --set devices=ens32 \
  --set hubble.enabled=true \
  --set hubble.relay.enabled=true \
  --set hubble.ui.enabled=true
cilium status --wait

每个参数的含义见第 4 部分。

Step 5 — 重建工作负载(关键,容易漏) ​

bash
kubectl -n kube-system rollout restart deploy coredns
kubectl -n kube-system rollout restart deploy metrics-server
kubectl -n local-path-storage rollout restart deploy local-path-provisioner
kubectl -n venus-plugin rollout restart deploy venus-registry

为什么必须做:老 Pod 还带着 Calico 时代分配的 172.16.x IP, 而 Cilium 的 IPAM 池是 10.244.0.0/24 —— 不动它们,Cilium 管不了这些 Pod 的网络。 rollout restart 让 Pod 重建并拿到新 IP。裸 Pod(不在 Deployment 里的)要手动删。

Step 6 — 验证 ​

见第 6 部分。

Step 7 — 前后对比 ​

bash
echo "before: $(wc -l < /root/cilium-migrate/iptables-before.txt)"
echo "after : $(iptables-save | wc -l)"

第 4 部分:核心配置项逐个解释 ​

参数含义为什么这么设
kubeProxyReplacement=true由 eBPF 接管 Service 负载均衡深度模式的目标;性能与"去 kube-proxy"
k8sServiceHost / k8sServicePortAPI Server 的地址没 kube-proxy 后,Pod 访问 API Server 不能靠 Service,需直连
ipam.mode=cluster-poolIP 分配模式:Cilium 自己从一个大池子切简单可控
clusterPoolIPv4PodCIDRList=10.244.0.0/16Pod 网段对齐 kube-controller-manager 的 --cluster-cidr=10.244.0.0/16(原来 Calico 用 172.16.0.0/16,两边不一致,属历史遗留)
devices=ens32只在指定网卡上做 eBPF/NodePort避免 Cilium 接管 docker0 之类的虚拟网卡导致混乱
hubble.*打开 flow 采集/聚合/UI可观测性
routingMode(默认 tunnel)跨节点怎么走:VXLAN 封装单节点其实用不到封装
bpf.masquerade(默认 false)出集群 SNAT 用 eBPF 还是 iptables我们当前是 IPTables,可切 eBPF 更纯粹

几个概念补一刀:

  • 隧道(tunnel):把 Pod 包再包一层(VXLAN),物理网络只看到节点间的普通包。简单但多一层封装开销。
  • native routing:不做封装,直接让节点路由器认识 Pod 网段。性能好,但要求网络可控。
  • 无 kube-proxy 模式的 NodePort:eBPF 在网卡钩子上拦截目标端口,直接 DNAT,绕过 netfilter。 (但见第 5 部分的坑 2 —— 测试工具会误报。)

第 5 部分:两个真实的坑,以及"怎么分辨是谁的问题" ​

坑 1:cilium connectivity test 卡在 DNS,报 exit code 139 ​

现象:app.c:306:isc_app_ctxshutdown(): fatal error: kill: Permission denied (13) → Segmentation fault。

排查过程(这就是方法论):

  1. 先看链路本身通不通:用另一个 Pod(glibc 的 netshoot)跑同样的 DNS 查询 → 正常。
  2. 再看是不是工具本身:把同一个镜像用 docker run 跑 dig → 正常;用 docker exec 跑 → 崩。
  3. 定位:该镜像(alpine/musl + BIND 9.18)里的 dig,只要是"exec 会话"里跑,退出时就崩(能出结果、退出码却是 139)。 而 cilium connectivity test 恰好用 kubectl exec 跑 dig。

结论:这是测试工具镜像的问题,与 Cilium、与你的数据面无关。

解法:自建一个 client 镜像 = glibc 工具集 + 测试要求的 /usr/bin/pause:

dockerfile
FROM nicolaka/netshoot:latest
USER root
RUN printf '#!/bin/sh\nexec sleep infinity\n' > /usr/bin/pause && chmod +x /usr/bin/pause
bash
docker build -t cilium-test-client:v1 .
cilium connectivity test --curl-image cilium-test-client:v1

(注意:直接用 --curl-image nicolaka/netshoot 会 CrashLoop —— 因为测试的 client 容器启动命令是 /usr/bin/pause,netshoot 里没这个文件。这个细节是查 describe pod 才发现的。)

坑 2:卡在 Waiting for NodePort ... 但 NodePort 明明能用 ​

现象:报超时,但报错正文里那句是 成功 的(Connection to <IP> ... succeeded!),极具误导性。

排查:

  1. 直接从 client Pod 手测那个端口:nc -z rc=0、curl 返回 200、BPF LB 表里条目齐全 → 数据面没问题。
  2. 于是断定是 cilium-cli 的检查逻辑 误报(它会在节点所有地址上验证 NodePort 就绪, 而节点会同时上报 InternalIP 和 Hostname)。

结论:已知的 cilium-cli 工具问题类型,不是 Cilium 问题。 绕过:--service-type ClusterIP(代价:nodeport 场景会因取不到端口而变成 :0 的假失败)。

方法论提炼(很重要) ​

看到测试报错,第一件事不是改配置,而是先判断"是数据面错了,还是测量工具错了"。 办法:换一个独立工具/独立路径去复测同一目标。若独立复测是好的 → 工具问题。


第 6 部分:日常怎么验证和排障(命令手册) ​

6.1 一眼看状态 ​

bash
cilium status                          # 组件全览
kubectl -n kube-system exec ds/cilium -- cilium-dbg status | head -20

关键行解读:

  • KubeProxyReplacement: True [ens32 ... (Direct Routing)] ← 深度模式生效
  • IPAM: IPv4: 8/254 allocated from 10.244.0.0/24 ← IP 池与已分配数
  • Routing: Network: Tunnel [vxlan] ← 跨节点走 VXLAN
  • Masquerading: IPTables ← SNAT 仍走 iptables(可切 BPF)

6.2 证明 Service 真的走 eBPF 了 ​

bash
kubectl -n kube-system exec ds/cilium -- cilium-dbg bpf lb list | head

能看到 ClusterIP → 后端 Pod IP 的映射在 BPF 表里,就说明 Service 已由 eBPF 接管。

6.3 看端点和策略 ​

bash
kubectl -n kube-system exec ds/cilium -- cilium-dbg endpoint list

6.4 看流量(Hubble) ​

bash
hubble observe --since 5m            # 需要 hubble CLI
# 或网页:http://192.168.60.128:31000/?namespace=default

6.5 网络连通性手测模板 ​

bash
kubectl -n default run nettest --image=nicolaka/netshoot --restart=Never --command -- sleep 600
kubectl -n default exec nettest -- nslookup kubernetes.default.svc.cluster.local   # DNS
kubectl -n default exec nettest -- curl -sk -o /dev/null -w '%{http_code}\n' https://10.96.0.1/healthz  # ClusterIP
kubectl -n default exec nettest -- curl -s -o /dev/null -w '%{http_code}\n' --max-time 5 http://223.5.5.5/  # 出网
kubectl -n default delete pod nettest

第 7 部分:当前状态快照 & 后续学习路径 ​

7.1 现状(改完的样子) ​

  • CNI:Cilium v1.20.2,kubeProxyReplacement=True,Hubble 已开
  • Pod 网段:10.244.0.0/16(节点实际分配 10.244.0.0/24)
  • kube-proxy:已 disable --now,其 iptables 规则已清 0
  • iptables 总行数:406 → ~101(只剩 kubelet 自带的几条 + Cilium 少量)
  • 隧道:Calico 的 tunl0(IPIP) 已删,改用 Cilium 的 cilium_vxlan
  • 新增可观测:Hubble Relay + Hubble UI(NodePort 31000)

7.2 你可能想继续的方向(按"性价比"排序) ​

  1. 把 Hubble 用起来:给关键服务建依赖图、设 L7 策略。
  2. NetworkPolicy:集群里目前一条策略都没有 —— 这是最大的安全空缺。先用 Cilium 的 CiliumNetworkPolicy 做"默认拒绝 + 按需放行"。
  3. bpf.masquerade=true:把 SNAT 也变成纯 eBPF。
  4. routingMode=native:去掉 VXLAN 封装(单节点收益有限,多节点才明显)。
  5. 把这套沉淀进你的二进制安装器:加一个 CNI=Cilium 分支 + 离线镜像资产。
  6. 多节点:Cilium 真正的性能优势(eBPF 负载均衡、无 kube-proxy)在节点多、Service 多时才明显。

7.3 概念速查表 ​

名词一句话解释
CNI给 Pod 配网络的插件规范;配置文件在 /etc/cni/net.d
iptables内核的包过滤/改写规则表,逐条匹配,规则多了慢
eBPF在内核挂"钩子程序"的技术,可常数级查表、可编程、可观测
kube-proxy传统上实现 Service(ClusterIP/NodePort) 的组件,用 iptables/IPVS
kubeProxyReplacementCilium 用 eBPF 取代 kube-proxy
IPAMPod IP 的分配机制
podCIDR节点分到的 Pod 网段
VXLAN / IPIP跨节点承载 Pod 流量的两种封装(隧道)
native routing不封装,直接路由 Pod 网段
masquerade (SNAT)Pod 出集群时把源地址改成节点地址
HubbleCilium 的可观测层,看 flow 和依赖
CiliumEndpointCilium 给每个 Pod 建的对象,记录其身份/策略状态

相关笔记 ​

归档位置 ​

  • 目标机:/root/cilium-migrate/(基线快照、渲染清单、连通性日志)

基于 MIT 许可发布