一次让我怀疑人生的排障
先交代背景:一个跑在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默认) | 245 | 462ms | 980ms | 无 |
| 方案A(MySQL调低持久化) | 1020 | 38ms | 110ms | 1秒 |
| 方案B(XFS延迟日志修复后) | 1580 | 4.2ms | 12ms | 无 |
| 方案C(direct IO绕过缓存) | 1830 | 2.6ms | 8ms | 无(但需应用层自己管理一致性) |
方案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延迟不再拖慢事务提交。
避坑指南
这轮排查中我们交了三个学费,值得单独写出来:
- iostat %util不是磁盘利用率。调度器把大量请求合并后,物理设备可能一直满负荷工作,但它处理的恰恰是合并后的高效请求,并不代表设备差。先分清楚是时间维度慢还是数量维度多。
- mount参数在虚拟机里看是XFS默认支持delaylog,但宿主机内核是CentOS 7.9(3.10.0-1160),该内核版本对XFS延迟日志有已知bug,会发生退化。文档写支持和实际生效是两码事,必须以blktrace实测为准。
- NVMe一定要关barrier。不是所有设备都支持fua/flush,但P5510这类数据中心级NVMe带电容保护,掉电数据不丢。关掉barrier后,每个commit少一次flush,对TPS提升立竿见影。
把这次经验沉淀到公司的性能排查手册里了。下一次再遇到“磁盘慢”,先看blktrace,再谈换硬件。换硬盘是最贵的自欺欺人。