用 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 换成实际故障场景,把过滤条件换成对应容器和端口,剩下的就是慢慢对事件了。
评论
还没有评论。
发表评论
提交后评论将经过自动审核,审核通过后公开展示。