马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?用户注册
×
数据库主机磁盘 IO 瓶颈排查实战:从 iostat 到 fio 压测
一、具体的问题
上周三晚批处理窗口,业务方报批量导入比平时慢了近一倍。登上数据库主机一看:CPU 不高(us 12%,sy 6%),内存充裕,但 load average 4.8,vmstat 的 b 列长期挂着 3~4 个不可中断进程,wa 列动辄 25% 往上。应用端的表现是:单条 insert 平时 2ms,高峰期飙到 200ms 以上;MySQL 错误日志里每隔几分钟就有一条 "page cleaner" / checkpoint 落后的告警。
很多人第一反应是"加块盘"或者"换 NVMe"。但先把问题拆开看:到底是哪块盘在忙?是读慢还是写慢?是单次 IO 本身就慢(设备延迟高),还是请求排队太多(队列积压)?是业务流量涨了,还是某个后台任务(备份、归档、监控 agent)在抢 IO?不搞清楚这些就动手换硬件,多半是把钱花在了不疼的地方。
这台主机上一共三块盘:sda 放系统和监控,sdb 放 MySQL 数据文件,sdc 放 redo/binlog。批处理窗口变慢,嫌疑最大的自然是 sdb 和 sdc。下面按实际排查顺序走一遍。
二、核心原理
第一,iowait 高不等于磁盘慢。 wa 只表示"CPU 里有比例的时间在等 IO 完成",它是 IO 需求和 CPU 空闲的相对值。业务低峰期 CPU 几乎空闲,哪怕磁盘一般忙,wa 也可能显示 30%+;反过来业务高峰期 CPU 满载,同样多的 IO 等待在 wa 里的占比反而被摊薄。wa 只能当"有 IO 等待"的信号,不能当"磁盘慢"的结论。
第二,%util 在现代设备上严重失真。 %util 的原始定义是"采样周期内至少有一个 IO 在处理的时间占比"。对机械盘单队列时代,它近似于"盘有多忙";但对 NVMe 和 RAID 卡背后的多队列设备,盘可以同时处理几十上百个 IO,%util 到 100% 只说明"这一秒里始终有 IO 在飞",离饱和可能还远。看饱和度要看 aqu-sz(平均队列深度)和 await 的变化趋势,别盯着 %util 下结论。
第三,区分设备延迟和排队延迟。 iostat -x 里的 await 是"每个 IO 从进队列到完成的总时间"(含排队),它高可能是设备本身慢,也可能是排队多。内核 4.18 以后的 iostat 输出里 svctm 已废弃(算出来就是个推导值,不要再用),判断设备本身快慢更可靠的办法是:低负载时段压一个单深度的 fio(iodepth=1),那个数字才是设备的裸延迟。
第四,数据库的 IO 是"放大"过的。 应用写一行 200 字节,落盘路径上可能是:一个脏页(16KB)的刷写、一次 redo fsync、一次 binlog 写、主从再各同步一次。做容量和延迟预算时要以数据库视角算放大倍数,而不是看应用侧的写入量。这也是为什么 redo 盘和数据盘混放时,提交延迟(commit)会被刷脏页连累——fsync 在同一块盘上排队。
三、实例参考(动手步骤)
步骤 0:确认现状基线
- # 每秒采样一次,连采 10 次,看全局
- iostat -x 1 10
- # 重点关注:r/s w/s、rkB/s wkB/s、r_await w_await、aqu-sz、%util
- # 同步看 CPU 侧
- vmstat 1 10 # b 列=不可中断进程数,wa=IO 等待占比
复制代码
当晚的采样结果(批处理窗口):
| 设备 | w/s | wkB/s | w_await | aqu-sz | %util | | sdb(数据盘) | 820 | 52400 | 86.4 | 12.3 | 98.9 | | sdc(日志盘) | 3100 | 9800 | 4.1 | 0.8 | 22.4 |
数据盘 sdb 写延迟 86ms、队列深度 12,明显是瓶颈在 sdb;日志盘 sdc 健康。问题收敛到一半了。
步骤 1:定位是哪个进程在打 IO
- # 按进程看读写吞吐,采样 10 次
- pidstat -d 1 10
- # 或者交互式看(需要 iotop)
- iotop -oPa
复制代码
结果发现除了 mysqld(w=38MB/s),还有一个 arch_ckpt.sh 起的 tar 进程在读 sdb 写 sda,速率 25MB/s——是归档脚本和批处理撞了窗口。
步骤 2:从数据库侧看谁在刷
- -- MySQL 8.0:按文件看 IO 排名
- SELECT file_name, count_read, sum_timer_read/1e12 AS read_s,
- count_write, sum_timer_write/1e12 AS write_s
- FROM performance_schema.file_summary_by_instance
- ORDER BY sum_timer_write DESC LIMIT 10;
- -- 看 checkpoint 压力
- SHOW GLOBAL STATUS LIKE 'Innodb_checkpoint%';
- SELECT VARIABLE_VALUE FROM global_status
- WHERE VARIABLE_NAME IN ('Innodb_buffer_pool_wait_free','Innodb_log_waits');
复制代码
Innodb_log_waits 期间涨了 4000 多,说明 redo 写入在等日志盘之外的资源——结合步骤 0,是数据盘拖累的脏页刷写跟不上。
步骤 3:测设备的裸延迟(低峰期做)
- # 单深度随机写,测裸延迟(务必对测试文件,别打业务数据文件!)
- fio --name=lat --filename=/data/fio_test --direct=1 --rw=randwrite \
- --bs=4k --iodepth=1 --numjobs=1 --time_based --runtime=30 \
- --size=2G --group_reporting
复制代码
sdb 单深度 4k 随机写延迟 0.9ms——设备本身没问题。结论修正:不是盘慢,是请求量叠加排队(820 写/秒 × 数据+日志混刷)。这直接改变了治理方向:不需要换盘,需要错峰和限流。
步骤 4:治理落地
- # 1) 归档脚本加 ionice + nice,并挪到凌晨 3 点执行
- # crontab 改为:
- # 0 3 * * * ionice -c3 nice -n19 /opt/scripts/arch_ckpt.sh
- ionice -c3 nice -n19 tar -czf /backup/db_1009.tgz /data/mysql/db1/
- # 2) 限流批处理的写入并发(应用侧改 batch 串行→半并发,从 32 线程降到 8)
- # 3) buffer pool 刷脏节奏对齐盘能力(sdb 实测可承受约 60MB/s 持续写)
- SET GLOBAL innodb_io_capacity = 1200;
- SET GLOBAL innodb_io_capacity_max = 2400;
- SET GLOBAL innodb_flush_neighbors = 0; -- SSD 不需要邻页刷新
- # 4) 后台刷脏提前铺路
- SET GLOBAL innodb_max_dirty_pages_pct = 60;
- SET GLOBAL innodb_max_dirty_pages_pct_lwm = 10; -- 低水位就开始匀速刷
复制代码
步骤 5:压测验证(下次变更窗口)
- # 用业务同规格写法压 sdb,确认治理后的可持续吞吐
- fio --name=wr --filename=/data/fio_test --direct=1 --rw=randwrite \
- --bs=16k --iodepth=32 --numjobs=4 --time_based --runtime=120 \
- --size=8G --group_reporting --invalidate=1
复制代码
压测拿到的 IOPS 和延迟曲线,就是下次做容量规划和告警阈值的依据。
四、实操检查清单
- 每次登录先跑 iostat -x 1 10 + vmstat 1 10,把 b 列、wa、各盘 w_await/r_await、aqu-sz 记进巡检表,留基线。
- 判断瓶颈先分层:设备裸延迟(低峰 fio 单深度)→ 排队(aqu-sz 与 await 走势)→ 请求来源(pidstat -d 定位进程)。
- %util 只当参考,NVMe/RAID 设备不看 %util 判断饱和;svctm 一律忽略。
- 数据文件、redo/binlog、备份归档分盘存放;归档/备份任务一律 ionice -c3 + 挪出业务批处理窗口。
- innodb_io_capacity 与实测盘能力对齐,flush_neighbors 在 SSD 上关掉,低水位刷脏(pct_lwm)开起来。
- fio 压测只打测试文件,绝不在业务数据文件系统上直接 rw=randwrite 覆盖;压测前确认路径。
- 每月用 fio 复测一次裸延迟,环比变化超 30% 触发硬件检查(RAID 卡电池、盘 SMART)。
- 监控侧对数据盘加两级告警:w_await 持续 >20ms 预警、>50ms 告警。
几个容易踩的坑
把 iowait 高直接当存储慢。 先区分"有 IO 在等"和"设备处理不动",后者要用裸延迟压测证明,否则换盘是玄学投资。
%util 100% 就判盘饱和。 多队列设备上这是常态,看 aqu-sz 和 await 才有效。
用 svctm 下结论。 这个指标早被内核废弃,iostat 新版本算出来的值没有意义。
fio 直接打业务路径。 曾有同行在数据目录里 rw=randwrite 把文件覆盖了——压测文件名、路径、size 三项每次都要人工确认两遍。
只调数据库参数不动应用。 批量导入 32 并发写单表,任何存储都扛不住,先限并发再看存储。
治理前后对比
| 指标 | 治理前 | 治理后 | | sdb w_await(批处理窗口) | 86.4ms | 1.8ms | | sdb aqu-sz | 12.3 | 1.6 | | Innodb_log_waits(窗口期) | 4200+ | 0 | | 批处理耗时(同量数据) | 3h52m | 2h05m | | 单条 insert P99 | 210ms | 6ms | | 归档任务与批处理撞车 | 每周 2~3 次 | 0(错峰+ionice) |
这次问题的根因其实不是"盘不行",而是两股写流量在同一块盘上撞车外加刷脏策略不匹配。方向对了以后,没花一分钱硬件预算就把延迟压回来了。下次再看到 await 飙高,先按这套顺序走一遍:基线 → 定位进程 → 数据库侧交叉验证 → 裸延迟压测定性 → 错峰/限流/参数对齐。存储问题十有八九,最后修的是调度和配置。 |