磁盘I/O慢得像老牛拉车,你该怎么办?——Linux bpftrace
故障定位思路
折腾Linux bpftrace时,我发现最麻烦的往往不是安装,而是配置。线上服务器突然响应变慢,数据库查询超时,应用日志里全是“等待磁盘I/O”的告警。你熟练地敲下iostat -x 1,看到await飙到几百毫秒,%util接近100%——磁盘确实成了瓶颈。可接下来呢?iostat只能告诉你“磁盘慢了”,却无法回答“是哪个进程导致的?”“是读还是写?”“延迟发生在驱动程序层还是硬件层?”
传统排查手段要么信息太粗(如iostat、sar),要么侵入性太强(如strace会拖慢进程)。有没有一种工具,既能深入内核追踪每一个I/O请求的生命周期,又几乎不影响线上性能?答案就是bpftrace——基于eBPF的动态追踪利器。
为什么传统工具很难定位根因?
配置前的检查
我们先看一张I/O请求的简化路径图:
- 用户态进程发起
read()/write()系统调用 - 经过VFS层、文件系统层、块设备层(Block Layer)
- 最终通过驱动程序提交给磁盘硬件
iostat是从/proc/diskstats读取累计计数器,每秒钟一次采样。它能给出平均延迟、队列长度、IOPS等,但无法告诉你:
- 延迟的分布是均匀还是两极分化?
- 高延迟是发生在排队等待还是硬件处理?
- 具体哪个进程在大量写数据?
于是我们常常陷入“调优参数靠感觉”的困境。
进阶阅读:此处可内链到“Linux bpftrace性能优化”指南。
延伸阅读:此处可内链到“Linux bpftrace配置案例”相关文章。
eBPF和bpftrace到底是什么?
配置前的检查
eBPF(extended Berkeley Packet Filter)是一项革命性的内核技术,它允许用户在Linux内核中安全地运行沙箱化的程序,而不需要修改内核源码或加载内核模块。你可以把它想象成“内核里的JavaScript”——编写的小程序可以在特定事件(系统调用、函数入口/出口、网络包到达、块设备请求等)触发时执行,收集数据。
bpftrace则是基于eBPF的高级追踪语言,类似于awk的语法。你只需写一两行命令,就能定义要追踪的内核探针、提取变量、输出统计结果。它属于只读追踪,不会修改内核行为,生产环境也可以放心使用(但注意高频率追踪可能增加少量CPU开销)。
补充参考:此处可内链到“Linux bpftrace故障排查实例”。
Linux bpftrace:安装bpftrace
实际操作要点
大多数现代发行版(Ubuntu 18.04+、CentOS 7+、Debian 10+)都可以直接安装:
# Ubuntu / Debian
sudo apt-get install bpftrace
# CentOS / RHEL 8+
sudo dnf install bpftrace
# 也可以从源码或官方GitHub release安装
安装后验证:sudo bpftrace --version。
实战:用bpftrace追踪磁盘I/O延迟
1. 查看块设备请求的延迟分布(直方图)
这是最直接的切入方式。bpftrace提供了一款生产级脚本blkflow.bt(位于/usr/share/bpftrace/tools/),不过为了说明原理,我们自己写一个简单的one-liner:
sudo bpftrace -e 'kfunc:blk_account_io_done { @ = hist(args->rq_end_io_time - args->rq_start_time_ns); }' -c 'sleep 5'
解释:
kfunc:blk_account_io_done:内核函数,在块设备I/O完成时被调用。我们用内核动态插桩(kfunc)追踪它。args->rq_end_io_time - args->rq_start_time_ns:计算请求完成时间减去开始时间,得到延迟(纳秒)。@ = hist(...):将延迟数据放入一个直方图变量@中。-c 'sleep 5':追踪5秒后退出。
输出示例:
@:
[1M, 2M) 120 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[2M, 4M) 45 |@@@@@@@@@@@@@@@@@@@ |
[4M, 8M) 12 |@@@@@ |
[8M, 16M) 3 |@ |
[16M, 32M) 1 | |
这里单位是微秒(1M=1000微秒=1毫秒)。可以看到大部分请求在1-2ms完成,但少数请求延迟高达16-32ms。如果直方图出现明显的“长尾”,说明存在周期性高延迟,可能是磁盘硬件队列深度或文件系统锁的问题。
2. 找出延迟最高的具体进程
光知道延迟分布还不够,我们要找到“肇事”进程。bpftrace可以同时捕获进程的PID和进程名:
sudo bpftrace -e 'kfunc:blk_account_io_done {
$lat = args->rq_end_io_time - args->rq_start_time_ns;
if ($lat > 10000000) { // 延迟超过10ms才记录
printf("PID %d (%s) delay %lld usn", pid, comm, $lat / 1000);
}
}' -c 'sleep 10'
这个脚本会实时打印出那些I/O延迟超过10ms的进程信息。你可以结合ps或者top判断该进程为什么会有大量磁盘访问。
3. 区分读延迟和写延迟
磁盘读和写的延迟特征不同,写操作可能因为回写缓存、磁盘缓存策略而表现不同。我们可以通过args->rq_data_len和args->rq_opf来区分读写:
sudo bpftrace -e 'kfunc:blk_account_io_done {
$op = args->rq_opf & 1;
$lat = args->rq_end_io_time - args->rq_start_time_ns;
if ($op == 0) { @read_hist = hist($lat); }
else { @write_hist = hist($lat); }
}' -c 'sleep 10'
分别输出读延迟直方图和写延迟直方图,帮你判断性能瓶颈类型。
4. 按设备号统计延迟
如果系统有多块磁盘(比如SSD+HDD),或者使用LVM、RAID,你可能想知道哪一块盘延迟高。可以按设备主次设备号分组:
sudo bpftrace -e 'kfunc:blk_account_io_done {
@dev_lat[args->rq_dev] = hist(args->rq_end_io_time - args->rq_start_time_ns);
}' -c 'sleep 5'
设备号可以通过ls -l /dev/sda查看(比如8:0)。
关联教程:此处可内链到“Linux bpftrace部署与验证”内容。
从数据到根因:三步定位法
我的处理经验
拿到bpftrace的输出后,按照以下思路分析:
- 看延迟长尾:如果直方图有远离主峰的小尾巴,说明存在偶尔的慢请求。慢请求通常由磁盘内部碎片整理、NCQ队列管理等引起,可以尝试调整磁盘调度器(如mq-deadline改为none)或增加IOPS。
- 看延迟最高的进程:如果是数据库进程(例如mysqld、postgres),可能是某个慢查询全表扫描;如果是日志进程(rsyslogd),可能是大量日志写入导致磁盘饱和。结合
iostat -x 1 -p sda确认%util是否很高。 - 区分读写行为:如果写延迟明显高,检查磁盘是否启用了Write Cache(
hdparm -W /dev/sda),或者考虑调整文件系统的挂载参数(如noatime)。如果读延迟高,检查是否有大量随机读(可以再用bpftrace追踪平均请求大小)。
生产环境安全回滚
配置前的检查
bpftrace脚本是只读的,不会修改内核数据。但注意:
- 高频率追踪(如每次I/O都printf)会增加CPU开销,建议添加过滤条件(如只追踪延迟>阈值)或设置采样。
- 使用
-c指定运行时间,或者用Ctrl+C手动停止,不要一直跑在后台。 - 如果怀疑某个bpftrace脚本导致系统异常,直接重启服务或卸载ceph等工具,实际上只需要停止bpftrace进程即可恢复,无需回滚内核。
想继续深入:此处可内链到“Linux bpftrace优化清单”文章。
总结
验证与回滚
bpftrace让Linux磁盘I/O延迟排查从“黑盒统计”升级为“白盒跟踪”。你不再需要猜测,而是可以直接看到内核里每一个I/O请求的出生和死亡。本文演示的只是冰山一角——你还可以追踪VFS层、文件系统、SCSI驱动等更深层次的函数。掌握bpftrace,等于拥有了一把透视服务器内核的钥匙。
下次再遇到磁盘I/O高延迟,别急着怀疑硬件坏了,先跑一个bpftrace直方图,让数据说话。后续只要定期检查关键指标,Linux bpftrace就不会变成维护负担。
延伸阅读
