换了 Cilium 以后,iptables 查不到了——跨 Namespace 超时的 eBPF 排障实录
场景:集群从 Flannel 迁移 Cilium 后,跨 Namespace gRPC 调用间歇超时——iptables 查不到任何规则、Pod 内 tcpdump 有 SYN 无 SYN-ACK 路径:坐标(Pod 状态 + 传统工具链失效)→ 分层(endpoint → monitor → identity → policy → CT → hubble)→ 路径(6 个 Cilium 排障命令)→ 定位(还在查 iptables?CNI 换了排查路径也得换)→ 标点(CiliumNetworkPolicy 修复 + Check-list) K8s 版本:v1.28,Cilium v1.15
上篇讲了 K8s 管不了带宽、要自己用 tc 限速——核心教训是 K8s 不调度带宽,你得自己去内核 tc 层控制。这次的问题从另一个角度印证了同一个规律:CNI 一换,你的排查工具箱得跟着换,否则什么都看不到。
集群从 Flannel 切到 Cilium 后,跨 Namespace 的 gRPC 调用开始超时。排障工程师查了 Pod 状态——Running,查了日志——没报错,查了 iptables——空的。"Flannel 下 iptables 不是有 MASQUERADE 规则吗?"不是 iptables 出问题了——Cilium 根本不用 iptables,规则全部跑在 eBPF 里,你用旧工具链当然什么都看不到。
金句:换了 CNI 不是换个网络插件——是你的排查路径要整个重学
【坐标】传统工具链失效:iptables 查不到,tcpdump 看不懂
Layer 1 — 部署背景
| 部署项 | 配置 |
|---|---|
| 消费方 | Pod A,frontend,Namespace prod-fe,10.244.1.15 |
| 目标服务 | backend-svc.prod-be:9090,gRPC |
| 网络插件 | Cilium v1.15(eBPF 模式) |
| 集群版本 | K8s v1.28,kube-proxy 模式 iptables |
| 前置条件 | 集群刚从 Flannel(vxlan)迁移到 Cilium |
Layer 2 — 症状:集群内不通,但不是全不通
当 Cilium NetworkPolicy 隔离设错了方向时,最典型的症状就是间歇性通——不是每次都超时,误以为是网络抖动。curl 超时、ping 却正常,这不是巧合——ping 走 ICMP 不需要经过 NetworkPolicy 评估,而 gRPC 走 TCP 需要在 eBPF policy 层做身份校验。Pod 状态 Running,Events 为空——scheduler 和 kubelet 都没有异常记录。问题不在 Pod 本身,在网络边界。
Layer 3 — 传统工具链失效
Flannel 下排查网络不通的第一反应是查 iptables,但 Cilium 环境完全不一样。iptables-save 输出为空——不是规则被清了,是 Cilium 根本不写 iptables。tcpdump 看到 3 次 SYN 发出、0 个 SYN-ACK 回来——包出了 Pod 但没人搭理。Flannel 下你会去查 Node 的 iptables POSTROUTING 链看 SNAT 有没有做,但 Cilium 不用 iptables。你的排查工具箱需要跟着 CNI 一起升级。

【分层】Cilium/eBPF 排查:从 endpoint 到 hubble,每层有专用工具
排查顺序表
| 层 | 排查对象 | Cilium 工具 | 等价传统工具 |
|---|---|---|---|
| Layer 1 | Endpoint 状态 | cilium endpoint list |
kubectl describe pod |
| Layer 2 | 数据路径跟踪 | cilium monitor |
tcpdump |
| Layer 3 | 安全身份 | cilium identity list |
(没有等价物) |
| Layer 4 | 网络策略评估 | cilium endpoint get <id> |
iptables-save |
| Layer 5 | BPF 连接跟踪 | cilium bpf ct list global |
conntrack -L |
| Layer 6 | 全链路可视化 | hubble observe |
传统抓包 + 人工关联 |
Layer 1 — Endpoint 层:Pod 在 Cilium 眼里长什么样
Cilium 不直接管 Pod——eBPF 程序挂载在内核的静态 hook 点上,但 Pod 的 PID 和 cgroup 会随重启改变,太动态了不适合做稳定标识。所以 Cilium 对每个 Pod 的 network namespace 创建了一个 endpoint(Cilium 管理的网络端点,与 Pod 一一对应,有自己的安全身份),所有网络策略和转发都基于 endpoint 和 identity 做。cilium endpoint list 看到两个 endpoint 都是 ready 状态——Cilium agent 和内核 eBPF 程序的交互是通的。
Layer 2 — Cilium Monitor:eBPF 版的 tcpdump
传统排查要在 Pod 和 Node 两端同时抓包再人工比对。Cilium monitor 直接从 eBPF 程序的事件管道输出,不需要攻入 Pod 的 network namespace。运行后立即看到 policy denied:目标 identity 是 78901(unknown),但 backend 的 identity 应该是 12345。包根本没有到达 backend——在 Cilium agent 做策略评估时就被 eBPF 程序丢掉了。

Layer 3 — Identity 层:跨 Namespace 的身份迷雾
Cilium 的隔离机制比 iptables 多了一层抽象——identity(安全身份,Cilium 根据 Pod 的 labels + namespace 计算出的数字标识)。网络策略基于 identity 做评估,不是基于 CIDR。
cilium identity list | grep prod-be 返回了两行——同一个 Namespace 的 backend Pod 居然有两个不同的 identity(12345 和 78901)。root cause 就在这里。
Pod rolling update 后新 Pod 多了个 version: v1 label。Cilium 根据 label 集合计算 identity:{app: backend, ns: prod-be}→12345,{app: backend, version: v1, ns: prod-be}→78901。但 CiliumNetworkPolicy 只引用了 matchLabels: {app: backend}——策略创建时编译锁定 identity 12345,新 identity 78901 完全不在策略的允许列表中。
label 变了 → identity 变了 → CiliumNetworkPolicy 没覆盖新 identity → 流量被策略丢掉了。 这就是"CNP 配了等于没配"的经典陷阱。

上图展示了 Cilium 在 eBPF 层面的完整数据路径。frontend Pod 的包进入内核后,eBPF 程序先查 identity(56789),然后查 CNP 编译后的 identity 允许列表——对旧 Pod(identity=12345)放行,对新 Pod(identity=78901)拒绝。策略评估和连接跟踪(BPF CT)全部在 eBPF 程序内部完成,不需要经过用户态的 iptables 或 conntrack。
Layer 4 — Policy 层:验证策略是否命中
cilium endpoint get 5678 | jq '.status.policy.realized' 显示 allowed-ingress-identities 只有 [56789](frontend 的 identity),但当前 backend endpoint 的实际 identity 是 78901。Cilium 的策略引擎在 CNP 创建/更新时编译 matchLabels 为具体 identity 列表——不是每次请求都去查 label。所以 Pod 更新后 identity 变了,已编译的策略规则不会自动更新。

Layer 5 — Hubble:把 30 分钟压到 3 秒
排查到这里根因已经清晰——identity 不匹配导致 policy denied。但如果一开始就用 Hubble,整个排查可以在 3 秒内完成。hubble observe --from-pod prod-fe/frontend --to-pod prod-be/backend 直接输出所有 flow,每条标注了 POLICY_VERDICT 和来源。--verdict DROPPED 过滤只看被丢的包,--last 20 看最近流量。
从传统排查到 Cilium 排查的范式转变在于——你不用再站在 Pod 里问"谁能到谁",数据路径上的每一步都在 eBPF 事件管道里等待 Hubble 来回答。

Hubble 不仅能看 DROPPED 事件,还能直接对比正常流量和异常流量:

【路径】Cilium/eBPF 排障核心命令速查
面对 Cilium 网络故障,按这个顺序跑一遍就能定位根因:
| 步骤 | 命令 | 发现什么 |
|---|---|---|
| 1 | cilium endpoint list |
endpoint 是否 ready,identity 是否正确 |
| 2 | cilium monitor --type drop |
实时丢包事件,附带 policy 来源 |
| 3 | cilium identity list --label k8s:ns=<ns> |
同一 Namespace 是否有 identity 不一致 |
| 4 | cilium endpoint get <id> \| jq '.status.policy' |
当前 endpoint 被哪些 identity 允许访问 |
| 5 | cilium bpf ct list global |
BPF 连接跟踪表状态 |
| 6 | hubble observe --verdict DROPPED |
全链路可视化,所有被丢的 flow 一览 |

【定位】最常见的误判:还在查 iptables
回忆一下你拿到"curl 超时、tcpdump 只有 SYN"的第一反应——"kube-proxy 出问题了?"于是 ssh 到 Node 查 kube-proxy 配置、重启 kube-proxy daemonset、甚至去翻 Flannel 的 NAT 规则……3 个小时过去,问题还在。
Cilium 做了 eBPF 代替 iptables 的三大替换:kube-proxy 的 Service 负载均衡 → eBPF kube-proxy replacement,网络策略执行 → eBPF policy enforcement(不是 iptables 的 FORWARD chain),连接跟踪 → eBPF CT map(不是 conntrack 内核模块)。排查路径必须跟着迁——iptables-save→cilium monitor,conntrack -L→cilium bpf ct list global,tcpdump→hubble observe。

【标点】修复 + Check-list + 命令清单
修复:重新应用 CiliumNetworkPolicy
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: prod-be
spec:
endpointSelector:
matchLabels:
app: backend
ingress:
- fromEndpoints:
- matchLabels:
app: frontend
k8s:io.kubernetes.pod.namespace: prod-fe
toPorts:
- ports:
- port: "9090"
protocol: TCP
kubectl apply -f 这个 YAML。注意——YAML 内容和原来一模一样,但这次执行后问题解决了。为什么?Cilium 在 CNP 创建时编译 matchLabels 为具体的 identity 列表。 第一次创建时集群里只有 identity 12345({app: backend}),所以编译结果是 allow [12345]。Rolling update 后新 identity 78901 产生了,但已编译的规则不会自动更新。重新 apply CNP 让 Cilium 重新执行 matchLabels 匹配——现在集群里有 identity 12345 和 78901 都匹配 app: backend——编译结果是 allow [12345, 78901],问题修复。
验证:cilium endpoint get 5678 | jq '.status.policy.realized' 会显示 allowed-ingress-identities: [56789, 78901]。
关键教训:CiliumNetworkPolicy 的 matchLabels 不是实时查询——它在策略创建/更新时编译锁死匹配的 identity 列表。Pod rolling update 加了新 label 会产生新 identity,但旧 identity 依然存在(旧 Pod 还在),所以你不会看到"策略失效"的报错,只会看到新 Pod 的流量被静默丢掉。一个简单的经验法则:CNP 的 selector 永远加上 Pod template 最完整的 label 集合,不要只写 app: xxx。
Check-list
- [ ]
cilium endpoint list— endpoint 状态都 ready? - [ ]
cilium monitor --type drop— 有被丢的包吗? - [ ]
hubble observe --verdict DROPPED— 谁在丢包、什么策略丢的? - [ ]
cilium identity list --label k8s:ns=<ns>— 同一个 NS 的 identity 是否一致? - [ ]
kubectl get pods -o yaml | grep labels— Deployment vs Pod template 的 labels 是否一致? - [ ]
cilium endpoint get <id> | jq '.status.policy.realized'— policy 是否覆盖了实际 identity? - [ ]
kubectl get ciliumnetworkpolicies -o yaml— CNP 的 matchLabels 是否匹配当前 label 集合?
金句:换了 CNI 不是换个网络插件——是你的排查路径要整个重学
下篇我们聊 eBPF 排障的终极手段——bpftrace。当 cilium monitor 不够用、hubble 也看不到你想要的底层事件时,怎么直接用 bpftrace 挂载 kprobe 做定点观测。
附:完整命令清单
# Endpoint 状态
cilium endpoint list
cilium endpoint get <id>
# 数据路径实时跟踪
cilium monitor --from-endpoint <id> --verbosity debug
cilium monitor --type drop
cilium monitor --type policy-verdict
# 安全身份
cilium identity list --label k8s:app=<name>
cilium identity list --label k8s:ns=<namespace>
# 策略评估
cilium endpoint get <id> | jq '.status.policy'
cilium policy get
# BPF 连接跟踪
cilium bpf ct list global
# 全链路可视化(hubble)
hubble observe --from-pod <ns>/<pod> --to-pod <ns>/<pod>
hubble observe --verdict DROPPED
hubble observe --last 100
# 策略调试
kubectl get ciliumnetworkpolicies -A -o yaml
kubectl describe ciliumnetworkpolicy <name>
# 故障注入验证
kubectl exec -n <ns> <pod> -- curl -m 3 <svc>:<port>/health
📺 公众号「Ai拆代码的曹操」 🌟 知识星球「Ai拆代码的曹操」