大家好,我是内核喵。
在生产排障中,很多同学喜欢一上来就 strace -p 或者开 tcpdump 抓大包。但凡在每秒几万 QPS 的核心服务上这么干过的人,大概率都吃过应用直接被挂起(ptrace 的全局挂起开销)或者丢包丢到怀疑人生的亏。传统的 top、iotop、netstat 粒度太粗,而 perf 采样在偶发性毛刺面前又显得像在“抽奖”。
要做到低开销、内核级全路径、确定性的现场还原,eBPF 是目前唯一的解。今天不聊假大空的概念,直接上 3 个生产级的排障实战场景,带大家看看怎么用 bpftrace 和 BCC 顺藤摸瓜揪出底层的鬼。
一、 隐蔽的 I/O 阻塞:谁偷走了主线程的 100ms?
场景:某 Go 编写的缓存服务,P99 延迟偶尔会从 0.5 ms 飙升到 80 ms。应用层打点只能看到某个写操作变慢,但不知道是阻塞在 Page Cache 回写、磁盘队列,还是等待 futex。
线上直接挂 strace 会导致严重的性能衰减,我们用一行 bpftrace 挂载到 VFS 层的 vfs_write,直接统计耗时分布:
bpftrace -e '
kprobe:vfs_write {
@start[tid] = nsecs;
}
kretprobe:vfs_write /@start[tid]/ {
$dur = (nsecs - @start[tid]) / 1000; // 微秒
if ($dur > 10000) { // 超过 10ms 的慢写入
printf("comm: %s, pid: %d, latency: %d us\n", comm, pid, $dur);
@[kstack()] = count();
}
delete(@start[tid]);
}
'
排查结果分析: 我们在输出中抓到了关键的内核调用栈:
comm: cache_worker, pid: 182421, latency: 42103 us
@[
ext4_file_write_iter+0x120
new_sync_write+0x112
vfs_write+0x1cd
ksys_write+0x5f
do_syscall_64+0x33
entry_SYSCALL_64_after_hwframe+0x44
]: 14
进一步配合 perf 查看内核调用链,发现耗时集中在 ext4 的日志提交阶段(jbd2 事务等待)。根因是同机有日志收集组件在疯狂刷盘,挤占了 Journal 的 I/O 资源。
二、 网络栈迷踪:定位诡异的 TCP 偶发重传
场景:微服务间 RPC 调用偶尔报 Connection reset by peer 或超时重试。网络团队反馈交换机和物理链路无丢包,监控看网卡也没有 drop 计数。
不用去抓海量的 pcap 文件,内核中所有 TCP 重传必然会流经 tcp_retransmit_skb 函数。我们直接挂载 BPF 探针捕获重传瞬间的五元组与网络套接字状态:
# 使用 bpftrace 捕获内核重传事件
bpftrace -e '
#include <net/sock.h>
#include <linux/tcp.h>
kprobe:tcp_retransmit_skb {
$sk = (struct sock *)arg0;
$inet = (struct inet_sock *)arg0;
$daddr = ntop($sk->__sk_common.skc_daddr);
$saddr = ntop($sk->__sk_common.skc_rcv_saddr);
$dport = $sk->__sk_common.skc_dport;
$dport = ($dport >> 8) | (($dport << 8) & 0xff00); // 大小端转换
printf("TIME: %llu | PID: %d(%s) | %s -> %s:%d | State: %d\n",
nsecs, pid, comm, $saddr, $daddr, $dport, $sk->__sk_common.skc_state);
}
'
运行后迅速抓到了特定节点的重传流量:
TIME: 3849102847192 | PID: 0(swapper/3) | 10.0.1.12 -> 10.0.2.45:8080 | State: 1
结合捕获到的时间戳,发现对端处于 TCP_ESTABLISHED (1) 状态,但 RTT 突然出现阶梯式上升。顺着追踪对端的 tracepoint:sock:inet_sock_set_state,证实是对端宿主机的 listen() backlog 队列满了,直接丢弃了三次握手后的 ACK,导致服务端协议栈不断要求客户端重传。
三、 幽灵耗时:Off-CPU 阻塞分析
很多同学排查性能瓶颈只看 On-CPU(CPU 跑满),但后端吞吐上不去,更多时候是因为线程处于 Off-CPU 状态(等待锁、等待 I/O、被调度器抢占等)。
perf 默认抓不到被移出 CPU 的上下文。利用 eBPF 挂载调度器核心事件 sched:sched_switch,我们可以精准记录线程被切走的时长与调用栈。
使用 BCC 自带的 offcputime 工具:
# 追踪 PID 为 8972 的应用,捕获离开 CPU 超过 1000 微秒(1ms)的栈,持续 10 秒
offcputime-bpfcc -p 8972 -m 1000 10 > offcpu.stacks
输出的堆栈聚合结果直接暴露了元凶:
__schedule+0x2cd
schedule+0x43
futex_wait_queue_me+0xb9
futex_wait+0x13d
do_futex+0x138
__x64_sys_futex+0x8a
do_syscall_64+0x33
entry_SYSCALL_64_after_hwframe+0x44
pthread_mutex_lock
-- cache_worker (8972)
7230104 us (累计阻塞 7.2 秒)
较真一下:
你看,进程总 CPU 利用率可能只有 20%,但它的关键 Worker 线程居然在 10 秒内有 7.2 秒都被挂在 pthread_mutex_lock(底层走 futex)上。这不是 CPU 算力不足,而是业务代码里锁粒度过大导致的严重线程竞争。
喵的总结
排查系统问题,最忌讳凭感觉“猜”。
- 别迷信传统工具:当排查进入毫秒级、微秒级延迟领域,
strace开销太大,top分辨率太低。 - 理解内核上下文:eBPF 不是银弹,它是一把精准的手术刀。你得先清楚
VFS -> Block -> Device的路径,或者 TCP 状态机的跃迁,才能把探针准确挂在最关键的kprobe或tracepoint上。 - 安全第一:在生产环境编写 eBPF 脚本时,尽量使用只读的 Tracepoint,控制好 Map 的内存占用,避免在高频路径(如
netif_receive_skb)做过多的字符串格式化打印。
多翻翻内核源码,用数据说话。欢迎在评论区贴出你们遇到过的诡异调度或网络 Bug,咱们一起用探针深挖到底。
License: CC BY-NC 4.0
Updated 10 hours ago
Was this article helpful? Give it a like.
0 comments


