大家好,我是数据库小学妹 👋
上周凌晨3点,被一通电话吵醒。运维说从库CPU飙了,应用在超时,让我赶紧看看。
我爬起来打开电脑,迷迷糊糊登录服务器,top一看——mysqldump。
当时心里就咯噔一下,备份怎么会搞出这么大动静?
排查下来才发现,事情一环扣一环:备份锁表,主从延迟飙升,应用跟着超时。从3点05分告警到3点52分恢复,整整47分钟,我那天基本没怎么睡。
今天把这次排查过程整理出来。
故障现场
告警群在3点05分炸的,消息一条接一条。从库CPU 100%,主从延迟从0飙到300秒,应用开始报超时。
我登录从库服务器,top一看,mysqldump进程CPU占用80%多,IO也在高位。再看从库状态:
SHOW SLAVE STATUS
Seconds_Behind_Master已经300多了,而且还在涨。
备份为什么会锁表
mysqldump备份时会执行FLUSH TABLES WITH READ LOCK(FTWRL),获取全局读锁。这个锁会阻塞所有写操作。
为什么mysqldump要加这个锁?因为它需要拿到一个一致性位点(consistent snapshot)。备份过程中如果有写操作,备份出来的数据就不一致了——比如你备份到一半,某个表被truncate了,备份出来的数据就缺了一块。
FTWRL的执行分三步:
- Close tables — 关闭所有已打开的表,把脏页刷到磁盘。这一步会等所有正在执行的语句结束
- Acquire global read lock — 拿到全局读锁,之后所有写操作被阻塞
- Get binlog position — 记录当前binlog位点,作为一致性快照的参照
问题就出在第一步。如果有大查询在跑,比如一个跑了20分钟的报表JOIN,Close tables会一直等它结束。关键是,这期间FTWRL已经开始阻塞新的写操作了——锁已经在排队,但还没拿到,新的写请求全卡在那。
更坑的是,如果你用的是MyISAM引擎,--single-transaction参数根本没用。这个参数只对InnoDB生效,通过开启一个一致性读事务来避免FTWRL。MyISAM没有事务机制,只能老老实实加全局锁。
我那个场景,从库上有个报表查询在跑,执行了20多分钟。FTWRL等这个查询结束,期间所有写操作都卡住了。
备份锁表为什么会影响主库
备份锁表影响的是从库,但为什么会波及主库?这个问题当时我也想了好一会。
主从复制的基本流程:主库执行写操作 → 写入binlog → 从库IO线程拉取binlog → 从库SQL线程回放。从库锁表后,回放被阻塞,binlog在从库这边就堆积起来了。
半同步复制下问题更严重。 如果你配置了半同步复制(semi-sync replication),主库提交事务后要等至少一个从库确认收到binlog才返回客户端。从库锁表导致回放卡住,binlog确认也卡住,主库的写操作直接被拖慢。我遇到的场景就是半同步,主库的TPS从3000掉到了个位数。
binlog堆积会撑爆磁盘。 主库的binlog要等从库确认接收后才能清理。从库一直不确认,主库的binlog就一直涨。我遇到过一次,binlog涨了50多G,磁盘直接告警。如果你配了expire_logs_days,MySQL会尝试清理过期binlog,但如果还有从库没读完,它不敢删,磁盘空间就这么被撑满。
并行复制场景下影响更大。 如果从库开了MTS(Multi-Threaded Slave),多个worker线程并行回放binlog。FTWRL一锁,所有worker线程全卡住,延迟蹭蹭往上涨。我见过延迟从0飙到3000秒的,就是因为MTS的协调线程在等全局锁。
还有个容易忽略的点:从库延迟变大,如果有读写分离配置,应用读到的是旧数据。用户下单成功,刷新页面发现订单没出来,投诉就来了。
应用为什么会超时
从库锁表、主从延迟,应用为什么会超时?
如果应用配置了读写分离,读请求会发到从库。从库锁表后,读请求被阻塞,等待锁释放。
应用有超时设置,比如30秒。从库锁表时间超过30秒,应用就报超时。
超时后应用可能会重试,重试又打到从库,又被阻塞。大量请求堆积,连接池被耗尽,应用开始报"连接池已满"。
我那个场景,应用配置了3次重试。每次重试都等30秒,一个请求就等90秒。并发一上来,连接池很快就满了。
排查过程
排查这个故障用了大概20分钟。说实话,刚看到CPU 100%的时候,我第一反应是磁盘满了或者内存泄漏,完全没想到是备份的锅。
第一步:看系统资源,排除硬件问题。
top -c
iostat -x 1 3
df -h
top一看,mysqldump进程CPU占用80%多。iostat的%util也在高位,说明IO也在打满。磁盘空间还有余量,排除磁盘满的可能。
到这一步我基本确认是mysqldump的问题了,但还得往下查影响面。
第二步:看从库复制状态,确认延迟程度。
SHOW SLAVE STATUS\G
重点看这几个字段:
Seconds_Behind_Master: 347
Slave_SQL_Running: Yes
Slave_IO_Running: Yes
Last_Error: (空)
SQL线程和IO线程都在跑,没有报错,说明不是复制本身出了问题,而是回放被阻塞了。Seconds_Behind_Master 347秒,而且还在涨,这个延迟量级不是小问题。当时心里就凉了半截。
第三步:看从库进程,找阻塞原因。
SHOW PROCESSLIST;
输出里看到大量这样的状态:
State: Waiting for table flush
我愣了一下——等了几秒才反应过来,这是FTWRL的特征。说明有全局读锁在排队。
第四步:找备份进程,确认根因。
ps aux | grep mysqldump
果然,凌晨3点的定时备份任务在跑。命令大概是:
mysqldump --all-databases --master-data=2 > /data/backup/full.sql
注意这里没加--single-transaction,所以会走FTWRL。
第五步:确认锁的范围。
SHOW OPEN TABLES WHERE In_use > 0;
SELECT * FROM performance_schema.metadata_locks
WHERE OBJECT_TYPE = 'GLOBAL';
SHOW OPEN TABLES可以看到哪些表被锁了。如果你用的是MySQL 5.7+,performance_schema.metadata_locks能更精确地看到锁的类型和持有者。
第六步:评估主库影响。
SHOW BINARY LOGS;
binlog数量在涨,说明从库确认跟不上,主库的binlog清理不了。
SHOW GLOBAL STATUS LIKE 'Rpl_semi_sync_master_no_tx';
这个值在涨,说明半同步复制已经在退化成异步了——主库等不到从库确认,开始降级。
第七步:看应用日志,评估业务影响。
应用日志里大量Connection pool exhausted和Query timeout。这里我走了一小段弯路——一开始以为是应用代码bug,查了半天才发现是从库锁表传导过来的。
定位到问题后,KILL了备份进程。主从延迟大概5分钟开始下降,应用在10分钟左右恢复正常。
解决方案
这次故障后,我做了几个改进。
用xtrabackup替代mysqldump
xtrabackup是Percona的物理备份工具,最大的优势是不需要FTWRL。它的工作原理分两个阶段:
备份阶段: 直接拷贝InnoDB的数据文件(.ibd),同时持续监听并拷贝redo log。因为拷贝的是物理文件,不需要锁表来保证一致性——后续的修改都记录在redo log里。整个过程只加一把轻量级的backup lock(MySQL 8.0是LOCK INSTANCE FOR BACKUP),不会阻塞DML操作。
Prepare阶段: 备份完成后,用redo log对数据文件做crash recovery,把备份期间的修改回放进去,得到一个一致性快照。这一步在备份结束后离线做,不影响线上业务。
# 备份
xtrabackup --backup --target-dir=/data/backup/ \
--user=backup_user --password=xxx
# Prepare(应用redo log,保证一致性)
xtrabackup --prepare --target-dir=/data/backup/
还有一个容易被忽略的好处:xtrabackup支持增量备份。第一次全量备份后,后续只拷贝变化的数据页,备份速度快很多,对IO的影响也小。
# 增量备份(基于上次全量)
xtrabackup --backup --target-dir=/data/backup/inc1 \
--incremental-basedir=/data/backup/full
如果只能用mysqldump怎么办
有些场景你可能没法装xtrabackup,那mysqldump也不是不能用,关键是要加对参数:
mysqldump --single-transaction --routines --triggers \
--master-data=2 --flush-logs --all-databases > /data/backup/full.sql
--single-transaction会开启一个一致性读事务,对InnoDB表不需要FTWRL。但有两个前提条件:
- 备份期间不能有DDL操作(ALTER TABLE、CREATE INDEX等),DDL会隐式提交事务,破坏一致性
- MyISAM表还是会锁,如果你还有MyISAM表的话
备份窗口规划
备份时间避开业务高峰,晚上10点到凌晨2点是备份窗口,其他时间不备份。这个看起来简单,但很多团队的定时任务是随便写的,凌晨3点恰好是某些报表任务的高峰期。
建议在cron里用nice和ionice限流,避免备份打满IO:
nice -n 19 ionice -c2 -n7 xtrabackup --backup --target-dir=/data/backup/
这样备份进程的CPU和IO优先级都是最低的,不会抢占业务资源。
监控前置
在主从延迟超过10秒时就告警,而不是等到300秒。我后来用PMM(Percona Monitoring and Management)搭了一套监控,设了三级告警:
- 延迟 > 10秒:钉钉通知
- 延度 > 60秒:电话告警
- 延迟 > 300秒:自动触发应急预案
另外备份进程也要监控。写个简单的脚本,备份开始和结束都打个时间戳到监控系统,如果备份超过预设时间(比如2小时)还没结束,也要告警。
长期架构改进
如果业务量继续增长,从库的压力会越来越大。几个方向可以考虑:
- 专用备份从库 — 单独起一个从库专门做备份,不承担业务读流量。备份延迟不影响业务
- 延迟从库 — 配置
CHANGE MASTER TO MASTER_DELAY=N,故意延迟N小时。万一主库误删数据,可以从延迟从库捞回来 - 逻辑备份+物理备份结合 — 物理备份做全量恢复用,逻辑备份做跨版本迁移和单表恢复用,两种工具各有各的场景,别只备一种
避坑清单
- 能用xtrabackup就别用mysqldump,物理备份走redo log保证一致性,不需要全局锁。如果只能用mysqldump,一定加
--single-transaction,但注意这个参数对MyISAM无效 - 备份前先SHOW PROCESSLIST看一眼从库有没有大查询在跑,我那次就是没检查,FTWRL卡了20多分钟才拿到锁。备份窗口也别随便定,凌晨3点看起来冷门,可能是报表高峰期
- 从库锁表会波及主库,尤其是半同步复制场景,主库TPS可能直接掉到个位数。主从架构一定要提前评估备份的影响面,业务量大的话建议搭专用备份从库,把备份压力和业务读流量隔离开
你们线上备份用的什么方案?有没有踩过类似的坑?评论区聊聊 👇
我是数据库小学妹,咱们下篇见 👋