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 是动态创建/销毁的,节点之间只有物理网络。于是需要有人来做这几件事:
- 给 Pod 分配 IP
- 让同节点的两个 Pod 能互相访问
- 让跨节点的两个 Pod 能互相访问(物理网络不认识 Pod 网段,需要隧道或路由)
- 让 Pod 能访问外网(SNAT)
- 实现 K8s 的 Service(ClusterIP / NodePort)—— 这是一组 Pod 的虚拟 IP,需要负载均衡
- 实现 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) 命中,直接改写包。 好处:
- 快:常数级查找,不是线性遍历
- 不用重建整本规则册:更新 map 里一个条目即可,抖动小
- 能看:程序里可以顺手把"这次流量是什么"上报出去 → 这就是 Hubble
- 能做 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要点:
ciliumCLI 内置了 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 / k8sServicePort | API Server 的地址 | 没 kube-proxy 后,Pod 访问 API Server 不能靠 Service,需直连 |
ipam.mode=cluster-pool | IP 分配模式:Cilium 自己从一个大池子切 | 简单可控 |
clusterPoolIPv4PodCIDRList=10.244.0.0/16 | Pod 网段 | 对齐 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。
排查过程(这就是方法论):
- 先看链路本身通不通:用另一个 Pod(glibc 的 netshoot)跑同样的 DNS 查询 → 正常。
- 再看是不是工具本身:把同一个镜像用
docker run跑 dig → 正常;用docker exec跑 → 崩。 - 定位:该镜像(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/pausebash
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!),极具误导性。
排查:
- 直接从 client Pod 手测那个端口:
nc -zrc=0、curl返回 200、BPF LB 表里条目齐全 → 数据面没问题。 - 于是断定是 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]← 跨节点走 VXLANMasquerading: 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 list6.4 看流量(Hubble)
bash
hubble observe --since 5m # 需要 hubble CLI
# 或网页:http://192.168.60.128:31000/?namespace=default6.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 你可能想继续的方向(按"性价比"排序)
- 把 Hubble 用起来:给关键服务建依赖图、设 L7 策略。
- NetworkPolicy:集群里目前一条策略都没有 —— 这是最大的安全空缺。先用 Cilium 的
CiliumNetworkPolicy做"默认拒绝 + 按需放行"。 bpf.masquerade=true:把 SNAT 也变成纯 eBPF。routingMode=native:去掉 VXLAN 封装(单节点收益有限,多节点才明显)。- 把这套沉淀进你的二进制安装器:加一个
CNI=Cilium分支 + 离线镜像资产。 - 多节点:Cilium 真正的性能优势(eBPF 负载均衡、无 kube-proxy)在节点多、Service 多时才明显。
7.3 概念速查表
| 名词 | 一句话解释 |
|---|---|
| CNI | 给 Pod 配网络的插件规范;配置文件在 /etc/cni/net.d |
| iptables | 内核的包过滤/改写规则表,逐条匹配,规则多了慢 |
| eBPF | 在内核挂"钩子程序"的技术,可常数级查表、可编程、可观测 |
| kube-proxy | 传统上实现 Service(ClusterIP/NodePort) 的组件,用 iptables/IPVS |
| kubeProxyReplacement | Cilium 用 eBPF 取代 kube-proxy |
| IPAM | Pod IP 的分配机制 |
| podCIDR | 节点分到的 Pod 网段 |
| VXLAN / IPIP | 跨节点承载 Pod 流量的两种封装(隧道) |
| native routing | 不封装,直接路由 Pod 网段 |
| masquerade (SNAT) | Pod 出集群时把源地址改成节点地址 |
| Hubble | Cilium 的可观测层,看 flow 和依赖 |
| CiliumEndpoint | Cilium 给每个 Pod 建的对象,记录其身份/策略状态 |
相关笔记
归档位置
- 目标机:
/root/cilium-migrate/(基线快照、渲染清单、连通性日志)