一次备份引发的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%。数据说话:
| 配置 | 备份耗时 | 磁盘util | MySQL慢查询数 |
|---|---|---|---|
| 原配置(parallel=8, 无throttle) | 35分钟 | 99.9% | 187 |
| parallel=4 + throttle=40 | 52分钟 | 68% | 64 |
| parallel=4 + throttle=40 + ionice + cgroup | 55分钟 | 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执行后,iostat的w_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定位到mysqld和xtrabackup疯狂写入 - 02:30 暂停备份,手动kill xtrabackup进程
- 02:35 MySQL恢复正常,告警解除
- 03:00 开始调优,14:00 全部优化完成
止损动作就是kill掉备份进程。但根治花了11个小时。这11小时里,大部分时间浪费在验证ionice没效果、查cgroup配置、看xtrabackup文档上。希望你看完这篇,直接把方案抄走,不要重走这些弯路。
最后总结一句:磁盘IO瓶颈排查,先看iostat定位,再用iotop抓真凶,最后根据业务选方案——别上来就调内核参数,先找到是谁在打IO。