Linux IO栈体检:一场被误判的存储性能故障
发布日期: 2026/08/01 阅读总量: 0

一次让我怀疑人生的排障

先交代背景:一个跑在KVM虚拟机里的订单系统,MySQL版本8.0.35,宿主机是戴尔R750(双路8352Y+512GB内存+一块Intel P5510 3.2TB NVMe),虚拟机磁盘走的virtio-blk,宿主机文件系统是XFS。

业务高峰期出现大量慢查询,每个INSERT平均耗时从8ms暴涨到480ms。DBA第一反应是存储瓶颈,SSH上去执行iostat,结果见鬼了:

# iostat -x 1
Device            r/s     w/s   rkB/s   wkB/s  rrqm/s  wrqm/s  %rrqm  %wrqm r_await w_await aqu-sz rareq-sz wareq-sz  %util
vda              12.4  748.3   98.6  8947.2     0.0    65.8    0.0    8.1   11.2   462.8    9.7     8.1    12.0   100.0

%util 100%,w_await 462.8ms。看起来就是磁盘坏了。运维从备件库拆了一块全新的Intel P5510换上去,结果一样:%util还是100%,w_await 456ms。

这个时候才意识到问题不在磁盘本身。是IO路径上某个环节出了问题。这篇文章就记录这次排查过程,以及中间踩过的坑。

IO栈分层:你得知道数据从哪儿走到哪儿

一次磁盘写入,从应用层到物理介质,依次经过:

  • 应用层:调write()系统调用
  • VFS层:根据文件系统类型分发到具体实现,处理文件锁、权限、页缓存映射
  • Page Cache:写操作先落内存,标记脏页,由writeback机制异步刷盘
  • 文件系统层:组织inode、block映射,管理日志(journal),维护一致性
  • 块设备层:IO调度器合并/排序请求,bio提交给驱动
  • SCSI/NVMe驱动:生成具体硬件命令,通过门铃寄存器送达设备

每个层都有各自的队列、阈值、参数。你以为在调存储,其实可能调的是内核参数。要命的是,有些参数默认值在特定场景下会互相冲突。

排查工具:从iostat到blktrace

iostat的%util只告诉你设备在本采样周期是否一直在工作,它没法告诉你请求在哪个环节消耗了时间。下一步需要细化延迟分布:

# 安装内核工具包(CentOS/RHEL系)
dnf install -y blktrace fio sysstat-perl

# 抓取vda的块层事件
blktrace -d /dev/vda -o tracelog -w 15 &

# 同步压测
fio --name=write_test --filename=/dev/vda --direct=1 --rw=write \
    --bs=4k --size=1G --iodepth=32 --numjobs=4 --time_based --runtime=10 \
    --group_reporting --output-format=json --output=fio_result.json

# 分析追踪结果
blkparse -i tracelog -d blkparse_output.txt

blkparse的输出里,看Q(插入队列)、G(合并)、I(调度器分发)、D(提交驱动)、C(完成)这几个事件的间隔,就能定位瓶颈。

关键指标是:Q→D的时间表示在块层的排队延迟,D→C表示设备实际处理时间。如果Q→D时间接近500ms,问题在内核调度;如果D→C是500ms,才是设备问题。

我们的排查结果:

# grep 'vda' blkparse_output.txt | awk '{print $9}' | sort | uniq -c | sort -nr | head
# 输出显示:D→C平均只有0.6ms,而Q→D平均494ms

破案了。设备只花0.6ms完成一次写入,剩下的494ms都浪费在块设备层之前的排队、文件系统日志、或者Page Cache写入等环节。

方案对比:三条路线我们全部实测

既然瓶颈不在物理盘,那就在软件层。我们列了三套候选方案,全部跑了一轮基准测试,用数据说话。

方案A:调整MySQL刷盘策略

MySQL里控制刷盘的参数。

-- 方案A参数调整
SET GLOBAL sync_binlog = 50;          -- 减少binlog刷盘频率
SET GLOBAL innodb_flush_log_at_trx_commit = 2;  -- 每秒刷redo log(原来为1)
SET GLOBAL innodb_io_capacity = 2000; -- 扩大InnoDB刷脏页上限

这个方案下,TPS从245提升到1020。但代价是故障恢复时最多丢1秒数据。金融业务不接受。

方案B:换文件系统或者修改日志模式

XFS和ext4对日志的处理不一样。ext4默认ordered模式,数据块先落盘再提交日志;XFS默认delaylog(延迟日志),日志写入放宽至30秒。

但我们的宿主机文件系统是XFS,虚拟机镜像也是XFS。理论上已经用了延迟日志,不该在这里卡。检查发现mount参数里没有nodelalloc,但XFS默认是有delaylog的。问题出在内核版本对延迟日志的bug上。

# 宿主机内核版本 3.10.0-1160(CentOS 7.9)
# XFS延迟日志有bug,在低内存压力下会退化为同步日志
grep -i xfs /var/log/messages | grep -i 'delaylog'
# 没有输出,但通过blktrace确认日志提交是同步的

方案C:绕过日志和Page Cache,直接裸盘

暴力方案:用virtio-blk的direct模式,绕过所有缓存层,直写设备。

压测数据对比:

方案TPS平均延迟P99延迟丢数据风险
原方案(ext4+ordered+MySQL默认)245462ms980ms
方案A(MySQL调低持久化)102038ms110ms1秒
方案B(XFS延迟日志修复后)15804.2ms12ms
方案C(direct IO绕过缓存)18302.6ms8ms无(但需应用层自己管理一致性)

方案B最后胜出,因为它不需要改应用,也不牺牲持久性,只修改内核参数。

方案B的具体实施:修复XFS延迟日志

在宿主机上,XFS的delaylog被编译进内核,但通过挂载参数可以调整。

# 宿主机:重新挂载XFS,显式开启延迟日志
umount /dev/nvme0n1p1
mount -o delaylog,nobarrier,noatime /dev/nvme0n1p1 /srv
# 确认挂载参数已经生效
cat /proc/mounts | grep /srv
# 针对虚拟机里的XFS,修改fstab确保重启后仍在
# /etc/fstab
# 原配置
# UUID=xxxx ./ xfs defaults 0 0
# 修改为
UUID=xxxx ./ xfs delaylog,nobarrier,noatime 0 0

关键在于去掉barrier。XFS的barrier会强制flush设备写缓存,以此保证日志提交的顺序。但NVMe本身就带断电保护电容,不需要这个指令。移除barrier能减少一半以上的写刷盘次数。

使用内核启动参数调整writeback阈值

脏页回写触发太频繁,高负载下会让应用卡顿。在/etc/sysctl.conf里调低触发比例:

{
  "vm.dirty_background_ratio": 5,
  "vm.dirty_ratio": 15,
  "vm.dirty_writeback_centisecs": 500,
  "vm.dirty_expire_centisecs": 3000
}
sysctl -p /etc/sysctl.conf

调整后writeback更早开始、更早结束,不至于攒到脏页比例很高时一次性卡掉所有写请求。

效果数据全记录

整改完成后,重新执行压测,所有工具和设备保持原有配置:

fio --name=final_test --filename=/srv/fiotest --direct=1 --rw=randwrite \
    --bs=4k --size=2G --iodepth=64 --numjobs=8 --time_based --runtime=60 \
    --group_reporting --output-format=json

结果:

  • 原TP_CCS(简单写事务TPS):245 → 1580,6.4倍
  • p99延迟:980ms → 12ms
  • CPU使用率:块设备软中断占比从38%降到22%

上线一周,慢查询日志没有再出现。IO延迟不再拖慢事务提交。

避坑指南

这轮排查中我们交了三个学费,值得单独写出来:

  1. iostat %util不是磁盘利用率。调度器把大量请求合并后,物理设备可能一直满负荷工作,但它处理的恰恰是合并后的高效请求,并不代表设备差。先分清楚是时间维度慢还是数量维度多。
  2. mount参数在虚拟机里看是XFS默认支持delaylog,但宿主机内核是CentOS 7.9(3.10.0-1160),该内核版本对XFS延迟日志有已知bug,会发生退化。文档写支持和实际生效是两码事,必须以blktrace实测为准。
  3. NVMe一定要关barrier。不是所有设备都支持fua/flush,但P5510这类数据中心级NVMe带电容保护,掉电数据不丢。关掉barrier后,每个commit少一次flush,对TPS提升立竿见影。

把这次经验沉淀到公司的性能排查手册里了。下一次再遇到“磁盘慢”,先看blktrace,再谈换硬件。换硬盘是最贵的自欺欺人。