📌今日关键词:CPU 100%、从库备份、锁表、Full GC、级联故障、线程池、故障复盘、DBA
大家好,我是数据库小学妹 👋
凌晨3点手机震醒的时候,我整个人是懵的。
CPU 100%告警。我之前猜过慢SQL、连接风暴、甚至以为是攻击。
直到亲身经历了一次。根因居然是从库的一个备份任务。
今天完整复盘一下。
故障时间线
凌晨3点15分。收到CPU 100%告警。
打开监控一看,Java应用CPU打满了。JVM堆内存Old区也快顶到头。Full GC每几秒就触发一次。
我当时第一反应是:是不是有慢SQL?连上去看了眼数据库:
SHOW PROCESSLIST;
结果吓了一跳。大量查询的状态全是 Waiting for table lock。连接池被占满了。
再查锁的情况:
SHOW ENGINE INNODB STATUS;
看到了一个熟悉的关键词:FLUSH TABLES WITH READ LOCK。
从库正在跑物理备份。这个命令会锁全库。所有读查询全被挡住了,一个接一个排队。
根因:从库备份锁表
问题出在备份策略。从库物理备份时执行了这个命令:
FLUSH TABLES WITH READ LOCK;
备份工具靠它来拿到数据文件的一致性快照。但代价是锁释放之前,所有查询全部阻塞。
主库正常写入,从库应用日志也正常。但应用侧的从库读请求全部超时堆积了。
完整故障链
这个故障是典型的级联故障。我后来画了张图才彻底搞明白。
一切从备份锁表开始。FLUSH TABLES WITH READ LOCK 拿到全局读锁后,所有从库读查询被阻塞。查询进来了但跑不了,就在连接池里排队等着。
问题来了。每个排队的查询,对应的JDBC连接和ResultSet对象都还占着内存。查询越来越多,线程池里的活跃连接一路飙升。堆内存迅速上涨,Old区很快被打满。
到这一步其实还有救。但JVM发现Old区满了,开始疯狂触发Full GC。问题是这些被阻塞的线程对象根本回收不掉——它们还在"活着",在等锁。GC跑了一遍又一遍,什么都回收不了。
我后来用jstat确认了这一点:
jstat -gcutil <pid> 1000
Full GC次数在飞涨,每次回收后Old区占用几乎没降。当时那堆数字我看不太懂,后来才知道这叫"GC风暴"——GC线程和业务线程抢CPU,但业务线程都在等锁,CPU基本全被GC吃掉了。
最终结果:Java应用线程池耗尽,CPU 100%,服务完全不可用。
从一个备份锁表到整个服务崩掉,一环扣一环,每一步都不致命,但叠加起来就要命。
快速止血
找到从库正在执行备份后,手都在抖。先止血再说。
第一步,kill掉从库的备份进程,释放全局读锁:
# 找到备份进程
ps aux | grep xtrabackup
# kill掉
kill -9 <pid>
第二步,重启Java应用,等线程池释放,GC恢复。
从发现问题到恢复服务,花了将近20分钟。说实话,当时脑子一片空白,走了不少弯路。如果监控更完善,应该能更快定位。
后来我怎么改的
止血之后,更得解决根因。不然下次还会踩。
当时真的很后悔没早点换备份方案。FLUSH TABLES WITH READ LOCK 是老方案了,锁全库这个特性,平时不觉得有问题,一出事就是大事。后来换成了 XtraBackup,备份期间不锁表,从根上断了这个隐患:
# XtraBackup 热备份,不需要全局锁
xtrabackup --backup --target-dir=/data/backup/
然后加了备份监控。备份开始和结束都记录时间。超过预期时长立刻告警,不能等出了事才查。
连接超时也调了。从30秒降到10秒。之前设太长了,请求排队30秒才超时,积压量翻了好几倍。
最后是告警分层。之前只盯CPU和数据库延迟,线程池和内存都没监控。这次加上了:线程池使用率80%就告警,别等到100%才反应。
我的教训
回头看,这次故障其实挺冤的。
备份策略居然没评估过锁表影响。备份方案选型的时候只看了"能不能备份成功",没想过备份期间对业务有什么影响。这是最关键的失误。
监控也缺了一大块。平时只盯CPU和数据库延迟,线程池和内存根本没管。这次jstat的Full GC数据是排查的关键线索,但平时根本没看过这个指标。连接超时也设太长了,排队30秒才超时,积压起来根本刹不住。
还有从库读请求缺少降级机制。数据库一挂,应用也跟着挂。如果应用侧有熔断,至少写入不会受影响。
排查的突破口是jstat的Full GC数据异常。顺着GC找到线程积压,再顺着线程找到锁表。和上篇讲的思路一样,先看现象,再一层层追。
避坑清单
- 备份方案选型时,先搞清楚备份期间数据库还能不能正常服务。不是"能备份"就行
- 生产环境首选 XtraBackup 这类热备份工具,备份期间不锁表
- 连接超时设太久,故障时请求会疯狂排队。积压比故障本身更致命
- 线程池使用率 80% 就该告警,等到 100% 服务已经挂了
- 数据库超时要有熔断,不能一个故障拖垮整套服务
- jstat 的 GC 数据别忽略,异常时能帮你快速缩小排查范围
- 备份任务要监控时长,超时说明有问题,不能等出了事才查
- 告警要分层。CPU、线程池、内存、IO 都要覆盖,只盯 CPU 会漏信号
- 凌晨 3 点先止血拉服务,根因白天再慢慢查。恢复永远优先排障
- 不复盘的故障迟早会重演
这次给我最深的教训是:备份方案不能只看"能不能备份成功"。备份期间对业务的影响,才是真正的评估重点。
级联故障的触发条件往往很普通。一个备份任务,一个锁表操作,每个环节单独看都没什么大不了。但叠在一起,系统就扛不住了。
上篇讲了排查工具和方法。这篇讲了真实故障场景。两篇结合起来,下次遇到CPU 100%就不慌了。
我是数据库小学妹,咱们下篇见 👋