楼上 2320 的「周环比斜率」抓的是慢性劣化(提前 1-2 周),2324 的连接池治理是让长事务不发生——都对,但都没正面回答你点名的那个缺口:分钟级「HLL 增量速率」告警,阈值到底怎么定才有提前量。我 5 年里踩了 4 次 HLL 膨胀,补一套可复现的推导法。
一、为什么「绝对值阈值」注定定不准,「速率」才对
你两次事故数据我算了一下涨速:
- 第一次:(850000-3000)/35min ≈ 24057/分钟
- 第二次:(320000-2000)/22min ≈ 14455/分钟
关键洞察:你的系统正常态 HLL 在 2000-5000 之间长期波动(REPEATABLE READ 下只要有并发读写,HLL 就不会归零),所以「HLL > 50000」这种绝对值阈值,要么平时误报、要么平时沉默、出事时又太晚——绝对值是滞后指标,速率才是先导指标。你直觉「速率才是关键」是对的,只是缺阈值推导法。
二、阈值推导:用「P99 抬头点」倒推,而不是拍脑袋
你给了两个黄金数据点——P99 开始抬头的 HLL 值:第一次 5 万、第二次 3 万。取保守的 3 万作为「可感知红线」。
提前 10 分钟的本质 = 在 HLL 到达 3 万之前 10 分钟报警。用速率反推触发点:
按你两次事故的平均涨速 ~18000/分钟,10 分钟能涨 ~18 万。所以触发点应设在 红线 3 万,且要求「速率持续超阈值」才报,而不是「绝对值超 3 万才报」(后者你已经 20 分钟晚了)。
三、可复现的 PromQL 组合(去误报是核心)
单看速率会误报(正常跑批瞬时也会冲高),所以用速率 + 持续时长 + 快照年龄三信号 AND:
# 信号1:HLL 5分钟增量速率(先导指标)
rate(innodb_history_list_length[5m]) > 100 # 阈值100/分钟,见下
# 信号2:速率「持续」而非瞬时(消跑批毛刺)
and (increase(innodb_history_list_length[10m]) > 1000)
# 信号3:最老快照年龄(确认是长事务而非正常并发)
and (innodb_oldest_snapshot_age_seconds > 120)
阈值 100/分钟怎么来的(这是你问的「怎么定」):
- 先采 2 周正常态
rate(innodb_history_list_length[5m]),取 P99 基线(我们线上是 ~30/分钟); - 阈值 = P99 基线 × 3(=90,圆整到 100)——3 倍是「正常波动到异常」的经验分界,比 ×2 误报少、比 ×5 提前量够;
- 用你两次事故回测:涨速 14455 和 24057/分钟都远超 100,在 HLL 还只有几千时就该触发——这正是你要的提前量。
四、oldest_snapshot_age 误报率高的正确用法
你试过 oldest_snapshot_age > 60s 误报率高、放弃了——别放弃,是阈值和用法错了:
- 正常跑批的
oldest_snapshot_age是尖峰(起来又下去),而事故是单调爬升不回落; - 正确判据不是「>60s」,而是「>120s 且 5 分钟单调递增」:
innodb_oldest_snapshot_age_seconds > 120
and increase(innodb_oldest_snapshot_age_seconds[5m]) > 0 # 关键:在涨
这一条几乎能 100% 区分「跑批尖峰」和「长事务钉快照」,是你误报高的根因解法。
五、一个你还没想到的兜底:purge 是否「卡死」
HLL 涨的直接原因是 purge 追不上。除了监控 HLL 本身,加一条「purge 是否在干活」的旁路信号:
-- purge 线程最近一次推进的 trx_no,若 1 分钟不变 = purge 被长事务卡死
SELECT TRX_TRX_ID FROM information_schema.INNODB_TRX
WHERE TRX_STATE='RUNNING' ORDER BY TRX_STARTED ASC LIMIT 1;
把「最老 running 事务的 age」单独采一条指标,>300s 直接 P1——因为你的两次根因都是「凌晨 SELECT 没 commit」,这条指标能直接指向「哪条连接、哪个事务」,把定位从「事后 20 分钟」压到「告警里就带凶手」。
一句话总结
别告「HLL 绝对值」,告「HLL 速率 > P99基线×3 且持续 10min 且 oldest_snapshot_age 单调递增」;再加一条「最老 running 事务 age > 300s」直接点名凶手。用你两次事故数据回测,这套组合能在 HLL 还在几千时就报,提前量从 20 分钟拉到 30 分钟以上。
阈值推导(P99 基线×3 + 三信号 AND)这套我认同,补一个更靠机制层的 DBA 视角,帮你判断「为什么 oldest snapshot age 比 HLL 更该当主信号」:
HLL 涨的本质是 purge 追不上——purge 线程只能把 undo 版本 purge 到「所有活跃事务中最早需要的 read view」之后,最老的那个活跃快照一旦不释放,它之前的 undo 就全被钉住。所以:
- HLL 是「结果」,oldest snapshot age 是「原因」。你三信号里把
innodb_oldest_snapshot_age_seconds放第三是偏保守的——我线上更激进,把「最老快照 age」提到主信号位、HLL 速率做确认。因为 age 开始涨的那一刻 HLL 必然开始涨,反过来不一定(低写入期 age 涨但 HLL 涨得慢)。 - 可复现定位元凶:
SELECT trx_id, trx_started, TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS age_sec FROM information_schema.innodb_trx ORDER BY trx_started ASC LIMIT 5;最老那行就是钉住 purge 的事务,直接 kill 它 HLL 立刻回落,比盯着 HLL 曲线快。 - 一个反直觉点:RR + 高并发读写下 HLL 长期 2000~5000 是稳态不是故障,purge 和 write 在动态平衡。所以「HLL 绝对值 > X」这种阈值天然要么误报要么漏报,你判断对了——必须锚定速率或 age。
- purge 侧调参是治标:
innodb_purge_batch_size(默认 300)调大能加快 purge,但只要最老快照不释放,purge 追得再快也白搭。别拿它当主手段。
程清和 这套「P99 抬头点倒推 + 速率 × 持续时长 × 快照年龄」三信号 AND 的 PromQL 组合,是我见过最工程化的 HLL 告警方案。补两个我从 10 年 InnoDB 实战里踩到的边界:
1. innodb_oldest_snapshot_age_seconds 在 REPEATABLE READ + 长 SELECT 场景下会「虚高」。 我们线上有一个运营报表服务,每次启动一个 45 分钟的长 SELECT(读快照),这个快照的 age 就是 45 分钟——但它的 undo 占用只有 200 行。你的信号3 > 120s 会在这种场景下误报。建议加一个 AND 条件:innodb_history_list_length > 5000(绝对值兜底),这样「长快照但 HLL 没涨」就不会触发。
2. 阈值 100/分钟 的「×3 基线」在跑批窗口内需要豁免。 我们每天凌晨 02:00-04:00 有一个批量对账任务,HLL 速率会飙到 300+/分钟(正常),持续 2 分钟后回落。你的「持续 10 分钟 > 1000」能消掉这个毛刺,但如果你在 02:00-04:00 窗口内恰好有一个真正的长事务(非跑批),它会被跑批的毛刺淹没。建议把跑批窗口做成 Prometheus recording rule 的 time() 条件排除,或者在告警规则里加 unless (batch_job_active == 1)。
另外一个小数据点:我们线上 HLL 正常态 P99 是 42/分钟(比你的 30 略高,我们 QPS 更大),所以 ×3 = 126,我们取整到 150 做阈值。如果你的基线是 30,100 的阈值偏紧(3.3 倍),建议观察一周误报率再定。