一次诡异的磁盘IO毛刺排查:从iostat到内核块层的抽丝剥茧

大家好,我是33blog的博主。最近在维护的一个线上服务集群中,频繁出现间歇性的应用响应延迟飙升,但CPU和内存使用率却波澜不惊。这种“无风起浪”的性能问题最是磨人。经过初步定位,怀疑的矛头指向了磁盘IO。今天,我就和大家分享一下这次从监控指标一路深挖到Linux内核块层的排查之旅,过程堪称一部微型侦探小说。
第一幕:案发现场与初步勘查(iostat)
问题发生时,第一反应就是祭出磁盘IO观测的“瑞士军刀”——iostat。我使用了一个稍微详细的命令,希望能捕捉到瞬间的波动:
iostat -x 1 10
输出结果中,%util(磁盘利用率)大部分时间在10%以下,但每隔几分钟就会突然飙到90%以上,持续2-3秒。与此同时,await(IO请求平均等待时间)也从平时的个位数ms暴涨到几百ms甚至上秒级。然而,一个诡异的现象出现了:每秒读写请求数(r/s, w/s)和读写吞吐量(rkB/s, wkB/s)在毛刺期间并没有显著增加。
踩坑提示:这里第一个迷惑点就来了。通常我们认为 %util 高意味着磁盘忙,理应伴随着高IOPS或高吞吐。但现实是,磁盘“感觉”很忙,却没什么“实际工作”。这暗示问题可能不在磁盘本身的物理速度,而在于IO请求在提交给磁盘之前的某个环节就被卡住了。
第二幕:深入调度队列(blktrace & blkparse)
为了看清IO请求的一生,我们需要更强大的工具——blktrace。它能跟踪一个IO请求从块设备层(Block Layer)下发,到最终完成返回的完整路径。我针对问题磁盘(假设是 /dev/sdb)进行抓取:
# 开始追踪,抓取10秒数据
blktrace -d /dev/sdb -w 10
# 追踪结束后,在当前目录会生成多个 sdb.blktrace.* 文件
# 使用 blkparse 解析并生成人类可读的报告
blkparse -i sdb -d sdb.blkparse.bin
接着,使用 btt 工具来分析生成的二进制数据,重点关注IO在各个环节的耗时:
btt -i sdb.blkparse.bin
分析 btt 输出的时间分布图,一个关键发现浮出水面:在毛刺期间,IO请求在 Q2C(从块设备层队列到设备驱动队列的耗时)和 G2I(从通用块层到IO调度层的耗时)阶段出现了异常长的延迟。
这指向了Linux内核的块设备层(Block Layer) 和 IO调度器(如mq-deadline, kyber, bfq)。请求在进入磁盘驱动队列前,在内核队列里等待了过久。
第三幕:聚焦内核队列与调度器
我们检查一下该磁盘使用的调度器:
cat /sys/block/sdb/queue/scheduler
输出可能是:[mq-deadline] kyber bfq none,表示当前使用的是 mq-deadline。
接下来,查看块设备层的队列深度和相关统计:
cat /sys/block/sdb/queue/nr_requests
cat /sys/block/sdb/queue/max_sectors_kb
同时,在毛刺发生时,我动态观察了更内核层的统计信息(需要内核编译时开启相关调试,或使用 bpftrace/systemtap 等动态追踪工具)。这里我采用一个简单的 bpftrace 脚本来采样块层队列的延迟:
#!/usr/bin/env bpftrace
kprobe:blk_mq_start_request
{
@start[tid] = nsecs;
}
kprobe:blk_mq_end_request
/@start[tid]/
{
$latency = (nsecs - @start[tid]) / 1000; // 转换为微秒
@us = hist($latency);
delete(@start[tid]);
}
运行这个脚本后,发现在毛刺时刻,延迟直方图 @us 的高位桶(例如几万到几十万微秒,即几十到几百毫秒)计数明显增多,证实了请求在块层队列中停留了异常时间。
第四幕:真凶浮现——不合理的屏障刷新(Barrier/Flush)
那么,是什么让块层队列“堵车”了呢?结合应用日志和系统其他线索(如 dmesg),我最终将注意力集中在了 磁盘刷新(Flush)请求 上。
某些文件系统操作(如fsync、sync)或数据库提交日志,会下发屏障(Barrier)或刷新(Flush)请求,要求在此之前的所有数据必须落盘后,才能处理之后的请求。这是一个同步操作,会阻塞队列。
使用 blktrace 时,可以通过 blkparse 过滤查看这些刷新请求:
blkparse -i sdb.blkparse.bin -a issue -a complete | grep -E ‘F[S|L]’
果然,在每次毛刺出现的时间点附近,都观察到了 FLUSH 请求。并且,这些FLUSH请求的完成时间长得不合理。根本原因是什么呢?进一步排查发现,是磁盘本身(或RAID卡缓存策略)对刷新请求的处理模式导致的。该磁盘在收到刷新请求时,会以一种非常保守且同步的方式清空其内部缓存,这个过程会暂时阻塞整个队列。
实战经验:对于SSD或带有电池备份缓存(BBU)的RAID卡,这种极端保守的刷新策略往往是不必要的,反而会引入性能毛刺。
第五幕:解决方案与验证
解决方案因环境而异:
- 调整磁盘/RAID卡写缓存策略:如果有BBU,可以将策略从“Write Through”改为“Write Back with BBU”。(警告:此操作有数据丢失风险,需充分评估硬件可靠性)。
- 调整文件系统挂载选项:对于非关键数据,可以考虑使用
nobarrier挂载选项(如mount -o nobarrier),但同样会牺牲一部分数据安全性。 - 优化应用写模式:与开发同学沟通,检查是否在关键路径上有过于频繁的
fsync()调用,能否批量合并。
在我们的场景中,通过与运维同事协作,谨慎地调整了RAID卡的缓存策略。调整后,再次使用 iostat 和 blktrace 长期观察,那诡异的、与流量不匹配的IO毛刺终于消失了,应用响应时间曲线恢复了平滑。
尾声:排查心法
这次排查给我的启示是:面对复杂的性能问题,需要建立一个从应用到硬件的完整观测链路。
- iostat 告诉我们“病了”(有性能毛刺)。
- blktrace/btt 告诉我们“病在何处”(阻塞在块层队列)。
- 内核调试/动态追踪 帮我们确认“病的细节”(刷新请求阻塞)。
- 硬件/驱动知识 最终找到“病的根因”(缓存策略)。
希望这次“抽丝剥茧”的经历,能为你下次遇到诡异的IO问题时,提供一条清晰的排查思路。性能调优的路上,我们都在不断踩坑和填坑。如果你有更精彩的排查故事,欢迎在评论区分享!


iostat这工具确实好用,排查io问题必备