← 行间 技术 / eBPF
技术

用 eBPF 观测容器网络延迟

把内核态的探针挂在 tcp_probe 上,绕开采样盲区,第一次把 P99 抖动的来源看清楚。这篇记录了从 bpftrace 一行脚本,到写一个常驻 collector 的完整过程,以及几个踩过的坑。

采样的盲区

传统的指标采集每隔十五秒抓一次快照,P99 抖动往往就藏在两次采样之间。要看清楚一次重传到底卡在哪里,得把观测点下沉到内核,挂在真正发生事件的地方。

你没法优化你看不见的东西,而采样让一半的世界不可见。

指标是聚合后的结果,它告诉你「变慢了」,却不告诉你「为什么」。而事件流保留了因果:哪一个连接、在哪一跳、被什么阻塞。eBPF 让我们能在不改一行业务代码的前提下,把这些事件捞出来。

bpftrace 一行脚本

先用 bpftrace 快速验证想法。下面这段把 tcp_rcv_established 的往返时间按毫秒分桶,几秒钟就能画出一张直方图:

// rtt 直方图,按毫秒
kprobe:tcp_rcv_established {
  @rtt = hist(args->tp->srtt_us >> 3);
}

直方图证实了怀疑:长尾集中在特定的几个 pod 上。接下来把它做成常驻的 collector,持续吐给 Prometheus。

写一个常驻 collector

一行脚本适合探索,但不能长期挂着——它把所有数据留在内核 map 里,需要用户态定期读取并清空。常驻 collector 要做三件事:

  • BPF_MAP_TYPE_HASH(pid, saddr, daddr) 聚合 rtt;
  • 用户态每秒 bpf_map_lookup_and_delete_batch 拉取一次;
  • 转成 Prometheus 的 histogram,打上 pod 标签。

关键是批量读取:逐条 lookup 在高连接数下会把 CPU 吃满,batch 接口能把系统调用从每连接一次降到每秒一次。

几个踩过的坑

第一个坑是 verifier。挂在热点函数上的程序,循环边界必须是编译期常量,否则加载直接被拒。第二个坑是 srtt_us 的单位——它是 8 倍的微秒值,右移 3 位才是真正的微秒。第三个坑最隐蔽:容器里的 pid 和宿主机不是一回事,得通过 bpf_get_current_pid_tgid 拿到宿主机视角的 pid,再回查 cgroup 才能对上 pod。

绕过这三个坑之后,长尾的来源终于清晰:一个共享节点上的邻居 pod 在做大流量拷贝,挤占了网卡队列。这是任何应用层指标都看不到的。