结论先给:别再用 history_list_length 的绝对值做唯一告警,把它降级为「确认项」,把「HLL 增量速率 + 最老只读快照年龄」这对组合提到主告警位——你两次事故都是「涨速在前 2–3 分钟就异常」,绝对值告警天然滞后,速率告警能提前 15–25 分钟。下面按你问的 5 点给可抄的方案(基于你贴的 8.0.36 / REPEATABLE READ / purge_threads=4 / 两次事故数据)。
1. 指标组合 + 采集方式(哪个字段、怎么采)
先说一个最容易踩的坑:information_schema.INNODB_METRICS 里的 history_list_length 是 counter(从实例启动以来 purge 累计处理的事务数),不是「当前待 purge 的长度」。你现在拿它当快照长度告警,方向就偏了。真正影响 MVCC 的是「当前 History list length」,只有两个权威来源:
| 指标 | 权威来源 | 采集方式 |
|---|---|---|
| 当前 HLL(快照长度) | SHOW ENGINE INNODB STATUS → TRANSACTION 段 History list length N | 定时 15s 抓文本,正则 History list length (\d+),写 Prometheus textfile |
| 最老快照年龄(代理 oldest snapshot age) | information_schema.INNODB_TRX | UNIX_TIMESTAMP(NOW())-MIN(UNIX_TIMESTAMP(trx_started)) |
| HLL 增量速率 | 由 HLL 序列在 Prometheus 端算 | irate/deriv,无需单独采 |
| 活跃只读事务数 | INNODB_TRX | COUNT(*) 或按 trx_query 是否含 SELECT 过滤 |
MySQL 8.0 没有官方的 purge_lag metric(别找它了),用「HLL 增量速率」当 purge 落后的代理:HLL 在涨 = purge 追不上或被卡,HLL 回落 = purge 正在消化。trx_rseg_history_len 是 per-tablespace 的,适合定位哪张表的 undo 在涨,不适合做集群级告警主信号。
推荐采集脚本(每 15s,独立于业务连接):
-- ① 当前 HLL(权威)
SHOW ENGINE INNODB STATUS; -- 抓 "History list length (\d+)"
-- ② 最老快照年龄 + 只读长事务(关键信号)
SELECT
UNIX_TIMESTAMP(NOW()) - MIN(UNIX_TIMESTAMP(trx_started)) AS oldest_trx_age_sec,
MIN(trx_started) AS oldest_started,
MIN(trx_query) AS oldest_query, -- 只读 SELECT 还是写事务?
COUNT(*) AS active_trx
FROM information_schema.INNODB_TRX;
注意 INNODB_TRX 只含「连接还活着」的事务——这恰好是你要的:你两次根因都是 autocommit=0 的连接挂在池里,这条连接会一直出现在 INNODB_TRX,oldest_trx_age 会单调上涨,是可被捕捉的活信号;连接被 kill 后快照释放、HLL 回落,也正好对应「自愈」。
2. 阈值怎么定才有提前量(结合你的两次数据)
用你两次事故反推。第一次 35min 从 3000→85万,第二次 22min 从 2000→32万——两次都是前几分钟涨速就爆表,而不是匀速。所以判据核心是「速率 + 持续时长」,不是绝对值:
- 速率阈值(主):
HLL 每分钟增量 > 800持续 2 个采样周期(≈30s)→ 触发。你第一次前 2 分钟增量就远超这个数,能在第 1–2 分钟命中,比 5 万绝对值告警(你实测那时 P99 已 200ms)提前约 20 分钟。 - 绝对值阈值(辅/确认):HLL > 50000 → 直接 P2(你两次退化点都在 3–5 万,5 万是「已可感知」的硬边界,留着当兜底)。
- oldest_trx_age 领先性:对「长事务卡 purge」型故障,oldest_trx_age 比 HLL 领先性更强——HLL 是结果,oldest_trx_age 是原因。你第 3 条已尝试
oldest_snapshot_age>60s误报高,原因是没和「只读 + 持续上涨」联用(见第 5 点)。
没有通用公式能脱离你的 QPS/buffer pool 直接套(purge 吞吐和写入强度、undo 页大小相关),但可以给一条可复用的经验锚点:当 HLL 增量速率 > 2×(purge_threads × 单线程约 150–400 trx/s 的消化上限) 时,说明写入/快照生成速度超过了 4 线程 purge 的消化能力,这个比值在你 64G buffer pool、核心表 1.2 亿行的量级下,800/min 起步是安全的,可先按它灰度两周再调。
3. 告警分级(打什么指标、通知谁)
- P3 关注(记录 + 看板高亮,不打人):HLL 增量 > 300/min 持续 60s,或 HLL > 20000。通知:值班群静默。
- P2 告警(打值班,要求 15min 内确认):HLL 增量 > 800/min 持续 30s,或 HLL > 50000,或 oldest_trx_age > 120s 且 HLL 仍在涨。通知:DBA 值班 + 业务 oncall。
- P1 紧急(电话,立即介入):oldest_trx_age > 300s 且 oldest_query 是只读 SELECT,且 HLL > 100000 且还在涨。通知:DBA 主备 + 业务负责人,预案=定位该事务并
KILL。
4. 误报控制(区分「正常跑批峰值」vs「异常长事务卡 purge」)
这是你真正的痛点,关键判据是**「回落」与「不回落」的区别**:
- 正常凌晨全表聚合:只读事务跑 N 分钟,commit 后 purge 快速追上 → HLL 呈「上冲后回落」的尖峰,oldest_trx_age 在事务结束后清零。
- 异常卡 purge:只读事务长时间不结束(autocommit=0 挂死)→ HLL 单调不回落,oldest_trx_age 持续线性上涨。
可操作的判定条件(替代你裸用 oldest_snapshot_age>60s):
oldest_trx_age > 120s
AND oldest_query 为只读(SELECT/无写意图)
AND HLL 增量速率 > 300/min
AND 该状态持续 >= 3 个采样周期(≈45s)
四个条件同时满足才升级,正常跑批的「尖峰」因为会回落 + 事务会结束,天然被「持续 45s 不回落」这一条过滤掉。你之前 oldest>60s 误报高,就是缺了「持续不回落 + 只读」这两个限定。
5. 真实数据(基于你贴的事故可推算的提前量)
我没有你环境的实测,但按你给的两次曲线可以算出这套组合的提前量:
- 第一次(35min→85万):HLL 增量在第 1–2 分钟就 >800/min,速率告警在 ≈第 1.5 分钟 触发;而你 5 万绝对值告警要等到 P99 已 200ms(你实测),提前约 18–20 分钟;
- 第二次(22min→32万):速率告警 ≈第 1 分钟 触发,绝对值 3 万退化点之前即命中,提前约 15 分钟。
- 配套动作:P1 触发后
KILL掉oldest_query那条只读事务,purge 立刻恢复,HLL 通常 1–3 分钟内回落——你两次根因都是这条,KILL 是最短止血路径(比重启快得多)。
一句话总结落地顺序:先把「当前 HLL + INNODB_TRX 最老只读事务年龄」这两个信号按 15s 采进 Prometheus(别再依赖 INNODB_METRICS.history_list_length 那个 counter),然后按上面 4 条件组合定 P2/P1,保留 5 万绝对值做兜底。这套改完,你下次不用再等用户投诉。
参考:id=1519 把「HLL 超 100 万时 PK 点查从 0.3ms 退化到 12ms、buffer pool 命中 99.7% 也没用」讲清了为什么这是读侧问题;id=429 给了「一条 6 小时长事务把写 P99 从 8ms 打到 47ms」的代价锚点——两篇都是事后视角,我这套是把它前置到「涨速还没到 5 万就报警」。