線上應用線程數從 200 暴增至 1400 又瞬間驟降 1200,所有增量線程名為 MySQL Statement Cancellation Timer。本文從 JDBC 超時機制、MySQL 驅動實現、連接池心跳策略、JDK Timer/Finalizer 機制四個層面,還原這場「線程洩漏 → GC 回收」的完整鏈路。
0.2k threads → (漸增) → 1.4k threads → (YGC 瞬間) → 0.2k threads
所有增量線程:daemon, name="MySQL Statement Cancellation Timer", state=TIMED_WAITING
"MySQL Statement Cancellation Timer" #2952 daemon prio=5 os_prio=0 tid=... nid=... in Object.wait()
java.lang.Thread.State: TIMED_WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
at java.util.TimerThread.mainLoop(Timer.java:552)
at java.util.TimerThread.run(Timer.java:505)
確認:JDK Timer 線程,非業務線程,處於定時等待狀態。
應用配置:每條查詢設置 statement.setQueryTimeout(seconds)。
MySQL Connector/J 實現:每個 Connection 創建一個 Timer 線程(Statement Cancellation Timer),定時檢查執行時間,超時發送 KILL QUERY。
-- 連接池心跳邏輯(偽碼)
if (conn.isValid(timeout)) { // JDBC 4.0 標準接口
return conn; // 可用
} else {
conn.close(); // 不可用,關閉連接
return getNewConnection();
}
MySQL 驅動 isValid() 實現:
COM_PING 協議包false連接池調用 conn.close() 時:
// MySQL Connection.close() 邏輯
public void close() {
if (!this.closed) { // 關鍵判斷!
this.realClose(); // 真正關閉:cancel Timer、釋放 socket 等
this.closed = true;
}
// 若 closed 已為 true,直接返回,Timer 未 cancel!
}
死結:isValid() 失敗時已將 closed=true,導致後續 close() 認為已關閉,跳過 realClose(),Timer 線程永不結束。
wait_timeout=28800 (8h)minEvictableIdleTimeMillis > wait_timeoutisValid() 失敗 → closed=true → close() 無效 → Timer 洩漏
// JDK Timer 內部類
private final Object threadReaper = new Object() {
protected void finalize() {
synchronized (Timer.this) {
timerThread.terminate(); // 結束 Timer 線程
Timer.this.notify();
}
}
};
YGC 回收無引用的 Connection → 關聯 Timer 失去引用 → threadReaper.finalize() 執行 → timerThread.terminate() → 大批 Timer 線程瞬間結束。
| 連接池 | 心跳方式 | 是否洩漏 | 原因 |
|---|---|---|---|
| DBCP | isValid() | ✅ 是 | 同上機制 |
| HikariCP | isValid() | ✅ 是 | 同上機制 |
| Druid | ping() | ❌ 否 | 使用驅動私有 ping(),不觸發 closed 標記 |
| Caelus (自研) | isValid() | ✅ 是 | 同上機制 |
# HikariCP / DBCP
# 改用驅動原生 ping() 而非 JDBC isValid()
# HikariCP 3.4.5+ 支援:
connectionTestQuery: "/* ping */ SELECT 1" # 強制用 Statement 查詢心跳
# 或升級 HikariCP 4.0+ 內建 ping() 支援
# 連接 URL 參數
jdbc:mysql://host:3306/db?enableQueryTimeouts=false
# 或代碼中不調用 setQueryTimeout()
CompletableFuture + ScheduledExecutorService 實現業務級超時控制
# 修改前:線程數隨時間無限增長,YGC 時驟降
# 修改後:線程數穩定在連接池大小 ± 閒置線程
# 關鍵指標:jstack 不再出現 "MySQL Statement Cancellation Timer"
Finalizer 這裡成了「救火隊員」,但依賴 GC 回收線程屬於非確定性清理:
finalize()isValid()?改為 ping() 或 SELECT 1wait_timeout vs 連接池驅逐時間:確保 minEvictableIdleTimeMillis < wait_timeout * 1000setQueryTimeout():非必要禁用isValid() 導致的 closed 標記問題jvm.threads.daemon.count 異常增長告警這場 Bug 的核心教訓:標準接口(isValid)的驅動實現缺陷 + 連接池盲目信任標準接口 + 服務端超時配置不匹配,三方共舞釀成事故。建立「驅動行為白盒化、配置相互制約驗證、異常路徑壓測覆蓋」的工程文化,才能從根本上杜絕此類隱性洩漏。