“自以為對的”MyBatis空閑連接探測的機制
最近碰到個現(xiàn)象,某個應(yīng)用,每天在21:00-23:00才會執(zhí)行,連接數(shù)據(jù)庫執(zhí)行操作,間隔性出現(xiàn)連接超時的錯誤,
- Connection timed out (Read failed)
因為應(yīng)用和數(shù)據(jù)庫是跨網(wǎng)段,咨詢了下,防火墻超時時間配置的是30分鐘,應(yīng)用用的MyBatis連接池,相關(guān)配置如下,
相關(guān)參數(shù)解釋,如下所示,
POOLED– 這種數(shù)據(jù)源的實現(xiàn)利用“池”的概念將 JDBC 連接對象組織起來,避免了創(chuàng)建新的連接實例時所必需的初始化和認證時間。這種處理方式很流行,能使并發(fā) Web 應(yīng)用快速響應(yīng)請求。
除了上述提到UNPOOLED下的屬性外,還有更多屬性用來配置POOLED的數(shù)據(jù)源:
poolMaximumActiveConnections – 在任意時間可存在的活動(正在使用)連接數(shù)量,默認值:10
poolMaximumIdleConnections – 任意時間可能存在的空閑連接數(shù)。
poolMaximumCheckoutTime – 在被強制返回之前,池中連接被檢出(checked out)時間,默認值:20000 毫秒(即 20 秒)
poolTimeToWait – 這是一個底層設(shè)置,如果獲取連接花費了相當長的時間,連接池會打印狀態(tài)日志并重新嘗試獲取一個連接(避免在誤配置的情況下一直失敗且不打印日志),默認值:20000 毫秒(即 20 秒)。
poolMaximumLocalBadConnectionTolerance – 這是一個關(guān)于壞連接容忍度的底層設(shè)置, 作用于每一個嘗試從緩存池獲取連接的線程。如果這個線程獲取到的是一個壞的連接,那么這個數(shù)據(jù)源允許這個線程嘗試重新獲取一個新的連接,但是這個重新嘗試的次數(shù)不應(yīng)該超過 poolMaximumIdleConnections 與 poolMaximumLocalBadConnectionTolerance 之和。默認值:3(新增于 3.4.5)
poolPingQuery – 發(fā)送到數(shù)據(jù)庫的偵測查詢,用來檢驗連接是否正常工作并準備接受請求。默認是“NO PING QUERY SET”,這會導致多數(shù)數(shù)據(jù)庫驅(qū)動出錯時返回恰當?shù)腻e誤消息。
poolPingEnabled – 是否啟用偵測查詢。若開啟,需要設(shè)置 poolPingQuery 屬性為一個可執(zhí)行的 SQL 語句(最好是一個速度非??斓?SQL 語句),默認值:false。
poolPingConnectionsNotUsedFor – 配置 poolPingQuery 的頻率??梢员辉O(shè)置為和數(shù)據(jù)庫連接超時時間一樣,來避免不必要的偵測,默認值:0(即所有連接每一時刻都被偵測 — 當然僅當 poolPingEnabled 為 true 時適用)。
P.S.
https://mybatis.org/mybatis-3/zh/configuration.html#environments
按照這字面意思,一開始我們理解poolPingConnectionsNotUsedFor參數(shù)控制的是連接多久沒用,即處于空閑狀態(tài),在參數(shù)poolPingEnabled開啟時,就會執(zhí)行poolPingQuery定義的SQL主動探測。
如果按照這理解,poolPingConnectionsNotUsedFor設(shè)置了3000,即3秒,遠小于30分鐘防火墻超時的設(shè)置,不應(yīng)該出現(xiàn)連接超時的現(xiàn)象。
我們懷疑過防火墻的配置,但從應(yīng)用端看,并不是所有的請求都超時,而且防火墻端,沒看到什么異常。數(shù)據(jù)庫層,應(yīng)該未設(shè)置過相關(guān)的配置。
原因是什么?
作為一款成熟的產(chǎn)品,不太可能因為bug,更多還是對他的理解存在偏差。
下載3.3.0的源碼,鏈接如下,
https://github.com/mybatis/mybatis-3/releases/tag/mybatis-3.3.0
搜索這幾個參數(shù)所在的文件,找到了PooledDataSource類,可以看到這三個參數(shù)都設(shè)置了初始值,
看下這個pingConnection方法,
如果連接未關(guān)閉,判斷邏輯如下,
1. poolPingConnectionsNotUsedFor的值>=0;
2. getTimeElapsedSinceLastUse()>poolPingConnectionsNotUsedFor;
getTimeElapsedSinceLastUse()定義如下,
lastUsedTimestamp是在構(gòu)造函數(shù)PooledConnection中定義的,
PooledConnection會在獲取連接(popConnection)和回收連接(pushConnection)的時候調(diào)用,獲取連接和回收連接則會被getConnection()和invoke()調(diào)用,因此,(2)的意思是當前這個連接空閑的時間是否大于這個參數(shù)poolPingConnectionsNotUsedFor定義的時間。
3. 如果滿足條件(1)和(2),則會執(zhí)行poolPingQuery的SQL,此處就是"select 1 from dual",如果執(zhí)行失敗,會關(guān)閉這個連接,
從應(yīng)用日志,能看到這些信息,
- Testing connection 0000000000 ...
- Execution of ping query 'select 1 from dual' failed: Connection timed out (Read failed)
這個問題的關(guān)鍵,就是這個pingConnection,在什么時候調(diào)用,就決定了poolPingConnectionsNotUsedFor什么時候起作用,可以看到,他是在這個isValid的方法中調(diào)用的,
而這個isValid是在每次獲取連接和回收連接時調(diào)用的,換句話說,他是被動調(diào)用,并不是我們認為的空閑時主動調(diào)用,所以這個應(yīng)用,只是晚上會跑,空閑連接超過30分鐘是很正常的,
應(yīng)用開了debug,這兩段之間的間隔時間,就是得到超時連接的時間,
經(jīng)過單線程測試,大約在15分鐘,
因此,對這種testOnBorrow的連接探測機制,各有優(yōu)缺點,優(yōu)點就是會在一定程度保證應(yīng)用正常的業(yè)務(wù)請求得到可用的連接,畢竟不可用的連接都已經(jīng)被poolPingQuery定義的SQL測試了,一般情況下,不會讓正常的業(yè)務(wù)請求出現(xiàn)報錯,除非連接池沒任何可用的連接。缺點就是如果配置的poolPingConnectionsNotUsedFor很小,某些請求都會在執(zhí)行之前先進行驗證,但是換個角度,如果是高并發(fā),只要參數(shù)不是0,一般可能都不會滿足需要驗證的條件,如果設(shè)置為0,就可能會有很多pingQuery定義的SQL執(zhí)行。而且,如果像上述單線程的操作,他會一個連接一個連接的嘗試,等待一個連接出現(xiàn)超時錯誤的時間間隔是15分鐘,這就很低效了。
對連接池的選擇和配置,確實得結(jié)合實際場景需求來決策。
通過這個問題,至少讓我明白,“自以為對的”機制正確還是錯誤,還是看他的實現(xiàn),這才是最可靠的驗證,而且,通過他的邏輯,可以讓我們借鑒一些設(shè)計路徑,多考慮他這么做背后的意義和影響,更有助我們將其用到正確的場景。