據(jù)庫連接池爆滿排查指南:從Threads_connected到根因定位與調(diào)優(yōu))
凌晨兩點手機(jī)被連續(xù)告警震醒。某個內(nèi)部系統(tǒng)的數(shù)據(jù)庫連接池在十分鐘內(nèi)被拉滿應(yīng)用層開始連環(huán)報錯用戶頁面卡成白屏部分接口直接 500。打開監(jiān)控一看MySQL 的Threads_connected已經(jīng)頂?shù)綆浊нB接池那邊的空閑連接趨近于零所有新請求都在排隊等連接。這類“數(shù)據(jù)庫連接池爆滿”的問題在業(yè)務(wù)高峰期太常見了但每次發(fā)生都讓人手心冒汗。表面上是“連接不夠用”實際上背后藏著完全不同的根因可能是連接泄漏可能是慢 SQL 拖住了連接也可能是事務(wù)邊界太長把連接占死了甚至可能是連接池參數(shù)和數(shù)據(jù)庫參數(shù)互相打架。這篇文章就把我多次排查連接池爆滿的經(jīng)驗完整梳理一遍從表象到根因再到止血、治根、防復(fù)發(fā)一步步講清楚。適合剛接觸數(shù)據(jù)庫和后端開發(fā)的朋友也適合每次遇到連接告警都不知道從哪下手的人。1. 連接池爆滿的典型信號先從現(xiàn)象避開調(diào)參誤區(qū)1.1 應(yīng)用層和數(shù)據(jù)庫層的異常表現(xiàn)連接池爆滿時最先報警的通常是連接池客戶端。用 HikariCP 的應(yīng)用日志里會出現(xiàn)類似Connection is not available, request timed out after 30000ms用 DBCP 或 C3P0 的則是Cannot get a connection, pool exhausted或者是Timeout: Pool empty. Unable to fetch a connection in 30 seconds, none available這些錯誤都有一個共同點應(yīng)用側(cè)拿不到可用連接后續(xù)請求只能排隊等待等到超時就直接拋異常。用戶看到的現(xiàn)象就是頁面轉(zhuǎn)圈、接口超時、部分功能不可用。數(shù)據(jù)庫側(cè)的指標(biāo)更直接。登錄 MySQL 執(zhí)行SHOW STATUS LIKE Threads_connected; SHOW STATUS LIKE Threads_running;正常情況下Threads_connected應(yīng)該穩(wěn)定在幾十到幾百的量級爆滿時會直接逼近甚至頂?shù)絤ax_connections此時再新進(jìn)連接就會收到“Too many connections”的報錯。這就是為什么很多人在這個時間點會下意識去改max_connections。1.2 最大的坑一上來就調(diào)大連接池和 max_connections遇到連接爆滿人的第一反應(yīng)往往是“連接不夠用那就加連接”。于是把 HikariCP 的maximumPoolSize從 50 調(diào)到 200再把 MySQL 的max_connections從 500 調(diào)到 2000重啟應(yīng)用。這種做法在低峰期可能管用但高峰期大概率會“死得更快”。原因很簡單連接數(shù)提高每個連接都要占用 MySQL 服務(wù)端的內(nèi)存和線程資源數(shù)據(jù)庫的負(fù)擔(dān)反而更重。如果根因是連接泄漏新的連接池上限只是讓池子從“2小時爆滿”變成“4小時爆滿”問題還在時間問題而已。如果根因是慢 SQL那么連接從 50 加到 200慢 SQL 占用的連接也同比放大并且會在數(shù)據(jù)庫端制造更多的鎖競爭和上下文切換。所以遇到連接爆滿千萬不要急著動參數(shù)。先把“連接都消耗到哪里去了”查清楚再決定怎么處理。工具其實很簡單MySQL 自帶的一張進(jìn)程列表就是最初的突破口。2. 用 show processlist 和線程堆棧給連接“畫像”2.1 SHOW PROCESSLIST 字段怎么看Command 分布決定排查方向在數(shù)據(jù)庫側(cè)執(zhí)行SHOW FULL PROCESSLIST;這是排查連接問題的第一板斧。它會把當(dāng)前數(shù)據(jù)庫上每一個連接列出來核心字段如下字段含義排查價值Id連接線程 ID后面 kill 連接時用User賬號來源區(qū)分是業(yè)務(wù)賬號還是運維賬號Host客戶端 IP 和端口找到連接來自哪個應(yīng)用節(jié)點db當(dāng)前連接的默認(rèn)庫確認(rèn)是不是目標(biāo)庫Command連接當(dāng)前的狀態(tài)Query、Sleep 等是關(guān)鍵Time當(dāng)前狀態(tài)持續(xù)秒數(shù)數(shù)字越大越可疑State具體執(zhí)行狀態(tài)Sending data、Waiting for lock 等Info正在執(zhí)行的 SQL慢 SQL 的現(xiàn)場證據(jù)這里最重要的一步是看 Command 列的分布。如果連接里有大量Sleep說明連接是被“占住但沒干活”的狀態(tài)帶爆的——重點懷疑連接泄漏、事務(wù)未提交、連接未歸還。如果大量是Query且Time很長說明是慢 SQL 或鎖等待拖住了連接導(dǎo)致后續(xù)請求全部排隊。簡單說Sleep 多查代碼漏連接Query 多查 SQL 和鎖。這兩條方向基本決定了后面所有排查動作。2.2 從應(yīng)用線程堆棧追蹤連接獲取點如果懷疑連接泄漏光在數(shù)據(jù)庫側(cè)看到一堆 Sleep 連接還不夠必須找出是應(yīng)用代碼里哪個位置拿了連接沒還。這時需要把 Java 線程堆棧倒出來看jstack 應(yīng)用PID thread.log然后在日志里找連接池等待和獲取連接的棧。例如 HikariCP 下搜索grep -n HikariCP thread.log | head -100或者看是否存在at com.zaxxer.hikari.pool.HikariPool.getConnection如果大量業(yè)務(wù)線程同時卡在getConnection等待并且你看到業(yè)務(wù)代碼的調(diào)用鏈停在一個具體 Service 方法上那么那個方法附近大概率就是連接發(fā)生泄漏的位置。再配合查看這段代碼里是否手寫了Connection獲取和關(guān)閉基本就能鎖定來源。2.3 用連接池監(jiān)控與 leakDetection 坐實泄漏多數(shù)連接池組件都提供活躍連接數(shù)的指標(biāo)。HikariCP 的監(jiān)控里可以看到active、idle、pending的實時值。如果在流量平穩(wěn)甚至下降時active仍然不斷上升那就是泄漏的典型曲線。HikariCP 還提供了泄漏檢測閾值參數(shù)leak-detection-threshold: 60000單位是毫秒。含義是一個連接從池中借出超過 60 秒未歸還就記錄一條泄漏告警日志。典型輸出是Apparent connection leak detected需要注意這個參數(shù)不應(yīng)設(shè)得過短否則一些本來耗時較長的合法請求也會誤報一般設(shè)成 30 秒到 60 秒比較合理。它不能自動修復(fù)泄漏只能幫你把問題代碼暴露出來在下一節(jié)我們會展開幾種最常見的泄漏寫法。3. 四個真實根因每種都有不同的“爆法”3.1 連接泄漏池被“僵尸連接”填滿連接泄漏是連接池爆滿的第一大戶也是最容易在代碼評審中漏掉的場景。最常見的寫法是// 錯誤寫法finally 里沒有 close public ListUser queryUsers() throws Exception { Connection conn dataSource.getConnection(); Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(SELECT * FROM user); while (rs.next()) { // ... } return list; // conn、stmt、rs 都沒有關(guān)閉 }這里面只要stmt或rs沒有關(guān)閉連接歸還池子就會延遲如果異常發(fā)生在中途連接就直接“丟”了。等連接池里的連接被這種代碼借走但不還池子很快就會被耗干。正確做法是用 Java 的 try-with-resources讓連接、語句、結(jié)果集都自動關(guān)閉// 正確寫法 public ListUser queryUsers() throws Exception { String sql SELECT * FROM user; try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { // ... } return list; } }排查技巧也很實用先搜全工程里所有g(shù)etConnection()的調(diào)用點逐個確認(rèn)是否有配套的 close然后重點檢查 try-catch-finally 結(jié)構(gòu)看 finally 里是否真的關(guān)閉了。很多老項目用的是手寫的 JDBC 工具類這種地方最容易藏連接泄漏需要重點盯。3.2 慢 SQL 與長事務(wù)一個查詢占住窗口不放連接池就像銀行柜臺每個窗口同一時間只能服務(wù)一個客戶。一條 SQL 跑了 30 秒就相當(dāng)于一個客戶占著柜臺 30 秒后面排隊的人只能干等。慢 SQL 觸發(fā)連接池爆滿的典型表現(xiàn)就是Threads_connected沒到頂?shù)玊hreads_running很高且有大量連接卡在同一個 SQL 上。在SHOW FULL PROCESSLIST里你會看到 Info 列是同一個查詢模板Time 字段都是幾十秒甚至幾分鐘State 常見為Sending data、Creating sort index、Copying to tmp table等。這類問題的處理順序是先拿這條 SQL 執(zhí)行EXPLAIN看是否走了索引、掃描行數(shù)是不是太大??词欠褚驗樯罘猪搶?dǎo)致掃描了大量數(shù)據(jù)例如LIMIT 1000000, 20??词欠裨诓樵兝镒隽舜蠓秶呐判蚧蚺R時表操作。一個很常見的例子是給大表做統(tǒng)計報表SQL 里寫法是SELECT province, COUNT(*) FROM order_table WHERE create_time NOW() - INTERVAL 1 DAY GROUP BY province;如果create_time上沒有索引這個查詢會全表掃描上百萬行跑個幾十秒很正常。一個這樣的查詢就能讓整個連接池的可用連接迅速下降。長事務(wù)和慢 SQL 經(jīng)常成對出現(xiàn)。一個事務(wù)里包了多次查詢用戶中途不操作也不提交事務(wù)連接就一直掛在那里表現(xiàn)同樣是連接池被占滿。3.3 鎖等待連接在排隊中耗干鎖等待是連接池爆滿里最隱蔽的一種。前面兩類是“連接被代碼占用”或“被 SQL 跑滿”鎖等待則是“連接在等鎖前端一直占著連接不釋放”。比如庫存扣減場景UPDATE inventory SET stock stock - 1 WHERE product_id 123;如果這條語句在某個事務(wù)里執(zhí)行而另一個事務(wù)已經(jīng)鎖住了這行記錄沒提交后續(xù)所有更新同一行的連接都會卡在Waiting for lock狀態(tài)。數(shù)據(jù)庫側(cè)看到的是大量連接都在等待連接池側(cè)看到的是所有連接都處于活躍但“假死”狀態(tài)。排查鎖等待需要看三張表SELECT * FROM information_schema.innodb_trx; SELECT * FROM information_schema.innodb_lock_waits; SELECT * FROM information_schema.innodb_locks;重點看trx_state是RUNNING但trx_started時間很長的舊事務(wù)那個事務(wù)基本上就是持鎖大戶。處理方式通常是先把這個舊事務(wù)處理掉鎖一釋放后面的連接就會繼續(xù)跑連接池爆滿現(xiàn)象自然緩解。防止這類問題的核心是做短事務(wù)UPDATE 之前先確認(rèn) WHERE 條件用了索引不要在事務(wù)里做遠(yuǎn)程接口調(diào)用不要在一個事務(wù)里更新大量行。這些習(xí)慣比事后調(diào)參重要得多。3.4 連接池參數(shù)和 MySQL 參數(shù)互毆還有一類爆滿是“配置打架”造成的。最典型的是連接池的maxLifetime大于 MySQL 的wait_timeout。MySQL 默認(rèn)wait_timeout是 28800 秒也就是 8 小時。如果連接池讓一個連接活過 8 小時后數(shù)據(jù)庫端已經(jīng)把它斷掉了但連接池不知道還會繼續(xù)拿這個連接去查詢。表現(xiàn)就是應(yīng)用運行一段時間后偶發(fā)性地報連接失敗之后連接數(shù)突然暴漲那是因為連接池在瘋狂重建連接一下子把連接數(shù)頂了上去。正確的參數(shù)協(xié)同思路如下參數(shù)推薦方向MySQL wait_timeout28800 秒默認(rèn)或按實際情況調(diào)整連接池 maxLifetime應(yīng)小于 wait_timeout比如 180 秒到 1400 秒之間connectionTimeout獲取連接超時默認(rèn) 30 秒可按響應(yīng)要求收緊idleTimeout / minIdle空閑連接回收策略避免大量空閑連接占住 DB這類問題排查時有個特點不是穩(wěn)定復(fù)現(xiàn)而是“隔一段時間爆一次”。如果連接池是 HikariCP它的默認(rèn)maxLifetime是 1800000 毫秒30 分鐘小于 MySQL 的 8 小時所以一般不容易踩到但用某些連接池組件時maxLifetime默認(rèn)是 0代表永不超時這時就很容易和數(shù)據(jù)庫的wait_timeout沖突。4. MySQL 側(cè)的連接上限到底該怎么定4.1 max_connections 不是第一個要動的旋鈕不少人一看到Too many connections就急著去改max_connections。實際上max_connections更像是一個“風(fēng)險防火墻”而不是性能調(diào)優(yōu)參數(shù)。合理的做法是正常情況下max_used_connections應(yīng)該離max_connections有足夠余量一旦距離被拉近就要警惕而不是把墻后移。要查看歷史峰值占用SHOW STATUS LIKE Max_used_connections;如果這個值和當(dāng)前Threads_connected都快貼到max_connections了那連接池爆滿是必然結(jié)果因為數(shù)據(jù)庫端根本沒有余量給新連接。4.2 Threads_connected 和 Threads_running 要分開看這兩個指標(biāo)被很多人混用其實含義完全不同Threads_connected當(dāng)前打開的連接數(shù)。它高說明“有很多連接掛著”。Threads_running當(dāng)前正在執(zhí)行查詢的線程數(shù)。它高說明“數(shù)據(jù)庫真的在忙”。如果Threads_connected高但Threads_running低多半是連接閑置或泄漏數(shù)據(jù)庫本身不忙是連接管理出了問題。如果兩者都高說明數(shù)據(jù)庫處理能力到瓶頸了要么優(yōu)化 SQL要么對讀寫做拆分和限流。如果Threads_running高但Threads_connected不高那反而是健康的——表明連接都被有效利用只是數(shù)據(jù)庫確實在超負(fù)荷工作。這兩者結(jié)合起來看才能判斷“連接爆滿”到底是資源問題還是代碼問題。4.3 連接數(shù)與內(nèi)存、線程調(diào)度之間的聯(lián)動每個 MySQL 連接都有成本。連接建立后服務(wù)端要給它分配線程、內(nèi)存尤其要考慮各類 buffer。例如sort_buffer_sizejoin_buffer_sizetmp_table_size如果這些參數(shù)設(shè)置得偏大比如sort_buffer_size是 2MB那么 2000 個連接在極端情況下可能同時占用數(shù) GB 內(nèi)存。這就是為什么把小內(nèi)存機(jī)器上的max_connections盲目調(diào)大反而可能直接把數(shù)據(jù)庫 OOM 拖垮。正確的做法是先估算機(jī)器內(nèi)存再看全庫連接數(shù)所需的 buffer 總量最后反向推導(dǎo)合理上限。例如 16GB 內(nèi)存的實例InnoDB buffer pool 分走 8GB給每個連接的平均 buffer 按 2MB 估那連接數(shù)控制在 1000 以內(nèi)都不算激進(jìn)但如果機(jī)器只有 4GB連接數(shù)就得壓到 300 以下。這個計算不需要很精確目的是讓你心里有數(shù)連接數(shù)從來不是越大越好它是有資源約束的。連接池的作用是“復(fù)用”而不是“無腦堆量”。5. 止血、治根、驗證一套能落地的完整流程5.1 先止血臨時 kill 的取舍連接已經(jīng)爆滿的現(xiàn)場首要任務(wù)不是排查而是讓業(yè)務(wù)先恢復(fù)。最直接的手段是殺掉一批可疑連接。殺連接之前必須確認(rèn)兩點連接是誰的、連接在干什么。前面用SHOW FULL PROCESSLIST看到的結(jié)果就是依據(jù)??梢詢?yōu)先殺掉Command Sleep且Time超過 60 秒的連接這類連接大概率是空閑的即使是事務(wù)未提交kill 之后事務(wù)也會回滾寧可損失極小范圍的數(shù)據(jù)一致性也不能讓整個服務(wù)持續(xù)不可用。執(zhí)行格式KILL 12345;生產(chǎn)環(huán)境更穩(wěn)妥的方法是寫腳本按條件批量處理但要設(shè)置白名單排除管理賬號連接。不推薦一上來就重啟數(shù)據(jù)庫或者重啟應(yīng)用。重啟只能讓當(dāng)前連接清零如果根因是連接泄漏或慢 SQL重啟之后很快又會滿。它只適合在完全無法定位問題時的應(yīng)急手段。5.2 治根代碼修復(fù)、SQL 治理、事務(wù)邊界止血只能維持幾個小時的穩(wěn)定真正要處理的是根因。連接泄漏把所有 JDBC 操作統(tǒng)一改成 try-with-resources可以做一個封裝的 DAO 基類避免團(tuán)隊里各寫各的。數(shù)據(jù)源獲取統(tǒng)一走 Spring 的Autowired或標(biāo)準(zhǔn)池 API不要再寫“自己 new Connection”的工具類。慢 SQL先開慢查詢?nèi)罩緎low_query_log 1 long_query_time 1把超過 1 秒的 SQL 全部收集起來挑出高頻和高耗時的逐條優(yōu)化。常見手段包括加索引、改寫查詢條件、把深分頁改成游標(biāo)或基于上次最大 ID 的分頁方式。事務(wù)邊界事務(wù)盡量只包裹核心的寫操作不要在事務(wù)內(nèi)做遠(yuǎn)程 HTTP 調(diào)用、循環(huán)查詢等慢操作。如果做批量更新控制在合理行數(shù)內(nèi)并分批提交長事務(wù)是連接占用的隱形殺手。5.3 驗證壓測 監(jiān)控告警 灰度發(fā)布修復(fù)之后不能直接宣布結(jié)束必須驗證。先做一輪壓力測試模擬高于平時的 QPS觀察連接池曲線是否平穩(wěn)、Threads_running是否處于合理范圍、連接獲取時間 P99 是否達(dá)標(biāo)。我常用的做法是壓測時把連接池的active、idle、pending三個指標(biāo)都打出來。如果流量上升時active同步上升、流量平穩(wěn)后active能降回去說明池子是健康的。如果active只升不降說明還會漏繼續(xù)找代碼問題。監(jiān)控告警也要跟上。至少給這幾個指標(biāo)配告警Threads_connected與max_connections的比值超過 80%。連接池獲取連接等待時間超過 1 秒。連接池pending長期大于 0。慢 SQL 數(shù)量突增。6. 用 QPS 反推連接池大小連接數(shù)規(guī)劃的計算方法6.1 連接占用公式與計算示例連接池大小不該拍腦袋可以用一個粗算公式來估算連接池大小 ≈ 平均 QPS × 單請求平均數(shù)據(jù)庫耗時 × (1 排隊系數(shù))舉例說明。某個業(yè)務(wù)接口每秒請求量 500這個接口每次請求訪問數(shù)據(jù)庫的平均耗時是 20 毫秒。那么單請求占用的連接時間就是 0.02 秒。同一時刻需要的連接數(shù)大約是500 × 0.02 10再考慮一部分請求可能因為等待鎖、結(jié)果集處理等原因多占一點時間加一個 30%50% 的排隊系數(shù)10 × 1.5 15也就是說這個接口的預(yù)留連接池大小 1520 已經(jīng)夠用。如果實測連接一直在 20 以上而不下降說明耗時估算偏低或存在泄漏而不是配置不夠。6.2 三種典型負(fù)載下的池大小設(shè)計不同業(yè)務(wù)形態(tài)對連接池的需求差異很大業(yè)務(wù)形態(tài)特征池大小建議低頻管理類QPS 低、單次查詢簡單1020高頻 OLTP 類QPS 高、單次查詢幾毫秒到幾十毫秒核心接口單獨估算池總大小 50100報表/批量類單次查詢秒級、并發(fā)不高池不用太大但 SQL 必須優(yōu)化否則池再大也堵這里面最關(guān)鍵的理念是連接池解決的是“復(fù)用連接”的問題不是“讓更多請求同時跑”的問題。單個 MySQL 連接的查詢速度取決于索引和執(zhí)行計劃而不是池的大小。池子設(shè)得再大SQL 本身要跑 10 秒照樣拖垮。6.3 需要長期盯的監(jiān)控指標(biāo)最后建議把這幾個指標(biāo)做成日常巡檢項連接池活躍連接數(shù)看漲跌趨勢是否與流量一致。連接獲取等待時長 P99超過 500ms 就要警惕。連接創(chuàng)建/銷毀速率如果頻繁創(chuàng)建銷毀說明maxLifetime或idleTimeout配置不合適。MySQLThreads_connected與Threads_running的比值。這些指標(biāo)可以在看板上做成兩張圖一張展示連接池狀態(tài)一張展示數(shù)據(jù)庫狀態(tài)。連接池爆滿不是突發(fā)事故大多是緩慢惡化的過程。有了這些圖基本都能在用戶感知之前提前處理。最后分享一個印象深刻的排查經(jīng)歷。某次連接池持續(xù)告警我把應(yīng)用、數(shù)據(jù)庫、參數(shù)都翻了個遍仍然時不時漲滿。最后發(fā)現(xiàn)是一個第三方 SDK 內(nèi)置了自己的連接池每次調(diào)用都新建一個池實例而不復(fù)用流量稍高就把數(shù)據(jù)庫連接數(shù)頂上天。那次之后我學(xué)會了排查連接問題時要把中間件、SDK、框架的隱藏連接全部考慮進(jìn)去不能只盯業(yè)務(wù)代碼。希望這篇排查順序能幫你少熬幾個夜少掉幾根頭發(fā)。