存溢出排查:數(shù)據(jù)庫(kù)連接對(duì)象堆積與連接池泄漏實(shí)戰(zhàn))
某個(gè)線(xiàn)上服務(wù)在凌晨開(kāi)始陸續(xù)出現(xiàn)接口超時(shí)堆內(nèi)存監(jiān)控曲線(xiàn)一路向上從2GB爬到3.8GB只用了幾個(gè)小時(shí)隨后接連拋出OutOfMemoryError: Java heap space。容器重啟后過(guò)了一會(huì)兒又反復(fù)。打開(kāi)GC日志和堆轉(zhuǎn)儲(chǔ)一看老年代里堆滿(mǎn)了同一個(gè)類(lèi)名com.mysql.cj.jdbc.ConnectionImpl對(duì)象數(shù)量達(dá)到六位數(shù)。這就是典型的“JVM內(nèi)存溢出-數(shù)據(jù)庫(kù)連接對(duì)象大量累積”問(wèn)題。數(shù)據(jù)庫(kù)連接本身不算龐然大物但成千上萬(wàn)個(gè)連接對(duì)象疊加在一起足以把堆內(nèi)存榨干。真正有價(jià)值的不只是知道“發(fā)生了什么”而是搞清楚連接對(duì)象為什么只增不減、怎樣在不影響線(xiàn)上業(yè)務(wù)的情況下把它定位出來(lái)、修完之后又怎么防止同類(lèi)問(wèn)題再次出現(xiàn)。這篇文章就按這個(gè)脈絡(luò)寫(xiě)給同樣被內(nèi)存溢出和連接池問(wèn)題折磨的人一份可復(fù)用的排查手冊(cè)。1. 先搞清楚連接對(duì)象累積與堆溢出是什么關(guān)系在動(dòng)手抓堆轉(zhuǎn)儲(chǔ)之前一定要先把問(wèn)題定性。很多人一看到內(nèi)存溢出第一反應(yīng)是堆太小或者GC參數(shù)不對(duì)實(shí)際上連接對(duì)象累積類(lèi)的問(wèn)題根因往往藏在業(yè)務(wù)代碼和連接池策略里??梢园堰@類(lèi)故障從兩個(gè)層面拆開(kāi)看一個(gè)是單條連接在JVM里到底占多少內(nèi)存一個(gè)是這些連接究竟是怎么積累起來(lái)的。1.1 一條數(shù)據(jù)庫(kù)連接在JVM里到底占多大地方“一條Connection能占多大頂多幾百字節(jié)”這是對(duì)數(shù)據(jù)庫(kù)連接對(duì)象的典型誤解。連接在JVM堆里的內(nèi)存占用遠(yuǎn)不止接口對(duì)象本身。以最常見(jiàn)的MySQL客戶(hù)端連接器為例一個(gè)Connection對(duì)象背后至少包含這樣幾塊內(nèi)容Socket通道的輸入緩沖區(qū)和輸出緩沖區(qū)每塊默認(rèn)就是8KB到16KB兩個(gè)加起來(lái)已經(jīng)有十幾KB協(xié)議解析層、編碼轉(zhuǎn)換層、事務(wù)狀態(tài)機(jī)等內(nèi)部對(duì)象預(yù)編譯Statement緩存如果打開(kāi)useServerPrepStmts每條SQL對(duì)應(yīng)一個(gè)對(duì)象連接上下文、字符集元數(shù)據(jù)、用戶(hù)權(quán)限相關(guān)的緩存把這些疊加起來(lái)一條空閑連接在MySQL 8.x連接器下實(shí)測(cè)常常是150KB往上。要是連接上執(zhí)行過(guò)較多SQL產(chǎn)生了較多預(yù)編譯緩存單條連接突破200KB到300KB也不奇怪。做道算術(shù)題堆內(nèi)存設(shè)4GB按單條連接200KB估算累積到2萬(wàn)條連接就是4GB左右。也就是說(shuō)堆里躺上2萬(wàn)個(gè)連接對(duì)象整個(gè)堆就基本廢了。而且連接數(shù)量越多GC掃描這些對(duì)象的時(shí)間越長(zhǎng)Full GC停頓會(huì)越來(lái)越明顯服務(wù)表現(xiàn)為周期性卡頓一直到OOM徹底爆發(fā)。1.2 兩類(lèi)累積泄漏型累積與池容量型累積“大量連接對(duì)象”這個(gè)描述太籠統(tǒng)實(shí)際要分成兩類(lèi)來(lái)看排查方向完全不同。第一類(lèi)是泄漏型累積。連接從池里借出來(lái)之后關(guān)閉動(dòng)作因?yàn)楫惓!⑻崆皉eturn、分支遺漏等原因沒(méi)有執(zhí)行連接對(duì)象就一直被某個(gè)線(xiàn)程棧或者靜態(tài)容器引用著永遠(yuǎn)回不到池里。池本身有上限但借出的連接只要還沒(méi)有歸還池的數(shù)據(jù)結(jié)構(gòu)里就一直持有它的狀態(tài)記錄。于是連接對(duì)象數(shù)量只增不減堆內(nèi)存持續(xù)爬升。這種在堆轉(zhuǎn)儲(chǔ)里的特征是連接對(duì)象被某條業(yè)務(wù)線(xiàn)程的棧幀、或者某個(gè)全局容器持久引用而不是掛在連接池的空閑隊(duì)列上。第二類(lèi)是池容量型累積。這種更隱蔽代碼沒(méi)有泄漏但連接池配置的maximumPoolSize本身極大池策略又允許大量空閑連接長(zhǎng)期存活。比如把池上限設(shè)成500系統(tǒng)實(shí)際日常只需要20個(gè)連接剩下的480個(gè)連接一直活著堆里躺著大量空閑連接純屬浪費(fèi)。每個(gè)空閑連接的內(nèi)存雖然不如活躍連接那么高但架不住數(shù)量多。更麻煩的是池創(chuàng)建連接的速度遠(yuǎn)大于釋放速度高峰期內(nèi)存占用會(huì)迅速膨脹。兩類(lèi)問(wèn)題都會(huì)讓你在堆轉(zhuǎn)儲(chǔ)里看到大量ConnectionImpl對(duì)象但前者要改代碼后者改配置加監(jiān)控就能解決。如果在第一步就把問(wèn)題歸錯(cuò)類(lèi)后面所有的排查動(dòng)作都會(huì)跑偏。2. 根因在哪里常見(jiàn)的連接對(duì)象累積路徑把這些年遇到過(guò)的線(xiàn)上事故疊在一起看連接對(duì)象累積基本逃不出下面幾種模式。2.1 手動(dòng)管理連接關(guān)閉路徑被異常跳過(guò)有些老項(xiàng)目里存在不少手寫(xiě)JDBC的場(chǎng)景開(kāi)發(fā)對(duì)連接池的理解停留在“調(diào)用getConnection就能拿連接”但關(guān)閉邏輯寫(xiě)得非常隨意。Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery(); while (rs.next()) { // 業(yè)務(wù)處理 } // 這里沒(méi)有close連接池里永遠(yuǎn)少一個(gè)連接這種代碼在低并發(fā)下長(zhǎng)期不炸因?yàn)檫B接池會(huì)自動(dòng)擴(kuò)容你總覺(jué)得連接夠用??闪髁恳坏┥蟻?lái)池里的連接被借光新請(qǐng)求就開(kāi)始排隊(duì)超時(shí)。同時(shí)泄漏一直在發(fā)生池在擴(kuò)容到上限之后也無(wú)法滿(mǎn)足需求堆內(nèi)存被新連接和等待隊(duì)列不斷撐大量變積累成質(zhì)變OOM就來(lái)了。還有一種更容易被忽視的寫(xiě)法明明寫(xiě)了finally塊做close但方法里catch塊把異常吞了而close操作又依賴(lài)前面某個(gè)資源的中間狀態(tài)。比如在try里初始化PreparedStatement時(shí)就拋了異常對(duì)象還是nullfinally卻直接調(diào)用ps.close()空指針把關(guān)閉動(dòng)作徹底打斷。這種問(wèn)題在代碼評(píng)審時(shí)極易漏掉因?yàn)檎B窂娇雌饋?lái)是完整關(guān)閉的只有異常路徑才露餡。2.2 連接池參數(shù)配置與業(yè)務(wù)容量嚴(yán)重失配另一類(lèi)常見(jiàn)情況是代碼邏輯沒(méi)有明顯泄漏問(wèn)題出在連接池容量規(guī)劃上。我見(jiàn)過(guò)有團(tuán)隊(duì)一遇到“連接池耗盡”的報(bào)錯(cuò)就簡(jiǎn)單粗暴把maxPoolSize從10改成1000。從工程角度講這不是在解決問(wèn)題而是給數(shù)據(jù)庫(kù)下毒。連接池的上限不只是內(nèi)存問(wèn)題1000個(gè)連接同時(shí)建到數(shù)據(jù)庫(kù)實(shí)例上數(shù)據(jù)庫(kù)端的線(xiàn)程、內(nèi)存、鎖全都會(huì)被拖垮整體吞吐量不升反降。更合理的做法是估算連接需求。經(jīng)驗(yàn)公式大概是需要連接數(shù) 接口QPS × 單次操作持有連接的平均秒數(shù)舉個(gè)例子某接口QPS是200每次請(qǐng)求大約占用連接200毫秒那么需要的連接數(shù)就是200 × 0.2 40再留20%到30%余量設(shè)置50就非常寬裕。如果QPS和平均耗時(shí)不變硬把池上限設(shè)成500大部分時(shí)間池里躺著450條空閑連接對(duì)JVM堆是純浪費(fèi)對(duì)數(shù)據(jù)庫(kù)是無(wú)謂壓力。2.3 Spring事務(wù)與MyBatis會(huì)話(huà)的隱性持有還有一類(lèi)比顯式泄漏更隱蔽的問(wèn)題連接并沒(méi)有丟失但被長(zhǎng)時(shí)間占用數(shù)量一多效果等同于泄漏。Spring的Transactional會(huì)把數(shù)據(jù)庫(kù)連接綁定到當(dāng)前線(xiàn)程的ThreadLocal資源管理器只有事務(wù)提交或回滾之后才釋放綁定。如果方法內(nèi)部嵌套了耗時(shí)很長(zhǎng)的本地緩存操作、大批量文件處理、或者一次外部HTTP調(diào)用連接就會(huì)一直被線(xiàn)程攥著不放。消息隊(duì)列消費(fèi)邏輯套事務(wù)注解是重災(zāi)區(qū)。一條消息被消費(fèi)時(shí)觸發(fā)了復(fù)雜的業(yè)務(wù)鏈路處理10秒連接就被綁定10秒。消費(fèi)線(xiàn)程池32個(gè)連接池上限30瞬時(shí)并發(fā)上來(lái)池里可用連接直接歸零。這類(lèi)問(wèn)題的特征是堆轉(zhuǎn)儲(chǔ)里的連接對(duì)象數(shù)量可能并不夸張但連接池的active指標(biāo)一直頂滿(mǎn)pending指標(biāo)持續(xù)上漲。修復(fù)方向不是調(diào)大連接池而是收縮事務(wù)邊界把遠(yuǎn)程調(diào)用、耗時(shí)操作全部挪到事務(wù)方法外面去。2.4 多數(shù)據(jù)源場(chǎng)景下的DataSource重復(fù)創(chuàng)建動(dòng)態(tài)數(shù)據(jù)源也是一個(gè)高頻坑位。為了做讀寫(xiě)分離或者多租戶(hù)隔離有些項(xiàng)目用Map緩存了多個(gè)DataSource實(shí)例每次切換數(shù)據(jù)源就new一個(gè)舊的實(shí)例卻沒(méi)有關(guān)閉。HikariDataSource創(chuàng)建后池后臺(tái)線(xiàn)程會(huì)一直存活。堆轉(zhuǎn)儲(chǔ)里不只是ConnectionImpl數(shù)量多還能看到多個(gè)HikariPool實(shí)例以及成倍增長(zhǎng)的池控制線(xiàn)程。這種場(chǎng)景下連接對(duì)象是被多個(gè)連接池對(duì)象分開(kāi)持有的單看數(shù)量可能沒(méi)到嚇人的程度但加上線(xiàn)程對(duì)象、隊(duì)列對(duì)象、緩沖區(qū)對(duì)象以后內(nèi)存占用依然非??捎^。定位后處理方式也很明確要么統(tǒng)一走路由層整個(gè)JVM只保留真正在用的數(shù)據(jù)源實(shí)例要么為動(dòng)態(tài)數(shù)據(jù)源設(shè)計(jì)完整的生命周期管理確保切換后舊實(shí)例能被正常關(guān)閉和回收。3. 從現(xiàn)象到定位一次完整的在線(xiàn)排查這一章是整個(gè)排查過(guò)程最實(shí)操的部分按我當(dāng)時(shí)處理事故的順序?qū)憽?.1 第一步用jstat觀察GC與堆內(nèi)存趨勢(shì)定位JVM內(nèi)存問(wèn)題最忌諱一上來(lái)就抓堆轉(zhuǎn)儲(chǔ)。正確的第一步是先看趨勢(shì)。登錄到報(bào)警機(jī)器先通過(guò)jps找到進(jìn)程PID然后執(zhí)行jstat -gcutil pid 1000 2000這條命令每秒采樣一次持續(xù)2000秒。輸出里重點(diǎn)看E、O、FGC這三列。E代表Eden區(qū)使用率O代表老年代使用率FGC是Full GC次數(shù)。如果你看到O列持續(xù)上漲Full GC之后O列也不回落說(shuō)明確實(shí)有對(duì)象在老年代不斷累積。再配合系統(tǒng)負(fù)載和接口監(jiān)控就能把問(wèn)題范圍先從“整臺(tái)JVM內(nèi)存溢出”壓縮到“某類(lèi)對(duì)象只增不減”。這一步的目的有兩個(gè)一是確認(rèn)堆轉(zhuǎn)儲(chǔ)有沒(méi)有必要抓二是為后續(xù)分析提供內(nèi)存使用基線(xiàn)。如果老年代穩(wěn)定不動(dòng)Full GC正常單純是堆參數(shù)設(shè)太小那調(diào)整-Xmx就行跟連接對(duì)象沒(méi)關(guān)系。如果像當(dāng)時(shí)那樣O列一路爬升就可以放心進(jìn)入下一步了。3.2 第二步抓堆轉(zhuǎn)儲(chǔ)確認(rèn)對(duì)象構(gòu)成確認(rèn)老年代在持續(xù)上漲后需要抓一份堆轉(zhuǎn)儲(chǔ)jcmd pid GC.heap_dump /tmp/heap-pid.hprof用jcmd而不是jmap -dump抓是因?yàn)閖cmd默認(rèn)不帶live參數(shù)不會(huì)先觸發(fā)一次Full GC能保留堆現(xiàn)場(chǎng)的原始狀態(tài)。抓堆之前先確認(rèn)磁盤(pán)剩余空間堆如果配置4GBhprof文件通常也會(huì)有接近4GB的大小別把磁盤(pán)寫(xiě)爆了。抓完堆之后先用快速命令看對(duì)象分布jmap -histo pid | head -40這段輸出的價(jià)值在于給出對(duì)象數(shù)量的Top N。在連接泄漏事故里你經(jīng)常會(huì)在最前面看到這樣幾個(gè)類(lèi)名com.mysql.cj.jdbc.ConnectionImplMySQL 8.x連接器com.zaxxer.hikari.pool.PoolEntryHikariCP池條目com.alibaba.druid.pool.DruidPooledConnectionDruid池包裝連接當(dāng)ConnectionImpl的對(duì)象數(shù)量達(dá)到幾十萬(wàn)級(jí)時(shí)基本可以確定方向。但我必須提醒一句對(duì)象數(shù)量多只是信號(hào)不直接等于泄漏。繼續(xù)往下看引用鏈才是真正的判定依據(jù)。3.3 第三步用MAT定位GC Root持有鏈路把hprof文件下載到本地用Eclipse MAT打開(kāi)。操作步驟是先看Histogram按對(duì)象個(gè)數(shù)排序過(guò)濾出ConnectionImpl這類(lèi)類(lèi)名右鍵選擇List objects - with outgoing references看這些連接都引用了什么再右鍵選擇Path to GC Roots - exclude weak references把GC Root路徑完整顯示出來(lái)。CG Root路徑通常會(huì)呈現(xiàn)三種典型情況連接被HikariPool內(nèi)部的ConcurrentBag引用這是連接池自己在持有的正常連接。如果數(shù)量已經(jīng)明顯超過(guò)池配置的上限說(shuō)明池被撐爆需要回到池配置審查。連接被某條線(xiàn)程棧的局部變量引用這是正在使用中的連接算不上異常。但如果這些線(xiàn)程都停留在同一個(gè)業(yè)務(wù)方法里而且數(shù)量持續(xù)上漲說(shuō)明這條方法大概率在泄漏連接。連接被某個(gè)實(shí)例字段或靜態(tài)容器引用這是最典型的泄漏實(shí)錘。代碼把Connection對(duì)象塞進(jìn)了數(shù)組、Map、ThreadLocal之類(lèi)容器里使用完沒(méi)有清理導(dǎo)致GC Root能一路找到它。通過(guò)MAT的引用路徑可以直接對(duì)應(yīng)到具體類(lèi)名和字段名再結(jié)合源碼倉(cāng)庫(kù)就能定位到方法行號(hào)比在代碼里瞎搜快得多。3.4 第四步結(jié)合業(yè)務(wù)代碼確認(rèn)泄漏點(diǎn)拿到MAT的結(jié)論后回到代碼倉(cāng)庫(kù)打開(kāi)對(duì)應(yīng)的類(lèi)。我在實(shí)際項(xiàng)目里遇到的漏法主要有三種catch分支里沒(méi)有釋放連接只有正常try路徑執(zhí)行了close方法返回前提前return跳過(guò)了統(tǒng)一釋放邏輯在循環(huán)體里多次調(diào)getConnection最后只關(guān)閉了一個(gè)前面的全部漏掉修好泄漏點(diǎn)之后先別急著擴(kuò)堆。部署新版本觀察連接池的活躍連接數(shù)和JVM內(nèi)存曲線(xiàn)。如果active數(shù)能隨著流量回落內(nèi)存曲線(xiàn)走平才算真正修好。這里還要提個(gè)建議修復(fù)一個(gè)泄漏點(diǎn)往往不夠建議在代碼庫(kù)全局搜索getConnection的調(diào)用點(diǎn)把所有手工獲取連接的代碼統(tǒng)一審一遍把同類(lèi)型的隱患一次性清理掉。4. 修復(fù)方案與參數(shù)調(diào)優(yōu)落地定位到根因之后修復(fù)方案要落在三個(gè)層面代碼寫(xiě)法、連接池配置、監(jiān)控告警。缺一不可。4.1 代碼層修復(fù)連接獲取與釋放的規(guī)范寫(xiě)法如果項(xiàng)目還在使用原生JDBC或者裸連接管理務(wù)必統(tǒng)一成try-with-resources寫(xiě)法try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { // 業(yè)務(wù)處理 } } catch (SQLException e) { log.error(query failed, e); }這種寫(xiě)法的好處是資源關(guān)閉順序由語(yǔ)言層面保證無(wú)論正常執(zhí)行還是拋出異常都會(huì)按逆序調(diào)用close方法。對(duì)于從連接池取出的代理Connection這個(gè)close的語(yǔ)義是“歸還連接池”并不是真正斷開(kāi)物理連接所以不用擔(dān)心頻繁開(kāi)關(guān)連接帶來(lái)的性能損耗。如果項(xiàng)目Java版本受限無(wú)法用try-with-resources也要遵守一個(gè)原則close動(dòng)作只寫(xiě)在finally里不要分散到try和catch的多個(gè)位置否則極易漏掉分支。還有一個(gè)實(shí)用技巧是給連接操作寫(xiě)一個(gè)統(tǒng)一的工具方法把連接獲取、關(guān)閉邏輯收斂到一處。業(yè)務(wù)代碼只調(diào)用工具方法從源頭杜絕寫(xiě)法混亂。4.2 連接池參數(shù)調(diào)整以HikariCP為例Spring Boot默認(rèn)使用的連接池是HikariCP配置文件可以這樣調(diào)整spring: datasource: hikari: maximum-pool-size: 50 minimum-idle: 10 connection-timeout: 30000 max-lifetime: 1800000 idle-timeout: 600000 leak-detection-threshold: 60000每個(gè)參數(shù)為什么要這樣設(shè)背后都有邏輯maximum-pool-size池上限。不要拍腦袋設(shè)一個(gè)整數(shù)按前面說(shuō)的QPS × 平均連接占用時(shí)間估算留20%到30%余量。連接數(shù)過(guò)大對(duì)數(shù)據(jù)庫(kù)本身也是一種壓力。minimum-idle池保持的最小空閑連接數(shù)。如果服務(wù)空閑期不想保留太多空閑連接可以把值調(diào)低配合idle-timeout使用如果日常流量波動(dòng)大建議設(shè)置成與maximum-pool-size一致避免池頻繁擴(kuò)縮容導(dǎo)致性能抖動(dòng)。connection-timeout客戶(hù)端等待連接的時(shí)間線(xiàn)上建議30秒。設(shè)太短突發(fā)流量時(shí)大量請(qǐng)求直接超時(shí)設(shè)太長(zhǎng)用戶(hù)側(cè)等待感過(guò)強(qiáng)。max-lifetime連接最大存活時(shí)間必須小于數(shù)據(jù)庫(kù)的wait_timeout。MySQL默認(rèn)wait_timeout是28800秒HikariCP默認(rèn)30分鐘。保持默認(rèn)值是安全的只要低于數(shù)據(jù)庫(kù)端即可。idle-timeout空閑連接最大存活時(shí)間。注意它只在池中連接數(shù)超過(guò)minimumIdle時(shí)才生效如果minimumIdle和maximumPoolSize相等空閑連接不會(huì)被銷(xiāo)毀。leak-detection-threshold連接被借出超過(guò)這個(gè)時(shí)間沒(méi)有歸還HikariCP會(huì)輸出警告日志。設(shè)成60000毫秒可以較早捕獲泄漏點(diǎn)。前提是它必須小于max-lifetime否則配置會(huì)沖突。這組參數(shù)改完后連接數(shù)曲線(xiàn)通常兩三個(gè)發(fā)布周期就能看到明顯收斂。參數(shù)本身不是越多越好而是要和業(yè)務(wù)容量的真實(shí)需求匹配。4.3 Druid的特殊兜底手段removeAbandoned如果你用的是Druid連接池它提供了一套針對(duì)連接泄漏的兜底配置spring: datasource: druid: initial-size: 5 min-idle: 5 max-active: 50 max-wait: 30000 remove-abandoned: true remove-abandoned-timeout-millis: 300000 log-abandoned: trueremove-abandoned的意思是連接被借出超過(guò)remove-abandoned-timeout-millis指定的毫秒數(shù)后還沒(méi)歸還Druid會(huì)從代碼里強(qiáng)制回收這個(gè)連接。log-abandoned打開(kāi)后Druid會(huì)打印包含調(diào)用棧的告警日志定位泄漏非常方便。但這把雙刃劍要用得謹(jǐn)慎。如果業(yè)務(wù)里存在合法的長(zhǎng)事務(wù)比如一個(gè)20分鐘的大批量導(dǎo)入任務(wù)而你設(shè)置的回收閾值是300秒事務(wù)后段的數(shù)據(jù)操作會(huì)被強(qiáng)制回收打斷引發(fā)數(shù)據(jù)一致性問(wèn)題。因此這類(lèi)兜底配置只建議在確實(shí)存在泄漏、且業(yè)務(wù)里沒(méi)有超過(guò)閾值的正常長(zhǎng)任務(wù)時(shí)才啟用。更穩(wěn)妥的順序是先用log-abandoned定位泄漏分支修復(fù)代碼再?zèng)Q定是否保留remove-abandoned作為最后防線(xiàn)。4.4 建立三層監(jiān)控防線(xiàn)排查和修復(fù)都完成后最后一步是把防線(xiàn)建立起來(lái)避免同類(lèi)問(wèn)題在無(wú)人值守的深夜再次爆發(fā)。我一般分三層來(lái)做第一層JVM內(nèi)存層。使用Spring Boot Actuator加Micrometer把指標(biāo)暴露給Prometheus重點(diǎn)監(jiān)控jvm_memory_used_bytes和Full GC次數(shù)。一旦老年代內(nèi)存持續(xù)增長(zhǎng)連續(xù)多個(gè)采集周期環(huán)比為正立刻告警。第二層連接池層。HikariCP會(huì)輸出hikaricp_connections_active、hikaricp_connections_idle、hikaricp_connections_pending這幾個(gè)關(guān)鍵指標(biāo)。建議給active占總?cè)萘勘壤O(shè)置80%閾值pending持續(xù)大于0且維持5分鐘以上就觸發(fā)告警。Druid也可以打開(kāi)監(jiān)控頁(yè)但生產(chǎn)環(huán)境一定要做訪問(wèn)鑒權(quán)。第三層SQL與事務(wù)層。開(kāi)啟數(shù)據(jù)庫(kù)慢查詢(xún)?nèi)罩景殉^(guò)1秒的SQL記錄下來(lái)。應(yīng)用側(cè)記錄每個(gè)事務(wù)方法的耗時(shí)超過(guò)閾值就預(yù)警。一條慢SQL執(zhí)行5秒就有一個(gè)連接被占用5秒。慢SQL一旦集中出現(xiàn)連接池很快見(jiàn)底。三層指標(biāo)互相配合連接對(duì)象的任何異常累積都能在早期被捕捉到。5. 常見(jiàn)問(wèn)題排查與避坑記錄這一路排查下來(lái)有些錯(cuò)誤和坑值得單獨(dú)記錄下來(lái)以后遇到類(lèi)似問(wèn)題能少走彎路?,F(xiàn)象可能原因排查或處理建議堆內(nèi)存持續(xù)上漲連接對(duì)象數(shù)量龐大連接泄漏或池配置過(guò)大先看GC Root引用鏈區(qū)分泄漏型還是容量型HikariCP連接池請(qǐng)求超時(shí)池耗盡多與慢SQL或長(zhǎng)事務(wù)有關(guān)查數(shù)據(jù)庫(kù)慢日志收縮事務(wù)邊界日志顯示連接一直未歸還方法提前return或異常分支漏close用try-with-resources重寫(xiě)復(fù)查catch分支連接池頻繁擴(kuò)縮容minimumIdle與maximumPoolSize差距過(guò)大把minimum-idle調(diào)高接近maximum-pool-sizeDruid強(qiáng)制回收后事務(wù)中斷長(zhǎng)任務(wù)超過(guò)回收閾值調(diào)大回收時(shí)間或去掉兜底優(yōu)先修代碼再分享三個(gè)實(shí)際踩過(guò)的坑第一個(gè)坑不要在業(yè)務(wù)高峰期直接抓dump并立刻重啟實(shí)例。抓dump本身會(huì)給服務(wù)增加一次內(nèi)存快照和磁盤(pán)寫(xiě)入如果堆已經(jīng)接近裝滿(mǎn)可能直接觸發(fā)又一次Full GC。更穩(wěn)妥的順序是先記錄現(xiàn)場(chǎng)指標(biāo)把實(shí)例從負(fù)載均衡上摘掉流量再抓dump最后重啟。先留證據(jù)再補(bǔ)救否則重啟完現(xiàn)場(chǎng)沒(méi)了根因也找不到了。第二個(gè)坑不要只盯著ConnectionImpl類(lèi)名就下結(jié)論。之前看一個(gè)案例dump里ConnectionImpl數(shù)量非常多我差點(diǎn)判定是代碼泄漏。但順著GC Root追下去發(fā)現(xiàn)這些連接幾乎全部被HikariPool自己持有原來(lái)是某次擴(kuò)容把maximumPoolSize沖到了800代碼本身反而沒(méi)問(wèn)題。引用鏈才是判官類(lèi)名數(shù)量只是線(xiàn)索。第三個(gè)坑連接池參數(shù)修完之后不要直接撒手。一個(gè)泄漏點(diǎn)往往只在一類(lèi)流量場(chǎng)景下才觸發(fā)比如某個(gè)爬蟲(chóng)任務(wù)、某個(gè)特殊的對(duì)賬流程。修完連接池配置把內(nèi)存壓下來(lái)不代表所有場(chǎng)景都安全了。只要代碼庫(kù)里還存在手工獲取連接的調(diào)用點(diǎn)建議花時(shí)間統(tǒng)一改造讓連接回收路徑經(jīng)得起評(píng)審。我個(gè)人在處理這類(lèi)問(wèn)題時(shí)的習(xí)慣是一開(kāi)始就拒絕調(diào)堆參數(shù)。加大-Xmx只是把癥狀往后推不會(huì)讓泄漏消失。正確路徑永遠(yuǎn)是把GC趨勢(shì)、堆轉(zhuǎn)儲(chǔ)、GC Root引用鏈走完讓證據(jù)說(shuō)話(huà)。如果你也被JVM內(nèi)存溢出和連接池累積問(wèn)題纏住按這套流程走一遍大部分事故都能在幾小時(shí)內(nèi)鎖定根因。最后送一句實(shí)操口訣連接泄漏看GC Root連接池容量看指標(biāo)修復(fù)之后看曲線(xiàn)。