换了 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 一起升级。

传统工具链全失效:iptables 为空、tcpdump 只有 SYN 无 SYN-ACK


【分层】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 是 78901unknown),但 backend 的 identity 应该是 12345包根本没有到达 backend——在 Cilium agent 做策略评估时就被 eBPF 程序丢掉了。

cilium monitor 显示 policy denied

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 数据路径:新旧 Pod 走不同的 identity→policy 分支

上图展示了 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 变了,已编译的策略规则不会自动更新。

Identity 对比:新旧 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 observe 全链路可视化

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

Hubble 多视图:对比 FORWARDED 和 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 一览

Cilium 排障命令清单


【定位】最常见的误判:还在查 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-savecilium monitorconntrack -Lcilium bpf ct list globaltcpdumphubble observe

双栏对比:传统排查路径 vs Cilium 排查路径


【标点】修复 + 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拆代码的曹操」