磁盘IO异常飙升?从定位到根治的完整指南
发布日期: 2026/08/11 阅读总量: 1

一次备份引发的P1事故

凌晨2点17分,监控大屏弹出告警:订单库主库IO延迟超过3000ms,大量慢查询堆积。我打开终端,uptime显示load average 23.5,而平时这个数字只有2左右。

这不是第一次了。上周三跑批量任务时也出现过类似情况,当时重启了MySQL草草收场。这次我决定挖到底。

排查过程只用了20分钟,但真正的优化花了一整天。文章末尾我把所有坑都列出来了,看完你能省下这一天。

问题现场:先确认瓶颈在磁盘

服务器配置:Dell R730,12块HGST 4T 7200转SAS盘组RAID10,操作系统CentOS 7.9,MySQL 5.7.44,内核3.10.0-1160。

第一步,看CPU和负载,排除CPU瓶颈:

# 查看CPU核心数和负载
nproc  # 16
uptime # 23.5, 18.7, 12.3

# 用top看CPU状态,重点看wa(iowait)占比
top -b -n 1 | head -20
# %Cpu(s):  5.1 us,  2.3 sy,  0.0 ni, 52.3 id, 40.2 wa,  0.0 hi,  0.1 si,  0.0 st

wa占了40.2%,磁盘明显是瓶颈。iowait高不代表一定是磁盘问题,但结合负载飙升,大概率是IO。接着用iostat确认:

# 每2秒采样一次,共3次
iostat -x 2 3

# 关键输出(第三次采样)
# Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
# sda  0.00 0.00 312.5 1562.5 40960 128000 86.5 34.18 18.5 12.2 35.6 6.2 99.9
# dm-0 0.00 0.00 312.5 1562.5 40960 128000 86.5  34.18 18.5 12.2 35.6 6.2 99.9

%util跑到99.9%,直接打满。await平均18.5ms,但注意r_await只有12.2ms,w_await却到了35.6ms。写入延迟是读取的3倍,磁盘阵列的写缓存可能已经耗尽。

再确认是谁在写。用iotop直接看进程IO:

# iotop需要root权限,没有就先yum install -y iotop
iotop -b -n 5 -o -d 3 | tail -30

# 输出关键行
# TID  PRIO USER DISK READ DISK WRITE SWAPIN IO COMMAND
# 2134 be/4 mysql 0.00 B/s 118.5 M/s 0.00 % 92.3 % mysqld
# 4561 be/4 root 0.00 B/s 7602 K/s 0.00 % 3.5 % xtrabackup

找到了:mysqld每秒写118.5MB,xtrabackup在跑全量备份。凌晨2点是备份窗口,脚本直接调用了innobackupex --parallel=8,8个线程同时读表,加上binlog刷新,把磁盘阵列打死了。

根因分析:为什么同一个备份以前没事

翻了下备份脚本的改动记录,两周前把libgcrypt版本升级后,xtrabackup从2.3.8升到了2.4.21。新版本的并行拷贝逻辑不一样,默认会多开一倍线程。加上MySQL配置文件里innodb_flush_log_at_trx_commit=1(每次提交都刷盘),binlog的sync_binlog=1,写放大直接翻倍。

但根本问题不在xtrabackup,也不在MySQL参数。磁盘本身是机械盘RAID10,单盘顺序写约150MB/s,随机写约50-80MB/s。RAID10理论最大写性能是单盘2倍(镜像写入),实际随机写测试:

# 用fio测当前系统的随机写性能
fio --name=random_write --ioengine=libaio --rw=randwrite --bs=4k --size=1G --numjobs=16 --iodepth=32 --runtime=30 --group_reporting

# 结果
# write: IOPS=2145, BW=8583KiB/s, latency=162.3ms

只有2145 IOPS,延迟162ms。这个性能本身就不够看,加上备份线程疯狂读,binlog疯狂写,不死才怪。

方案对比:三种思路,我全试了

方案一:ionice给备份进程降级

思路:xtrabackup是IO密集型,但允许延迟。用ionice -c 3(idle)让备份进程只在磁盘空闲时运行。这个方案最轻量,一行命令搞定:

ionice -c 3 -p 4561

但实际效果有限。因为xtrabackup的--parallel=8会同时发起8个读线程,即使进程IO优先级降为idle,CFQ(Completely Fair Queuing)调度器在超高负载下仍会分配一部分IO时间给它。实测%util降了20%,但MySQL的慢查询还是多。

方案二:cgroup隔离IO带宽

思路:用cgroup v1的blkio子系统限制xtrabackup的读写带宽。这比ionice更硬核,直接在内核层面限制IOPS:

# 创建cgroup并限制读带宽为50MB/s
cgcreate -g blkio:/backup_limited
# 限制读带宽(设备8:0是sda的主次设备号)
echo "8:0 52428800" > /sys/fs/cgroup/blkio/backup_limited/blkio.throttle.read_bps_device
# 把xtrabackup进程加入cgroup
cgclassify -g blkio:backup_limited 4561

这个方案有效,但cgroup v1在CentOS 7默认是启用的,配置麻烦,而且需要root操作,不适合每次备份都手动执行。更适合写进脚本。

方案三:改变备份策略,先加缓存再备份

思路:不跟备份硬刚,先解决磁盘本身的短板。用flashcache或bcache给RAID10加一层SSD缓存。但这需要重启服务器、重新格式化磁盘,业务不允许。放弃。

三个方案对比后,我选了组合拳:ionice + cgroup + 调整备份脚本参数。这是改动最小、见效最快的。

完整实施:从脚本到系统配置

第一步:重构备份脚本

原来的备份脚本长这样:

#!/bin/bash
# 原始备份脚本 backup.sh
BACKUP_DIR="/data/backup/mysql"
DATE=$(date +%Y%m%d)

innobackupex --user=backup --password=xxxx --parallel=8 \
  --no-timestamp "$BACKUP_DIR/$DATE" 2>>"$BACKUP_DIR/backup.log"

改成这样:

#!/bin/bash
# 修复后的备份脚本 backup.sh
BACKUP_DIR="/data/backup/mysql"
DATE=$(date +%Y%m%d)
LIMIT_BW=51200  # 限制50MB/s读带宽

# 启动备份进程,背景执行
innobackupex --user=backup --password=xxxx \
  --parallel=4 --throttle=40 \
  --no-timestamp "$BACKUP_DIR/$DATE" 2>>"$BACKUP_DIR/backup.log" &
BACKUP_PID=$!

# 给备份进程设置ionice idle调度
ionice -c 3 -p $BACKUP_PID

# 用cgroup限制读写带宽
if [ -d /sys/fs/cgroup/blkio/backup_limited ]; then
  cgclassify -g blkio:backup_limited $BACKUP_PID
else
  cgcreate -g blkio:/backup_limited
  echo "8:0 52428800" > /sys/fs/cgroup/blkio/backup_limited/blkio.throttle.read_bps_device
  echo "8:0 52428800" > /sys/fs/cgroup/blkio/backup_limited/blkio.throttle.write_bps_device
  cgclassify -g blkio:backup_limited $BACKUP_PID
fi

# 等待备份结束
wait $BACKUP_PID

# 清理cgroup
rmdir /sys/fs/cgroup/blkio/backup_limited 2>/dev/null
echo "Backup finished: $(date)"

注意--throttle=40:这个参数是xtrabackup内置的IO限速,单位是每秒IO操作次数。文档说默认0(不限速),我实测设成40,备份时间从35分钟增加到52分钟,但IO压力降了60%。数据说话:

配置备份耗时磁盘utilMySQL慢查询数
原配置(parallel=8, 无throttle)35分钟99.9%187
parallel=4 + throttle=4052分钟68%64
parallel=4 + throttle=40 + ionice + cgroup55分钟42%12

第二步:调整MySQL IO相关参数

备份期间,MySQL自己也在刷脏页、写binlog。为了降低备份期间的IO争抢,我临时调整了MySQL参数(不是永久改,备份完改回来):

-- 降低脏页刷盘频率,减少后台IO
SET GLOBAL innodb_max_dirty_pages_pct = 30;
SET GLOBAL innodb_io_capacity = 800;
SET GLOBAL innodb_io_capacity_max = 1600;
-- 降低binlog sync频率,从每次提交改为每秒
SET GLOBAL sync_binlog = 5;

这几条SQL执行后,iostatw_await从35ms降到了12ms。

第三步:优化系统层IO调度

机械盘用deadline比cfq更适合数据库场景。检查当前调度器:

cat /sys/block/sda/queue/scheduler
# 输出:noop [deadline] cfq   (说明已经是deadline)

# 如果不是,临时修改
echo deadline > /sys/block/sda/queue/scheduler

# 永久修改:在grub内核参数里加 elevator=deadline
sed -i 's/GRUB_CMDLINE_LINUX="\(.*\)"/GRUB_CMDLINE_LINUX="\1 elevator=deadline"/' /etc/default/grub
grub2-mkconfig -o /boot/grub2/grub.cfg

deadline调度器对读请求有优化,会优先合并相邻的读操作。备份是大量顺序读,deadline把读请求合并成大块,能显著提升吞吐量。

第四步:验证优化效果

第二天凌晨备份窗口,盯紧监控,最终数据:

iostat -x 2 3

# 备份期间的输出
# Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
# sda  0.00 0.00 512.5 682.5 64512 65536 96.5 8.31 8.2 7.8 9.1 4.5 42.5

%util从99.9%降到42.5%,await从18.5ms降到8.2ms。MySQL慢查询从187条降到12条,应用侧接口P99延迟从2.8秒降到400毫秒。

备份耗时从35分钟增加到55分钟,但凌晨2点的备份窗口是2小时,完全能接受。

避坑指南:这4个坑我全踩过

坑1:ionice对deadline调度器无效

第一次用ionice -c 3后,发现iostat的变化微乎其微。查了一下,ionice依赖CFQ调度器实现优先级控制。而CentOS 7默认用的是deadline。所以ionice在deadline下就是摆设。必须配合cgroup或用--throttle参数。

坑2:cgroup的blkio设备号写错

blkio.throttle.read_bps_device需要设备主次设备号。用lsblk看:

lsblk
# NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
# sda 8:0 0 3.5T 0 disk
# ├─sda1 8:1 0 1G 0 /boot
# └─sda2 8:2 0 3.5T 0 /

如果写错设备号,cgroup规则不生效,而且不会报错。要用cat /sys/fs/cgroup/blkio/backup_limited/blkio.throttle.read_bps_device确认写入成功。

坑3:xtrabackup的--throttle参数只对读有限制

误以为--throttle同时限制读和写。实际看xtrabackup源码,throttle只影响读线程。如果备份文件要写到另一块盘,写带宽还是要靠cgroup或系统IO调度。

坑4:sync_binlog调小后主从延迟反而变大

为了降低IO,把sync_binlog从1调到5,结果从库延迟从秒级涨到分钟级。原因是主库binlog刷盘频率降了,从库拉取binlog后要等主库定期刷盘才能读到新数据。这个参数要看主从架构谨慎调整。单机可以调,有从库就别动。

复盘:这次事故的完整时间线

  • 02:17 告警触发,P1会议拉响
  • 02:19 确认wa高,CPU不是瓶颈
  • 02:22 iostat确认sda打满,w_await异常高
  • 02:25 iotop定位到mysqldxtrabackup疯狂写入
  • 02:30 暂停备份,手动kill xtrabackup进程
  • 02:35 MySQL恢复正常,告警解除
  • 03:00 开始调优,14:00 全部优化完成

止损动作就是kill掉备份进程。但根治花了11个小时。这11小时里,大部分时间浪费在验证ionice没效果、查cgroup配置、看xtrabackup文档上。希望你看完这篇,直接把方案抄走,不要重走这些弯路。

最后总结一句:磁盘IO瓶颈排查,先看iostat定位,再用iotop抓真凶,最后根据业务选方案——别上来就调内核参数,先找到是谁在打IO。