小Cの已经记不起来的博客

用 eBPF 观察容器网络丢包的基础实验步骤

最近排查一个容器网络问题,应用侧说偶发超时,宿主机上看接口也没红,ss 里重传倒是有一些。想用 eBPF 看看包到底在哪儿没的,顺手把最小实验步骤记一下,免得下次又从零开始翻命令。

先说清楚,这个实验不是要复现某个特别复杂的生产故障,只是搭一个能稳定丢包的容器网络环境,然后用 bpftrace 去观察内核里跟丢包相关的事件。实验做完,至少能知道 kfree_skb、qdisc drop、TCP 重传这几类信号大概长什么样。

先把实验环境缩到最小

我这里用 Docker 起一个带网络工具的临时容器,镜像用 nicolaka/netshoot,里面 ping、tcpdump、tc 都有,省得自己装。宿主机上需要 bpftrace,没有的话先装:

sudo apt install bpftrace
# 或者
sudo dnf install bpftrace

bpftrace 一般要 root,或者至少要有 CAP_BPF、CAP_PERFMON 之类的权限。实验环境里直接 sudo 最省事。

起容器:

docker run -d --name loss-demo --cap-add=NET_ADMIN nicolaka/netshoot sleep 100000

这里加 NET_ADMIN 是为了后面能在容器里改 qdisc。然后看一眼容器网络:

docker exec loss-demo ip addr
docker exec loss-demo ip route

默认 bridge 网络下,一般会看到 eth0 和默认网关,比如 172.17.0.1。后面就用这个网关做 ping 目标。

制造一个可重复的丢包

别急着上 eBPF,先用 tc netem 在容器 eth0 上制造 20% 的出方向丢包:

docker exec loss-demo tc qdisc add dev eth0 root netem loss 20%
docker exec loss-demo tc -s qdisc show dev eth0

然后 ping 一下网关:

docker exec loss-demo ping -c 20 172.17.0.1

这时候应该能看到比较明显的丢包。tc -s qdisc show dev eth0 里也会累积 dropped 计数。这个计数很关键,它是后面跟 eBPF 结果对照的基线。

实验结束后记得删掉 qdisc,不然容器一直丢包:

docker exec loss-demo tc qdisc del dev eth0 root

用 kfree_skb 看内核释放点

容器网络里的包最终都要经过内核协议栈,很多丢包路径最后会走到 kfree_skb 或 kfree_skb_reason。先看看当前内核暴露了哪些 tracepoint:

sudo bpftrace -l 'tracepoint:skb:*'
sudo bpftrace -lv 'tracepoint:skb:kfree_skb'

不同内核版本字段不完全一样,有的有 reason,有的没有。先按最稳妥的方式,用 location 和 comm 聚合:

sudo bpftrace -e '
tracepoint:skb:kfree_skb /comm == "ping"/
{
  @[ksym(args->location), comm] = count();
}
interval:s:5
{
  print(@);
  clear(@);
}
'

这里只过滤 comm == "ping",是因为 kfree_skb 不只会被丢包触发,正常包被消费时也会调用。不过滤的话输出会非常多,容易看花眼。ksym(args->location) 是把内核地址转成符号,能大致看出是哪个调用点释放的 skb。

如果你的内核支持 reason 字段,可以再加一列:

sudo bpftrace -e '
tracepoint:skb:kfree_skb /comm == "ping"/
{
  @[args->reason, ksym(args->location)] = count();
}
'

如果提示字段不存在,就把 args->reason 删掉。别硬套命令,先 -lv 看字段最靠谱。

再看 qdisc 的丢包事件

netem loss 这种丢包更直接发生在 qdisc 层。可以观察 qdisc drop:

sudo bpftrace -l 'tracepoint:qdisc:*'
sudo bpftrace -e '
tracepoint:qdisc:qdisc_drop
{
  @[comm, args->reason] = count();
}
'

同样,字段名以 bpftrace -lv 'tracepoint:qdisc:qdisc_drop' 为准。这个输出和 tc -s qdisc show dev eth0 里的 dropped 计数能对上,基本就能确认丢包发生在容器 eth0 的出方向 qdisc,而不是更后面的 bridge 或宿主机协议栈。

TCP 重传和抓包交叉验证

如果是 ICMP,丢包表现比较直接。换成 TCP 服务时,还可以看 TCP 重传:

sudo bpftrace -e '
tracepoint:tcp:tcp_retransmit_skb
{
  @[comm] = count();
}
'

有些内核上这个 tracepoint 名字可能不一样,可以先用 bpftrace -l 'tracepoint:tcp:*' 找一下。没有的话就用 kprobe:tcp_retransmit_skb 代替,思路一样。

再配合抓包做交叉验证。容器里面抓:

docker exec loss-demo tcpdump -ni eth0 icmp

宿主机上抓 bridge:

sudo tcpdump -ni docker0 icmp

因为我在容器 eth0 上加了出方向 netem loss,所以有可能容器里 tcpdump 还能看到一部分包,但宿主机 docker0 上看到的数量明显少一些。这个差异本身就能说明包在离开容器网络命名空间前后被丢掉了。

几个别踩的坑

第一,kfree_skb 不等于丢包。正常包处理完也会释放,所以一定要结合 reason、location、comm 或端口来过滤。

第二,bpftrace 在宿主机跑,看到的是整个内核范围的事件,不是只看到某个容器。容器多的时候,靠 comm、PID、端口过滤只是临时办法,后面可以再研究 cgroup id 或 netns 过滤。

第三,eBPF 适合看“内核在哪个点处理了这个包”,不适合替代抓包。实际排查里,tc -s、ss -ti、tcpdump 和 eBPF 一起看,结论会踏实很多。

最后把实验容器清掉:

docker rm -f loss-demo

这套步骤不复杂,但足够把容器网络丢包的基本观察链路跑通。真到生产环境里,把 netem 换成实际故障场景,把过滤条件换成对应容器和端口,剩下的就是慢慢对事件了。

评论

还没有评论。

发表评论

提交后评论将经过自动审核,审核通过后公开展示。

未在播放