十一、HikariCP源碼分析之HouseKeeper

歡迎訪問我的博客,同步更新: 楓山別院

源代碼版本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)時間的啊,如果時間被調整了,那么定時任務就錯亂了,后果非常嚴重,會導致該回收的連接回收不了。

開始正式任務之前,idleTimeoutnow依然是準備工作,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的功能我們分析完了

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容