记一次 Cilium VXLAN 全零 MAC 丢包问题的排查与修复
记一次 Cilium VXLAN 全零 MAC 丢包问题的排查与修复
本文所有 IP、MAC、节点名、网卡名均为脱敏后的示意值,仅用于说明网络结构与排查思路,不代表真实生产环境。
背景
某次接到反馈:从公有云上访问某边缘 K8s 集群的推理服务不通。
网络架构上有两条专线(WireGuard 隧道)链路,分别接到集群内两个不同节点:
- 公有云 VM-1 → WireGuard 隧道-1 → 集群的 edge-gw 节点(一个不在 K8s 集群内的网关节点) ✅
- 公有云 VM-2 → WireGuard 隧道-2 → 集群的 master 节点(K8s 控制面节点) ❌
诡异的是:两条链路打到的后端其实是同一个推理服务 Pod,一条通、一条不通。这种"对称中又不对称"的现象,往往最耗时间——因为它会反复推翻你的假设。
整体拓扑
1 | ┌──────────── 公有云 ────────────┐ |
两条链路对照:
| Link 1 ✅ | Link 2 ❌ | |
|---|---|---|
| 公有云客户端 | VM-1 | VM-2 |
| WireGuard 隧道 | wg-tunnel-1 | wg-tunnel-2 |
| 落地节点 | edge-gw (harbor,非 K8s) | master (K8s 控制面) |
| 该节点是否有 cilium | 否 | 是 |
| wg 网卡上是否有 cilium BPF | 无 | 有(提前 DNAT+SNAT) |
| 流量走向 | 命中静态路由,直接转发往 gpu-171 | 被 BPF 拐进 cilium_vxlan overlay 去 gpu-119 |
| VXLAN 内层以太网 MAC | 有效 | 全零 |
| 对端处理 | vxlan_rcv 解封装成功,交付到 Pod | vxlan_rcv 解封装发现 MAC 非法,kfree_skb 丢弃 |
| 使用的 VIP | VIP1 (10.20.1.x,pool-1) | VIP2 (10.20.2.x,pool-2) |
| 后端 Pod | 同一个 (172.18.5.34 / 测试用 172.18.5.17) | 同一个 |
关键不对称点:edge-gw(harbor)不在 K8s 集群里、没有 Cilium,所以 wg-tunnel-1 上没有 BPF attach,流量老老实实走 Linux 静态路由直达 gpu 节点;而 master 是 K8s 节点、有 Cilium,wg-tunnel-2 上被 attach 了
cil_from_netdev-wg-tunnel-2,它在路由查找之前就把流量 DNAT+SNAT 并塞进了 VXLAN overlay,既让静态路由失效,又因为 L3 设备无 MAC 产生了内层全零 MAC 的非法 VXLAN 帧。
现象与初步排查
两个 Service 都是 LoadBalancer 类型,由 MetalLB 实现,分别分配了两个 VIP,最终 DNAT 到同一个后端 Pod IP。
1 | $ kubectl get svc -n <ns> | grep if-xxx-service |
用 cilium-dbg service list 看两条链路的后端:
1 | 208 10.20.1.5:80/TCP LoadBalancer 1 => 172.18.5.34:80/TCP (active) |
两个 VIP 都指向同一个 Pod IP 172.18.5.34。
在集群内 master 节点上直接通过 Cluster IP 访问,均没有问题。这一步非常关键:它说明 Cilium 的 DNAT 和集群的 overlay 网络本身是正常的——问题只出在"从公有云经 WireGuard 进来"这条外部链路上。
排查丢包点:逐层推进
1. master 节点 Cilium 丢包监控
1 | $ kubectl exec -it -n kube-system cilium-xxx -- cilium monitor --type drop |
丢包日志里满屏的 Policy denied,但仔细看——这些全是 Pod 主动出集群到 world 的流量(目的 IP 是公网地址),与我们要查的 VIP 10.20.2.1 和 Pod 172.18.5.34 毫无关系。
⚠️ 第一个坑:
cilium monitor --type drop会把节点上所有丢包都打出来,包括与本问题无关的噪声。必须用目的 IP/Port 过滤,否则会被淹没。
结论:包不是在 master 节点的 Cilium BPF 这里丢的。
2. master 节点抓包
在 master 上抓包,看到 Cilium 对从 WireGuard 进来的流量做了三件事:
1 | 10:25:56.081452 cilium_vxlan Out ifindex 9 ... 172.18.2.240.42750 > 172.18.5.17.80: Flags [S], ... |
- 将 VIP
10.20.2.1DNAT 成后端 Pod IP172.18.5.17; - 将源 IP SNAT 成
172.18.2.240(master 节点cilium_host设备的 IP); - 把数据包送入
cilium_vxlanVTEP 设备,二次封装进 VXLAN,走隧道去往运行 Pod 的gpu 节点。
这里有个麻烦:SNAT 抹掉了真实来源。由于另一条链路是好的、推理服务正在 serving,从 Pod 侧根本看不出来到底有没有来自 VM-2 的流量到达。为了把"流量有没有真正打到 Pod"这件事变成可观测的,我部署了一个固定到 gpu 节点 的 nginx Pod,并复刻推理服务的拓扑——两个 Service、两个 VIP、同一个后端 Pod:
1 | apiVersion: apps/v1 |
得到和推理服务一致的拓扑:
- VIP
10.20.1.7→ Pod IP172.18.5.17✅ - VIP
10.20.2.2→ Pod IP172.18.5.17❌
3. nginx Pod 内抓包
只在 VM-2(问题链路)发包,在 nginx Pod 网络命名空间内抓包:
1 | $ tcpdump -nne -i eth0 tcp and port 80 |
TCP 包根本没到 Pod。
4. gpu 节点 Cilium 丢包
1 | $ kubectl exec -it -n kube-system cilium-yyy -- cilium-dbg monitor -vv --type drop --type trace --type policy-verdict | grep 172.18.5.17 |
包也不在 gpu 节点的 Cilium BPF 这里丢。
5. master bond0 出包
在 master 的 bond0 上抓 VXLAN 出包,过滤内层封包目的 IP 为 172.18.5.17:
1 | $ tcpdump -nne -i bond0 \ |
包成功从 master 出去了(走 VXLAN 隧道)。
这里其实已经出现了一条决定性线索,但我(包括一起协作排查的 AI)当时都没第一时间看出来。先按下不表。
6. gpu 节点 bond0 入包
在 gpu 节点 bond0 上抓入包:
1 | $ tcpdump -nne -s 0 -Q in -i bond0 'src host 192.168.10.192 and dst host 192.168.10.119 and udp dst port 8472 and udp[46:4] = 0xac120511' |
包也成功到达了 gpu 节点,但后面只有 SYN 重传,没有 SYN-ACK——握手建立不起来。
这里那条决定性线索第二次出现,AI 依然没能点破。
7. 用 pwru 定位内核丢包点
Cilium BPF 层没丢、bond0 有入包、Pod 却收不到——丢包只可能发生在 VXLAN 解封装到 Cilium 接管之间的内核路径。这种"内核态细粒度丢包"靠 cilium monitor 看不到了,得上 pwru(基于 kprobe 的 skb 跟踪工具):
1 | root@gpu-node:~# pwru \ |
包的释放点清清楚楚:
1 | kfree_skb_reason(SKB_DROP_REASON_NOT_SPECIFIED) vxlan_rcv[vxlan] |
包是在内核 vxlan_rcv 解 VXLAN 封装时被丢弃的。 NOT_SPECIFIED 是因为当前内核版本没有为 VXLAN 接收分支提供更细粒度的 drop reason——这也是为什么前面 cilium monitor 和常规抓包都看不到它,只能靠 pwru 这种内核态跟踪工具才能抓到。
根因:VXLAN 内层以太网 MAC 全零
定位到丢包函数后,回头对比"正常链路"和"异常链路"在 gpu 节点 bond0 上的 VXLAN 入包,差异一目了然。
正常链路的包(来自 edge-gw 那条):
1 | 23:46:11.060589 aa:bb:cc:00:00:01 > aa:bb:cc:00:00:02, ... OTV, flags [I], overlay 0, instance 6 |
异常链路的包(来自 master 那条):
1 | 23:49:00.327167 aa:bb:cc:00:00:01 > aa:bb:cc:00:00:02, ... OTV, flags [I], overlay 0, instance 2 |
对比结论:
| 正常链路 | 异常链路 | |
|---|---|---|
| VXLAN 内层源 MAC | aa:bb:cc:00:00:03(有效) |
00:00:00:00:00:00(全零) |
| VXLAN 内层目的 MAC | aa:bb:cc:00:00:03(有效) |
00:00:00:00:00:00(全零) |
| 握手结果 | 完成 | 失败 |
VXLAN 内层以太网帧的 MAC 全为 0,是丢包的直接原因。 内核在 vxlan_rcv 解封装后,拿到一个源/目的 MAC 都非法的以太网帧,无法继续处理,直接 kfree_skb 丢弃。
这就是前面两次"决定性线索"——在 master bond0 出包和 gpu bond0 入包时,tcpdump 都已经打印出了
00:00:00:00:00:00 > 00:00:00:00:00:00这行内层全零 MAC。但我(和协助排查的 AI)一开始都把它当成 tcpdump 对 L3 设备流量的"正常占位"忽略了,没有意识到它就是丢包的根因。这是一个值得反复提醒自己的教训:抓包输出里的每一行"奇怪但稳定出现"的字段,都可能是钥匙,而不是噪声。
为什么内层 MAC 会全零?
因为问题链路流量进入 master 节点的入口网卡,是 WireGuard 的虚拟网卡 wg-tunnel-2:
1 | $ ip -d link show wg-tunnel-2 |
type 65534 即 ARPHRD_NONE,这是一个 L3 设备,没有标准的以太网 MAC 地址,自然也就没有正常的以太网头。
完整的数据通路对比:
问题链路(master):
1 | 公有云 client → wg-tunnel-2 (master, L3/ARPHRD_NONE, 无 MAC) |
正常链路(edge-gw):
1 | 公有云 client → wg-tunnel-1 (edge-gw) |
更进一步的根因是:attach 在 wg-tunnel-2 上的 Cilium BPF,在 Linux 路由查找之前就把流量 NAT 并塞进了 VXLAN。 这使得原本配置在 wg-tunnel-2 上"转发到 gpu 节点"的静态路由失效——流量根本走不到那条静态路由,就被 Cilium 拐进了 overlay 隧道,而且因为 L3 设备没有 MAC,封装出的内层以太网帧 MAC 全零,最终被对端内核丢弃。
一句话:L3 设备 + Cilium BPF 提前 NAT = 内层全零 MAC 的非法 VXLAN 帧 = 对端 vxlan_rcv 丢弃。
解决方案
1. 临时修复:detach 问题网卡上的 Cilium BPF
先看一下当前 attach 在 wg-tunnel-2 ingress 上的 Cilium 程序:
1 | $ tc filter show dev wg-tunnel-2 ingress |
把它删掉:
1 | $ tc filter del dev wg-tunnel-2 ingress protocol all pref 1 handle 0x1 bpf |
detach 完成后,wg-tunnel-2 链路立即通了——流量不再被 Cilium 提前 NAT,而是正常命中静态路由、转发到 gpu 节点,从 bond0 入站,内层 MAC 有效。
⚠️ 这个修复是非持久的:Cilium node plugin 一旦重启,会重新把 BPF 程序 attach 回
wg-tunnel-2,问题会复现。
2. 持久化修复:用 CiliumNodeConfig 排除问题设备
治本的办法是告诉 Cilium:“这个节点你只接管 bond0,不要碰 wg-tunnel-2”。通过为 master 节点创建专属的 CiliumNodeConfig 实现:
1 | apiVersion: cilium.io/v2 |
应用后只重启该节点的 Cilium,让配置生效。这样 Cilium 就不会再向 wg-tunnel-2 attach BPF,流量得以按静态路由正常转发,问题彻底解决且重启不还原。
复盘与几点经验
这次排查从现象到根因绕了不短的弯路,复盘下来有几条值得记住的经验:
-
cilium monitor --type drop的噪声很大。节点上无关的 Pod 出向丢包会刷屏,必须严格按目的 IP/Port 过滤,否则会误判丢包点。 -
“内核态丢包” Cilium BPF 看不到。当
cilium monitor、cilium-dbg都查不到、bond0 又确实有入包、Pod 却收不到时,要果断上 pwru,从kfree_skb_reason反查丢包函数。本次正是靠 pwru 把丢包点精确锁到vxlan_rcv[vxlan]。 -
抓包输出里稳定出现的"奇怪字段"要当成钥匙,而不是噪声。内层全零 MAC 在 master bond0 出包、gpu bond0 入包时各出现了一次,都被我(和 AI)当成 L3 设备的正常占位忽略了。事后看,它就是根因本身。
-
L3/ARPHRD_NONE 设备是 Cilium overlay 的"天然雷区"。WireGuard、tun、IPIP 等
ARPHRD_NONE设备没有以太网 MAC,一旦 Cilium BPF 在其 ingress 上提前做 DNAT/SNAT 并封装进 VXLAN,就会产生内层全零 MAC 的非法帧,被对端内核在解封装时丢弃。这类设备原则上不应被 Cilium 接管,应通过devices/CiliumNodeConfig显式排除。 -
临时修复(
tc filter del)和持久化修复(CiliumNodeConfig)要成对给出。运维现场先用tc快速止血恢复业务,再补持久化配置防止重启复现——这是处理"急"与"治本"的标准节奏。 -
AI 协作的反向教训:排查全程我都在和 AI 一起看抓包,但它在两次关键节点(master 出包、gpu 入包的 tcpdump 输出)都没能识别出"内层全零 MAC = 根因"。这提醒我:把原始 tcpdump 输出丢给 AI 时,应当显式提示它逐字段对比正常包与异常包,而不是笼统问"这有什么问题"。AI 对"稳定出现的异常字段"的敏感度,目前仍弱于一个有经验、知道"哪些字段本不该为零"的工程师。
一句话总结:公有云经 WireGuard L3 设备进 master 节点的流量,被该网卡上 attach 的 Cilium BPF 提前 DNAT/SNAT 并封进 VXLAN,因 L3 设备无 MAC 导致内层以太网 MAC 全零,被对端内核在 vxlan_rcv 解封装时丢弃;通过 detach/排除该设备上的 Cilium BPF(临时用 tc filter del,持久用 CiliumNodeConfig 限定 devices: bond0)解决。

