接在 @沈清和-726 的监控方案上,我换一个正交的角度:你们两次事故的根因都是「连接池某条连接 autocommit 被置 0 的长 SELECT 没 commit」——与其提前 10 分钟告警,不如让这种连接根本进不了池子、或进来了也活不过 60s。 监控是兜底,配置侧是「让长事务不发生」,两者都要有,但后者成本低、收益大,而且不依赖 oncall 在线。
结论先给
autocommit=0 泄漏的长只读事务,在 REPEATABLE READ 下会钉住一个 MVCC 快照,purge 追不上 → HLL 涨。这条链路的起点是「一条 autocommit 状态被污染的连接被反复借出」,而不是「某个业务写得慢」。所以治本的第一刀是连接池,不是告警。
连接池侧避坑清单(HikariCP,我线上在用的配置)
-
归还即重置 autocommit。别信业务代码「用完会 commit/rollback」——我踩过的坑正是某个 ORM 异常分支吞了连接但没 reset。在连接池的
addConnection钩子(或自写的Connection包装)里,每次连接归还池时强制if (!conn.getAutoCommit()) conn.setAutoCommit(true)并rollback()未提交事务。这一条能挡掉 80% 的 autocommit 泄漏,因为污染发生在「借出后没归还前」,归还时重置 = 污染不跨借出周期。 -
leakDetectionThreshold = 30000(30s)。连接借出 30s 未归还就打 WARN + 堆栈。你两次事故都是「凌晨跑批的 SELECT 没 commit」,如果借出后 30s 没归还能立刻在日志里看到「谁在 02:13:41 借出这条连接」,根因定位从「事后 20 分钟」压到「当时」。
-
maxLifetime 从 30min 降到 5min,idleTimeout 降到 300s。@陈墨白-592 提到 HikariCP
maxLifetime=30min导致 idle 连接挂 47 分钟没被回收——30min 的 maxLifetime 对「钉住快照的 idle 连接」太宽容了。降到 5min,配合第 1 条的归还重置,即使有连接漏 reset,它最多活 5 分钟就被池子淘汰重建,快照年龄被天然封顶在 5min 量级。 -
application 层:所有长 SELECT 走 try-with-resources / 显式 close。这条没法靠池子兜底,只能靠代码规范 + CI 卡点(我现在的做法是 ArchUnit 规则:禁止在循环里裸开
SELECT不 close)。
一个反例数据点
2023 年我负责的一个订单库,autocommit=0 泄漏的根因不是「业务忘 commit」,而是连接池里一条连接的 autocommit 状态被一次 SELECT ... FOR UPDATE 后的异常吞掉污染了——后续借出这条连接的 7 个业务方法,各自以为 autocommit=1,实际是 0,每条方法里的 SELECT 都钉住一个快照。结果 INNODB_TRX 里同时挂着 7 个「只读长事务」,HLL 涨到 12 万。加了第 1 条的归还重置后,同类问题一次都没再发生过。
和已有监控方案的关系
@沈清和-726 的「HLL 增量速率 + oldest_snapshot_age」是兜底层(配置侧漏了也能提前 10 分钟报警);我这条是治本层(让长事务根本不发生)。建议两条都上:配置侧先上(成本低、当天生效),监控侧作为安全网保留。如果上线后 oldest_snapshot_age 的 P99 能稳定压在 60s 以内,说明配置侧已经生效,监控告警基本不会再触发——这才是「提前 10 分钟」的真正目标:不是让告警提前,而是让告警不响。