Jedis連接池究竟是何物
一、前言
連接池的用途實際上有過開發(fā)經(jīng)驗的朋友都已經(jīng)比較清楚了,當資源對象的創(chuàng)建/銷毀比較耗時的場景下,可以通過"池化"技術(shù),達到資源的復用,以此來減少系統(tǒng)的開銷、增大系統(tǒng)吞吐量,比如數(shù)據(jù)庫連接池、線程池、Redis 連接池等都是使用的該方式,而我們在開發(fā)場景中使用較為廣泛的 Jedis 就是使用了 GenericObjectPool 作為它底層的連接池實現(xiàn)。
二、原理概述
圖示
圖片
- BorrowObject
業(yè)務(wù)模塊通過 BorrowObject 方法從空閑連接隊列中獲取空閑連接,最長會等待 maxWaitMillis 毫秒,如果拿不到則走 Create。
- ReturnObject
把連接重新放回到 IdleObjects 隊列中。
類結(jié)構(gòu)
圖片
Jedis里如何使用的
圖片
而 RedisConnectionFactory 的其中一個實現(xiàn)就是 JedisConnectionFactory,其中就包含了 Pool。
圖片
而 Pool 本身內(nèi)部就能看到我們真正的主角。
圖片
捋一下其中的關(guān)系,我們常用的 Spring-Data-Redis 的 Jedis 實現(xiàn)最終是通過以下的層級結(jié)構(gòu)來使用 GenericObjectPool 的。
圖片
三、深入分析
參數(shù)說明
如上述類結(jié)構(gòu)所示,GenericObjectPool 都是在 GenericObjectPoolConfig 或 BaseObjectPoolConfig 中進行配置相關(guān)參數(shù)的,其中核心參數(shù)以及默認值如下:
圖片
上圖對這些參數(shù)按顏色進行了一個歸類:
圖片
這里需要注意的是,雖然 GenericObjectPool 支持我們配的參數(shù)較多,但是 Spring-Data-Redis 將這部分參數(shù)收斂了,具體可供我們修改的只有表格上面的這部分內(nèi)容,其他參數(shù),有一部分在 JedisPoolConfig 類中,繼承了 GenericObjectPoolConfig 進行了修改,比如 Spring-Data-Redis 就修改了以下參數(shù)的默認值。
testWhileIdle=true
minEvictableIdleTimeMillis=60000
timeBetweenEvictinotallow=30000
numTestsPerEvictinotallow=-1
核心方法
本文只會針對方法的一些核心鏈路進行說明,如想知道更多細節(jié),針對源碼解析的可以在網(wǎng)上搜索其他相關(guān)文章或是到我的參考鏈接里進行翻看。
BorrowObject
- 超時時間怎么用的?
該方法用于從連接池中獲取一個空閑對象,它有可能是從空閑池中直接獲取的,或是直接創(chuàng)建出來的,如果第一次從空閑對象中沒有獲取到,會走創(chuàng)建后重新獲取,此時如果對象池目前配置的 BlockWhenExhausted=true,那么就會受 maxWaitMillis 參數(shù)所配置的超時時間所控制,如果超過了超時時間,都沒拿到一個空閑的對象,則會直接拋出異常。
- testOnBorrow 和 testOnCreate 的使用場景
當獲取到一個對象后,由于對象池中往往存放的是諸如數(shù)據(jù)庫連接、Redis 連接等創(chuàng)建時較為耗時的資源,但是因為連接本身是復用的,如果 MySQL/Redis Server 端如果因為某些原因斷開/釋放了該鏈接,那么此時拿到的對象就是個無效的對象,因此在 borrowObject 階段會判定,如果:
testOnBorrow=true || (create && testOnCreate=true)
就會走到:
factory.validateObject
這里如何進行 validateObject 的,是由上層使用對象池的場景所決定的,比如在 Jedis 場景中,會向 Redis Server 發(fā)送一個 Ping 命令,如果 Server 返回了 Pong,則認為該連接仍然有效,可以給業(yè)務(wù)層使用。
但是!?。。。?!
線上環(huán)境千萬不要配置 testOnBorrow=true 或是 testOnCreate=true。
每個對象的獲取都需要先校驗再拿,會大大增加單次請求的 RT。
ReturnObject
- testOnReturn 的使用場景
實際上 testOnReturn 的使用場景與上述 borrowObject 時的 testOnBorrow 是類似的,只是testOnReturn就是一個歸還對象的操作。同理,線上千萬不要配置 testOnReturn=true。
- 什么時候歸還,什么時候銷毀?
對象池中維護了一個結(jié)構(gòu)為 LinkedBlockingDeque,名為 IdleObjects 的對象用于維護空閑對象隊列,且是否歸或銷毀的判斷邏輯如下:
final int maxIdleSave = getMaxIdle();
if (isClosed() || maxIdleSave > -1 && maxIdleSave <= idleObjects.size()) {
...銷毀對象
}else{
...返還至idleObjects
}
如果:
- 對象池已經(jīng)關(guān)閉(只要是程序在運行,且正常使用,不會關(guān)閉)
或
- 配置了 maxIdle 且空閑對象列表數(shù)量 >=maxIdle
則對象會被銷毀,否則對象會重新回到 IdleObjects 中。
四、內(nèi)部機制
Evict(定期驅(qū)逐/?;顧C制)
- 周期怎么定?
當 timeBetweenEvictionRunsMillis 配置 >0 時,在 GenericObjectPool 所繼承的基類中,會啟一個周期性執(zhí)行的線程,它的執(zhí)行周期就是 timeBetweenEvictionRunsMillis 的值。
- 為什么要驅(qū)逐?
當空閑對象過多,對于客戶端或服務(wù)端的 TCP 連接維護來講,本身就是一個開銷,因此,需要有一個規(guī)則,當有一些對象實在太空閑了,就把它們踢掉。
- 哪些對象應(yīng)該被驅(qū)逐?
首先會從空閑對象列表中挑選出一部分對象,而這個挑選過程本身也有一個規(guī)則,它受 numTestsPerEvictionRun 參數(shù)控制。
當 numTestsPerEvictionRun>0,會挑選出 numTestsPerEvictionRun 數(shù)量的空閑連接進行檢查。
當 numTestsPerEvictionRun<0 時,首先會對 numTestsPerEvictionRun 取絕對值,再然后挑選出空閑數(shù)量 /numTestsPerEvictionRun 絕對值的數(shù)量進行檢查,舉個例子,如果 numTestsPerEvictinotallow=-2,就會挑選出一半進行檢查。
- 驅(qū)逐檢查怎么做?
本身驅(qū)逐檢查的實現(xiàn)方式是支持自定義的,也就是 evictionPolicy 參數(shù),但是往往只會選擇用默認的實現(xiàn),也就是 DefaultEvictionPolicy,它的驅(qū)逐檢查策略如下:
if ((config.getIdleSoftEvictTime() < underTest.getIdleTimeMillis() &&
config.getMinIdle() < idleCount) ||
config.getIdleEvictTime() < underTest.getIdleTimeMillis()) {
return true;
}
return false;
underTest 為被檢查對象,當存在以下場景時,滿足驅(qū)逐檢查規(guī)則,會觸發(fā)驅(qū)逐。
underTest 的空閑時間 > softMinEvictableIdleTimeMillis 且當前空閑對象數(shù)量 > minIdle 或 underTest 的空閑時間 > minEvictableIdleTimeMillis。
Tips:有一些好奇的同學可能會問,對象的空閑時間是怎么算的?
池中的對象本身會維護一個 lastReturnTime 的時間戳,它會隨著對象每一次 returnObject 時進行更新,當獲取對象空閑時間時,只要它還是在空閑對象中,那么用當前時間戳 -lastReturnTime 就是認為該對象的空閑時間。
- 驅(qū)逐與保活的關(guān)系是怎么樣的?
由于前面提到過,不能配置 testOnBorrow 和 testOnReturn,那么如果 Server 端的鏈接直接斷開了,怎么能保證池中對象的有效性呢?如果讓調(diào)用端調(diào)用時再觸發(fā),會不會太晚了呢?這時候就有個參數(shù) testWhileIdle,當此參數(shù)打開時,就代表會在對象空閑時進行對象可用性檢查,具體代碼如下:
if (evict) {
destroy(underTest);
destroyedByEvictorCount.incrementAndGet();
} else {
if (testWhileIdle) {
try {
factory.activateObject(underTest);
} catch (final Exception e) {
destroy(underTest);
destroyedByEvictorCount.incrementAndGet();
}
}
}
這里隱掉了一些相關(guān)的非核心邏輯,這里可以看到 testWhileIdle 的?;顧C制實際上和 evict 是配套使用的,如果被檢查對象需要被驅(qū)逐,也就是 evict=true,則會直接 destory 對象,否則它會判定 testWhileIdle 的狀態(tài),此時如果 testWhileIdle=true,那么就會激活一下對象,具體激活的方式是由使用對象池的上層工廠所決定的。
Test(檢查機制)
testOnBorrow、testOnReturn、testOnCreate。
這幾個基本看名字就知道是什么意思了,在前面講 borrowObject 和 returnObject 的時候也有提到,還有一個相對比較特別的是:
testWhileIdle。
該參數(shù)目的是為了對象在空閑期間可以進行檢查,而它的觸發(fā)實際上是和 evict(定期驅(qū)逐機制)聯(lián)合起來進行使用的。
Abandoned(拋棄機制)
五、排障方式
本身 GenericObjectPool 默認會把自己的一些參數(shù)通過 JMX 的方式進行注冊,那么我們可以通過 Jvisualvm 進行查看,或是通過 Arthas,輸入如下命令:
mbean org.apache.commons.pool2:type=GenericObjectPool,name=pool-redisConnectionFactory
可以獲取到對象池當前的一些屬性,如下圖:
圖片
其中對于優(yōu)化比較有用的就是 CreatedCount(創(chuàng)建對象的數(shù)量)、DestoryedCount(對象銷毀的對象)、DestoryedByEvictorCount(因為驅(qū)逐機制而被銷毀的對象數(shù)量)。
六、總結(jié)
上述文章以 Jedis 為引,分析了 GenericObjectPool 連接池的底層原理以及 Jedis 是如何使用該連接池的,并且結(jié)合了 Arthas 分享了一個簡單的排障方式,實際上如果知道了 GenericObjectPool 連接池的原理,其他連接池也是大同小異,本文希望拋磚引玉,帶大家對于連接池的底層實現(xiàn)有個基本概念,相信以后遇到此類問題也會有分析的思路,不再迷茫~