HikariCP源碼分析之leakDetectionThreshold及實戰解決Spark/Scala連接池泄漏
1.這是一個系列,有興趣的朋友可以持續關注
2.如果你有HikariCP使用上的問題,可以給我留言,我們一起溝通討論
3.希望大家可以提供我一些案例,我也希望可以支持你們做一些調優
1. 概念
2. 源碼分析
2.1.1 HikariConfig
2.1.2 HouseKeeper
2.1.3 小結
2.2.1 getConnection
2.2.2 leakTaskFactory、ProxyLeakTaskFactory、ProxyLeakTask
2.2.3 close
3. 測試模擬
4. Spark/Scala連接池泄漏問題排查
5. 參考資料
6. 系列文章
宛如清風
S.E.N.S.
00:00/00:00
概念
此屬性控制在記錄消息之前連接可能離開池的時間量,單位毫秒,默認為0,表明可能存在連接泄漏。
如果大於0且不是單元測試,則進一步判斷:(leakDetectionThreshold maxLifetime && maxLifetime > 0),會被重置為0。即如果要生效則必須>0,而且不能小於2秒,而且當maxLifetime > 0時不能大於maxLifetime(默認值1800000毫秒=30分鐘)。
leakDetectionThreshold
This property controls the amount of time that a connection can be out of the pool before a message is logged indicating a possible connection leak. A value of 0 means leak detection is disabled. Lowest acceptable value for enabling leak detection is 2000 (2 seconds). Default: 0
源碼解析
我們首先來看一下leakDetectionThreshold用在了哪裡的綱要圖:
Write
還記得上一篇文章【追光者系列】HikariCP源碼分析之從validationTimeout來講講Hikari 2.7.5版本的那些故事提到:我們可以看到在兩處看到validationTimeout的寫入,一處是PoolBase構造函數,另一處是HouseKeeper線程。
leakDetectionThreshold的用法可以說是異曲同工,除了構造函數之外,也用了HouseKeeper線程去處理。
HikariConfig
validateNumerics方法中則是解釋了上文及官方文檔中該值validate的策略
該方法會被HikariConfig#validate所調用,而HikariConfig#validate會在HikariDataSource的specified configuration的構造函數使用到
也在每次getConnection的時候用到了,
這裡要特別提一下一個很牛逼的Double-checked_locking的實現,大家可以看一下這篇文章 https://en.wikipedia.org/wiki/Double-checked_locking#Usage_in_Java
HouseKeeper
這裡簡要說明一下,ScheduledThreadPoolExecutor是ThreadPoolExecutor類的子類,因為繼承了ThreadPoolExecutor類所有的特性。但是,Java推薦僅在開發定時任務程序時採用ScheduledThreadPoolExecutor類。
在調用shutdown()方法而仍有待處理的任務需要執行時,可以配置ScheduledThreadPoolExecutor的行為。默認的行為是不論執行器是否結束,待處理的任務仍將被執行。但是,通過調用ScheduledThreadPoolExecutor類的setExecuteExistingDelayedTasksAfterShutdownPolicy()方法則可以改變這個行為。傳遞false參數給這個方法,執行shutdown()方法之後,待處理的任務將不會被執行。
取消任務後,判斷是否需要從阻塞隊列中移除任務。其中removeOnCancel參數通過setRemoveOnCancelPolicy()設置。之所以要在取消任務後移除阻塞隊列中任務,是為了防止隊列中積壓大量已被取消的任務。
從這兩個參數配置大家可以了解到作者的對於HouseKeeper的配置初衷。
小結
Hikari通過構造函數和HouseKeeper對於一些配置參數進行初始化及動態賦值,動態賦值依賴於HikariConfigMXbean以及使用任務調度線程池ScheduledThreadPoolExecutor來不斷刷新配置的。
不允許在運行時進行改變的主要有
Read
getConnection
leakTaskFactory、ProxyLeakTaskFactory、ProxyLeakTask
在HikariPool構造函數里,初始化了leakTaskFactory,以及houseKeepingExecutorService。
如果leakDetectionThreshold=0,即禁用連接泄露檢測,schedule返回的是ProxyLeakTask.NO_LEAK,否則則新建一個ProxyLeakTask,在leakDetectionThreshold時間後觸發
NO_LEAK類裡頭的方法都是空操作
一旦該task被觸發,則拋出Exception("Apparent connection leak detected")
我們想起了什麼,是不是想起了【追光者系列】HikariCP源碼分析之allowPoolSuspension那篇文章里有著一摸一樣的設計?
isAllowPoolSuspension默認值是false的,構造函數直接會創建SuspendResumeLock.FAUX_LOCK;只有isAllowPoolSuspension為true時,才會真正創建SuspendResumeLock。
由於Hikari的isAllowPoolSuspension默認值是false的,FAUX_LOCK只是一個空方法,acquisitionSemaphore對象也是空的;如果isAllowPoolSuspension值調整為true,當收到MBean的suspend調用時將會一次性acquisitionSemaphore.acquireUninterruptibly從此信號量獲取給定數目MAX_PERMITS 10000的許可,在提供這些許可前一直將線程阻塞。之後HikariPool的getConnection方法獲取不到連接,阻塞在suspendResumeLock.acquire(),除非resume方法釋放給定數目MAX_PERMITS 10000的許可,將其返回到信號量
close
在connection的close的時候,delegate != ClosedConnection.CLOSED_CONNECTION時會調用leakTask.cancel();取消檢測連接泄露的task。
在closeStatements中也會關閉:
在checkException中也會關閉
小結關閉任務如下圖所示:
測試模擬
當代碼執行到了quietlySleep(SECONDS.toMillis(4));時直接按照預期拋異常Apparent connection leak detected。
緊接著在close的過程中執行到了delegate != ClosedConnection.CLOSED_CONNECTION來進行leakTask.cancel()
完整的測試輸出模擬過程如下所示:
Spark/Scala連接池泄漏問題排查
金融中心大數據決策數據組的同學找到反饋了一個問題:
我們在同一個jvm 需要連接多個資料庫時,發現總體上 從連接池borrow 的 connection 多於 歸還的,一段時間後 連接池就會報出
Caused by: java.sql.SQLTransientConnectionException: HikariPool-0 - Connection is not available, request timed out after 30000ms的異常。
用戶使用的spark的場景有點特殊,單機上開的鏈接很小,但是有很多機器都會去連。用戶在一個jvm中就只會並發1個鏈接。
程序也會出現block的情況,發現是執行mysql時出現的,
mysql show processlist;發現大多停留在query end的情況,程序 thread dump 進程 持有monitor的線程。
DBA介入之後發現存在slow sql。
當然,這個問題出了是寫頻繁導致的,一次寫入的量有點大,每一個sql都巨大走的batch,寫入的 records 數在每秒 30-50條,一個record 有70多個欄位。一個解決方式是把 binlog 移到 ssd 盤;還有一個方式是innodb_flush_log_at_trx_commit把這個參數改成0了,估計可能會提高20%~30%。
修復了如上一些問題之後,又發現用戶反饋的問題,加了leakDetectionThreshold,得出的結論是存在連接泄漏(從池中借用後連接沒有關閉)。
針對這個問題,我們懷疑的連接池泄漏的點要麼在hikari中,要麼在spark/scala中。採用排除法使用了druid,依然存在這個問題;於是我們就去翻spark這塊的代碼,仔細分析之後定位到了問題:
因為scala map懶載入,一開始mapPartitions都落在一個stage中,我們調整代碼toList之後result.iterator就分在獨立的stage中,連接池泄漏問題就不再存在。
根本原因可以參見《Spark : How to use mapPartition and create/close connection per partition
》:
https://stackoverflow.com/questions/36545579/spark-how-to-use-mappartition-and-create-close-connection-per-partition/36545821#36545821
一開始以為這是一個連接池問題,或者是spark問題,但是實際上通過leakDetectionThreshold的定位,我們得知實際上這是一個scala問題 :)
參考資料
https://segmentfault.com/a/1190000013092894
系列文章
TAG:工匠小豬豬的技術世界 |