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

2025.12.30 奇思妙想 780
33BLOG智能摘要
当CPU和内存波澜不惊,线上服务却频频陷入"无风起浪"的响应延迟陷阱,你是否也曾在监控面板前束手无策?这次诡异的磁盘IO毛刺排查之旅,将带你直击一个反常识的性能谜题:磁盘利用率%util突然飙至90%,但IOPS和吞吐量竟纹丝不动!资深运维博主化身系统侦探,从iostat的蛛丝马迹出发,借blktrace穿透内核块层,最终锁定真凶——那些被忽视的屏障刷新(FLUSH)请求如何在内核队列中制造"隐形堵车"。全文手把手拆解排查全链路:如何用btt分析Q2C/G2I阶段延迟、动态追踪块层队列、识别RAID卡缓存策略陷阱,并提供可落地的三重解法——从调整Write Back策略到优化fsync调用,每一步都附实战验证。读完你将掌握一套从应用层直抵硬件层的IO问题诊断心法,下次再遇同类毛刺,不再被表象迷惑。这不仅是技术复盘,更是献给运维工程师的深度调优指南:性能迷雾中,真相永远藏在内核的队列逻辑里。
— 此摘要由33BLOG基于AI分析文章内容生成,仅供参考。

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

一次诡异的磁盘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卡,这种极端保守的刷新策略往往是不必要的,反而会引入性能毛刺。

第五幕:解决方案与验证

解决方案因环境而异:

  1. 调整磁盘/RAID卡写缓存策略:如果有BBU,可以将策略从“Write Through”改为“Write Back with BBU”。(警告:此操作有数据丢失风险,需充分评估硬件可靠性)
  2. 调整文件系统挂载选项:对于非关键数据,可以考虑使用 nobarrier 挂载选项(如 mount -o nobarrier),但同样会牺牲一部分数据安全性。
  3. 优化应用写模式:与开发同学沟通,检查是否在关键路径上有过于频繁的 fsync() 调用,能否批量合并。

在我们的场景中,通过与运维同事协作,谨慎地调整了RAID卡的缓存策略。调整后,再次使用 iostatblktrace 长期观察,那诡异的、与流量不匹配的IO毛刺终于消失了,应用响应时间曲线恢复了平滑。

尾声:排查心法

这次排查给我的启示是:面对复杂的性能问题,需要建立一个从应用到硬件的完整观测链路。

  • iostat 告诉我们“病了”(有性能毛刺)。
  • blktrace/btt 告诉我们“病在何处”(阻塞在块层队列)。
  • 内核调试/动态追踪 帮我们确认“病的细节”(刷新请求阻塞)。
  • 硬件/驱动知识 最终找到“病的根因”(缓存策略)。

希望这次“抽丝剥茧”的经历,能为你下次遇到诡异的IO问题时,提供一条清晰的排查思路。性能调优的路上,我们都在不断踩坑和填坑。如果你有更精彩的排查故事,欢迎在评论区分享!

评论