SRE 应急止血清单制定:针对核心数据库 CPU 飙高至 95% 的标准化降级止血预案
在大促保障的实战中,每一个 SRE 架构师最不愿意看到、却又必须准备万全的“地狱级故障”,莫过于核心关系型数据库(如 MySQL / PostgreSQL)的 CPU 监控曲线突然笔直冲顶,瞬间钉死在95% 甚至 100%。
一旦核心主库的 CPU 被打满,整个系统的吞吐量会在几十秒内发生断崖式下跌:
数据库内部的线程并发(Threads_running)从正常的几十暴增到数千,行锁与表锁竞争加剧;上游几百个微服务的数据库连接池(HikariCP)在几秒内被彻底抽干;订单中心、支付中心相继瘫痪,网关层的 504 错误像海啸一样在监控大屏上蔓延。
此时,如果现场值班人员慌乱失措,做出了“重启数据库”或者“拉上开发慢慢分析 SQL 执行计划”的错误决定,系统就彻底没救了——在千万级未提交事务和高并发写入下,重启数据库意味着漫长的 Redo Log 崩溃恢复(Crash Recovery),主库可能需要 20 到 40 分钟才能重新拉起,这个时间足以让公司蒙受上千万元的直接资损。
SRE 在极端故障面前必须遵循一条冷酷的铁律:“止血永远先于排查”。在大促备战阶段,必须为数据库 CPU 飙高制定一套不讲情面、按秒级推进的标准化应急降级止血清单(Emergency Stop-Bleeding Runbook)。
黄金三分钟止血四部曲
面对 CPU 95% 的濒死状态,值班负责人的大脑不能有任何迟疑,必须严格按照以下四步组合拳强制压降负载:
[00:00 - 00:30] ── 证据链固化:自动抓取现场活跃连接与慢 SQL 快照 [00:30 - 01:30] ── 暴力斩首:执行 pt-kill 批量秒杀堆积的超长读查询 [01:30 - 02:30] ── 业务降级:网关层一键切断所有非核心旁路流量穿透 [02:30 - 03:30] ── 入口限流:通过数据库代理(ProxySQL)开启流量硬性配额第一步:30 秒内锁定“犯罪现场”
在敲下任何清理命令前的 10 秒钟,脚本必须第一时间将当前正在运行的活跃线程与 SQL 文本导出归档。如果直接把连接全杀了,事后根本无法定位究竟是哪一条恶魔 SQL 拖垮了系统。
第二步:批量斩首非核心慢查询(Kill Long-Running Reads)
CPU 被打满通常是因为某条未走索引的复杂查询(如大范围报表扫描、全表排序或笛卡尔积)占满了所有 CPU 核心。
此时必须果断动用武器:只针对SELECT慢读查询执行强制 Kill,绝对不允许让写事务卡在半中间。
第三步:业务非核心功能一键熔断
通知业务团队通过配置中心(Nacos/Apollo)或网关一键触发预先准备好的“大促一级降级开关”:
- 关闭历史订单翻页查询;
- 关闭积分计算与优惠券复杂凑单推荐;
- 关闭用户端实时物流轨迹更新。
将宝贵的主库算力百分之百留给最核心的“支付扣款”与“订单创建”主链路。
第四步:代理层硬性限流压制
如果 CPU 依然居高不下,说明纯粹是写入 QPS 超过了硬件物理极限。必须在中间件或网关层启用令牌桶限流,把发往数据库的并发请求强行截断在安全水位以内(如限制最大并发 800 QPS),宁可让部分用户看到“排队中,请稍后再试”,也绝不能让数据库彻底挂死。
自动化应急止血脚本与 SQL 工具实战
为了杜绝在危急关头人工敲错 SQL,我们编写了生产级的一键止血自动化脚本:
#!/usr/bin/env bash # 核心数据库 CPU 飙高紧急止血脚本 # 用法: ./db_emergency_stop_bleeding.sh <db_host> <db_port> set -eo pipefail DB_HOST="${1:-127.0.0.1}" DB_PORT="${2:-3306}" DB_USER="sre_emergency_admin" TIMESTAMP=$(date +"%Y%m%d_%H%M%S") SNAPSHOT_DIR="/tmp/db_incident_${TIMESTAMP}" mkdir -p "${SNAPSHOT_DIR}" echo "[*] >>> 启动数据库极限止血程序: Host=${DB_HOST}, Port=${DB_PORT} <<<" # 1. 抓取现场证据链快照 echo "[*] [00:10s] 正在固化活跃连接与 InnoDB 引擎状态..." mysql -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -e ' SHOW FULL PROCESSLIST; SHOW ENGINE INNODB STATUS\G ' > "${SNAPSHOT_DIR}/db_snapshot_before_kill.log" 2>&1 # 2. 自动化识别并批量杀掉运行时间 > 3 秒的非核心只读 SELECT 语句 echo "[*] [00:40s] 正在执行针对慢查询的精准阻断..." mysql -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -e ' SELECT concat("KILL ", id, ";") FROM information_schema.processlist WHERE command = "Query" AND time >= 3 AND info NOT LIKE "%INSERT%" AND info NOT LIKE "%UPDATE%" AND user != "system user" AND user != "sre_emergency_admin" ' > "${SNAPSHOT_DIR}/kill_candidates.sql" # 执行安全击杀 if [ -s "${SNAPSHOT_DIR}/kill_candidates.sql" ]; then KILL_COUNT=$(wc -l < "${SNAPSHOT_DIR}/kill_candidates.sql") echo "[!] 发现 [${KILL_COUNT}] 条卡死的长查询,立即执行强制终止..." mysql -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" < "${SNAPSHOT_DIR}/kill_candidates.sql" || true else echo "[*] 未发现明显慢只读长查询,可能为高并发短事务积压,准备启动代理限流!" fi # 3. 查看杀完后的恢复状态 echo "[*] [01:10s] 正在验证 CPU 释放情况..." mysql -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -e ' SHOW STATUS LIKE "Threads_running"; SHOW STATUS LIKE "Threads_connected"; ' echo "[+] 紧急止血动作执行完毕,快照已保存在: ${SNAPSHOT_DIR}"必须坚守的三条救火红线
在执行数据库紧急止血时,值班工程师必须坚守三条防线:
- 绝对禁止无差别
kill -9数据库进程:如果盲目在宿主机上把mysqld进程强行杀死,内存中尚未落盘的脏页会导致下一次启动经历漫长的崩溃恢复,甚至发生表空间损坏风险。除非主库物理硬件彻底烧毁,否则止血必须在 SQL 协议层进行。 - 只杀读请求,慎重动写事务:慢查询通常是由于复杂的聚合报表引发的,这类
SELECT查询可以直接杀掉,对业务只产生一次重试影响。而对于处于事务进行中的UPDATE或INSERT,强行 Kill 会引发大量的回滚操作(Rollback),如果单事务锁定了数十万行数据,回滚时的大量 Undo 日志逆向写入甚至会把 I/O 彻底打死。 - 止血后必须保持持续观察 15 分钟:杀掉慢查询后,CPU 通常会在 10 秒内迅速回落至 30%。但此时绝不能宣布故障解除!如果业务网关没有开启限流或降级,上游堆积的重试请求会在 1 分钟后再次涌入并重新打垮数据库。必须在完成业务降级、确保流量平稳可控后,方可逐步放开限流阀门。
一份合格的 SRE 止血清单,是团队在无数次真实故障中用惨痛教训换来的保命符。面对最致命的数据库危机,唯有冷静、冷酷且按部就班地执行确定性的止血动作,才能在万丈深渊的悬崖边缘,把系统稳稳地拉回安全地带。