歡迎訪問我的博客,同步更新: 楓山別院
源代碼版本2.4.5-SNAPSHOT
HouseKeeper是一個HikariPool的內部類,它實現(xiàn)了Runnable接口,也就是一個線程任務。這個任務是由ScheduledThreadPoolExecutor類型的線程池執(zhí)行的,也就是說它是一個定時任務。我們在《HikariCP源碼分析之初始化分析二》中分析 HikariCP 初始化的時候,遇到了houseKeepingExecutorService的初始化,簡單分析了它的初始化過程,但是這個任務是非常重要的,我們要仔細分析一下。
它的主要作用就是:檢測時間回撥,并關閉空閑時間超期的連接。下面的代碼依然是有詳細的注釋,同時也記錄了我當時自己分析代碼時遇到的一些疑問和解答。
我們看下代碼:
/**
* HouseKeeper用于空閑連接過期
*/
private class HouseKeeper implements Runnable {
private volatile long previous = clockSource.plusMillis(clockSource.currentTime(), -HOUSEKEEPING_PERIOD_MS);
@Override
public void run() {
//①
//刷新通過MBean修改的設置
connectionTimeout = config.getConnectionTimeout();
validationTimeout = config.getValidationTimeout();
leakTask.updateLeakDetectionThreshold(config.getLeakDetectionThreshold());
//②
final long idleTimeout = config.getIdleTimeout();
final long now = clockSource.currentTime();
//檢測時間回撥, 即網絡對時服務對時鐘的調整, 允許 128 毫秒的時間差
if (clockSource.plusMillis(now, 128) < clockSource.plusMillis(previous, HOUSEKEEPING_PERIOD_MS)) {
LOGGER.warn("{} - Retrograde clock change detected (housekeeper delta={}), soft-evicting connections from pool.",
clockSource.elapsedDisplayString(previous, now), poolName);
previous = now;
//連接池中所以的連接都標記刪除
softEvictConnections();
//重新創(chuàng)建連
fillPool();
return;
} else if (now > clockSource.plusMillis(previous, (3 * HOUSEKEEPING_PERIOD_MS) / 2)) {
// No point evicting for forward clock motion, this merely accelerates connection retirement anyway
//時鐘快了, 沒必要調整連接池, 反正是加速了連接的過期, 不影響
LOGGER.warn("{} - Thread starvation or clock leap detected (housekeeper delta={}).", clockSource.elapsedDisplayString(previous, now), poolName);
}
//原來的實現(xiàn)代碼如下文件的633-650 行: https://github.com/brettwooldridge/HikariCP/blob/bc010fba486b27ae3d034cc9701e0c4217457ddb/src/main/java/com/zaxxer/hikari/pool/HikariPool.java
// logPoolState("Before cleanup ");
// for (PoolBagEntry bagEntry : connectionBag.values(STATE_NOT_IN_USE)) {
// if (connectionBag.reserve(bagEntry)) {
// if (bagEntry.evicted) {
// closeConnection(bagEntry, "(connection evicted)");
// }
// else if (idleTimeout > 0L && clockSource.elapsedMillis(bagEntry.lastAccess, now) > idleTimeout) {
// closeConnection(bagEntry, "(connection passed idleTimeout)");
// }
// else {
// connectionBag.unreserve(bagEntry);
// }
// }
// }
//
// logPoolState("After cleanup ");
//
// fillPool(); // Try to maintain minimum connections
// }
// 代碼中, 先將所有超過空閑時間的連接都關閉, 然后將連接池中的連接再填充到minIdle最小空閑連接數(shù)
// 后來有個名為 yaojuncn 的人跟brett提了個 issue, 如下: https://github.com/brettwooldridge/HikariCP/issues/379
// issue的內容就是yaojuncn發(fā)現(xiàn), 清理空閑連接的時候, 連接數(shù)會小于minIdle, 極端情況下會是 0, 他認為這樣有問題, 服務請求多的時候, 會大量的創(chuàng)建連接, 給數(shù)據(jù)庫造成壓力
// 但是brett認為, 創(chuàng)建連接非??? 極端情況的幾率極小, 這不是個問題, 提議yaojuncn使用固定大小的連接池
// 討論來討論去, brett終于煩了, 接受了yaojuncn的建議, 并且合并了yaojuncn的 merge request.
// yaojuncn提的實現(xiàn)就是目前的請清理方式, 這個: https://github.com/yaojuncn/HikariCP/commit/cbb1e1cc93d050457ffe9939b67eacd6c6bd97a0
//③
//開始清理超過idleTimeout的空閑連接
previous = now;
String afterPrefix = "Pool ";
if (idleTimeout > 0L) {
//查出連接池中所有的空閑連接
final List<PoolEntry> idleList = connectionBag.values(STATE_NOT_IN_USE);
//空閑連接數(shù)量 - 用戶配置的最小連接數(shù) = 目前可以回收的連接數(shù), 不明白詳見Question①
int removable = idleList.size() - config.getMinimumIdle();
//如果有可以回收的連接
if (removable > 0) {
logPoolState("Before cleanup ");
afterPrefix = "After cleanup ";
// 按照最近訪問的實際, 從小到大排序, 排序指標是最后訪問時間的時間戳, 時間大的是最近使用的, 從小到大遍歷比較合理, 能先清理掉長時間沒用的, 不用遍歷所有的空閑連接
//如果要清理的連接數(shù)夠了,那么就不用繼續(xù)遍歷了,可以減少循環(huán)次數(shù)
Collections.sort(idleList, LAST_ACCESS_COMPARABLE);
for (PoolEntry poolEntry : idleList) {
//判斷最后訪問時間和當前時間的時間差, 是否超過了用戶配置的最大空閑時間, 超過了就將連接變?yōu)楸A魻顟B(tài)
if (clockSource.elapsedMillis(poolEntry.lastAccessed, now) > idleTimeout && connectionBag.reserve(poolEntry)) {
//關閉連接
closeConnection(poolEntry, "(connection has passed idleTimeout)");
//可回收連接數(shù)減 1, 如果可回收連接數(shù)等于 0, 就是清理完了
if (--removable == 0) {
break; // keep min idle cons
}
}
}
}
}
//記錄日志
logPoolState(afterPrefix);
//可能有些連接過期了, 重新填充連接池到用戶配置的最小連接數(shù)
fillPool(); // Try to maintain minimum connections
}
}
①刷新配置
如果你看過《HikariCP源碼分析之獲取連接流程二》的話可能還記得,我們是可以通過 JMX 的方式來掛起整個連接池的,此時連接池是不可用的狀態(tài),然后我們就可以修改連接池的一些配置,然后將連接池恢復。修改了配置之后,并不是立即生效的,因為配置是在這里刷新的,而這里是一個定時任務,是每 30 秒觸發(fā)一次。
為什么不能立即修改這些配置呢?
因為是在運行期修改的配置,你修改配置之后,連接池中之前的連接還是原來的配置呢,總得要處理一下這些連接吧?而這個處理過程正好是HouseKeeper的職責范圍,因此就在這里刷新配置了,而且使用 JMX 修改配置這個需求,對時效性實在沒有什么要求,30 秒完全可以接受,畢竟這不是一個常規(guī)的操作,一般是測試用途。
至于刷新的配置內容,略過,大家可以看下配置分析那一節(jié)的內容。
②時間回撥
這里比較有意思,通常我們的服務器都是有網絡對時服務的,如果本地的系統(tǒng)時間不對的話就會自動調整。但是我們的 HikariCP 中的定時任務是依賴系統(tǒng)時間的啊,如果時間被調整了,那么定時任務就錯亂了,后果非常嚴重,會導致該回收的連接回收不了。
開始正式任務之前,idleTimeout和now依然是準備工作,idleTimeout是用戶的配置項,連接的最大空閑時間,而now就是當前的系統(tǒng)時間。
我們看下這個if判斷clockSource.plusMillis(now, 128) < clockSource.plusMillis(previous, HOUSEKEEPING_PERIOD_MS),clockSource.plusMillis(now, 128)的意思是當前時間加128毫秒,clockSource是一個時間工具類。previous是上一次任務的執(zhí)行時間,HOUSEKEEPING_PERIOD_MS是任務的執(zhí)行間隔,它們相加也就是本次任務應該執(zhí)行的時間。如果當前時間+128 毫秒,小于,當前任務應該觸發(fā)的時間,那么就是系統(tǒng)時間回退了128 毫秒以上對吧?這個是不行的。
有兩種情況HikariCP 是可以容忍的:
系統(tǒng)時間回退 128 毫秒以內
系統(tǒng)時間前進了,具體多長時間不管
上面兩種情況下,是不會進入 if 條件里的。但是我們還是要分析一下的,假如我們進入了 if 代碼塊,previous = now;這個的業(yè)務意思保存一下本次任務執(zhí)行的時間,因為下次執(zhí)行任務要用。softEvictConnections();一句簡簡單單的代碼,就將連接池中所有的連接都驅逐出去了,連接首先會被標記刪除,然后就真的關閉了這個連接,我們后面可以出一個 HikariCP 中連接關閉的單獨文章分析下。fillPool();含義一眼就能看出來,是重新填充連接池,重新創(chuàng)建連接加入到連接池中。
如果是 else-if ,那么就是系統(tǒng)時間被調快了,這個只是加速了連接的生命結束,對 HikariCP 沒有影響,連接被回收了是會自動創(chuàng)建新的連接,這個沒有關系,因此不處理,只是打印一個警告。
③清理過期連接
我們直接看 if 里面,使用connectionBag.values(STATE_NOT_IN_USE)方法查詢出來所有的空閑狀態(tài)的連接,int removable = idleList.size() - config.getMinimumIdle();計算了當前空閑連接數(shù)超出用戶配置數(shù)幾個,也就是要清理的連接個數(shù)。
如果removable大于0,那么確實有需要清理的連接。
這里Collections.sort(idleList, LAST_ACCESS_COMPARABLE);先對所有的空閑連接按照最后使用時間從小到大進行排序,因為每個連接上都記錄了最后使用時間,時間戳越小的,說明它最后使用時間越早,越大的越是最近使用過的。最近使用過的連接很可能被某個線程保存在本地的 ThreadLocal 中了,我們不清理這些連接,方便線程下次使用的時候直接獲取。
然后循環(huán)遍歷排序后的空閑連接,if 的條件是clockSource.elapsedMillis(poolEntry.lastAccessed, now) > idleTimeout && connectionBag.reserve(poolEntry),如果clockSource.elapsedMillis(poolEntry.lastAccessed, now),它是連接上次使用時間距離當前時間的時間差,大于用戶配置的連接的最大空閑時間,說明這個連接空閑了太久,需要回收,此時在 if 條件中就執(zhí)行connectionBag.reserve(poolEntry),修改連接的狀態(tài),修改成功之后,closeConnection方法是關閉這個底層連接,是真的被關閉了,然后可回收連接數(shù)減 1,繼續(xù)循環(huán),直到可回收連接數(shù)為 0 ,說明已經達到了用戶的配置要求,結束。
但是有個問題,我們是回收了多余的空閑連接,假如在這期間,有其他連接生命周期時間到了,被關閉了,或者是連接發(fā)生了致命錯誤,被關閉了。那么,現(xiàn)在剩下的連接數(shù)又少于用戶配置的最小空閑連接數(shù)了,怎么辦呢?
這就是fillPool()的作用,它會將連接池中的連接數(shù)量重新填充到最小連接數(shù)。
好了,至此,HouseKeeper的功能我們分析完了