歡迎光臨
每天分享高質量文章

【追光者系列】HikariCP 原始碼分析之從 validationTimeout 來講講 2.7.5 版本的那些故事

點擊上方“芋道原始碼”,選擇“置頂公眾號”

技術文章第一時間送達!

原始碼精品專欄

 


摘要: 原文可閱讀 http://www.iocoder.cn/HikariCP/zhazhawangzi/validationTimeout 「渣渣王子」歡迎轉載,保留摘要,謝謝!

  • 概念

  • 原始碼解析

  • Write

    • #PoolBase

    • #HouseKeeper

  • Read

    • #getConnection

    • #newConnection

  • Hikari 2.7.5的故事

  • 兩個關鍵的Mbean

  • 2.7.5迎來了不可變設計

  • 且看大神論道


img

今晚給大家講一個故事,如上圖所示,Hikari作者brettwooldridge先生非常無奈的在issue里回覆了一句“阿門,兄弟”,到底發生了什麼有趣的故事呢?這是一篇風格不同於以往的文章,就讓我來帶大家從原始碼validationTimeout分析角度一起揭開這個故事的面紗吧~

概念

此屬性控制連接測試活動的最長時間。這個值必須小於connectionTimeout。最低可接受的驗證超時時間為250 ms。 預設值:5000。

validationTimeout
This property controls the maximum amount of time that a connection will be tested for aliveness. This value must be less than the connectionTimeout. Lowest acceptable validation timeout is 250 ms. Default: 5000

更多配置大綱詳見文章 《【追光者系列】HikariCP預設配置》

img

原始碼解析

我們首先來看一下validationTimeout用在了哪裡的綱要圖:

img

Write

我們可以看到在兩處看到validationTimeout的寫入,一處是PoolBase建構式,另一處是HouseKeeper執行緒。

PoolBase

在com.zaxxer.hikari.pool.PoolBase中的建構式宣告了validationTimeout的初始值,而該值真正來自於com.zaxxer.hikari.HikariConfig的Default constructor,預設值為

private static final long VALIDATION_TIMEOUT = SECONDS.toMillis(5);

但是在HikariConfig的set方法中又做了處理

/** {@inheritDoc} */
   @Override
   public void setValidationTimeout(long validationTimeoutMs)
   
{
      if (validationTimeoutMs 250) {
         throw new IllegalArgumentException("validationTimeout cannot be less than 250ms");
      }
      this.validationTimeout = validationTimeoutMs;
   }

這就是概念一欄所說的如果小於250毫秒,則會被重置回5秒的原因。

HouseKeeper

我們再來看一下com.zaxxer.hikari.pool.HikariPool這個代碼,該執行緒嘗試在池中維護的最小空閑連接數,並不斷掃清的通過MBean調整的connectionTimeout和validationTimeout等值。
HikariCP有除了這個HouseKeeper執行緒之外,還有新建連接和關閉連接的執行緒。

/**
    * The house keeping task to retire and maintain minimum idle connections.
    */

   private final class HouseKeeper implements Runnable
   
{
      private volatile long previous = plusMillis(currentTime(), -HOUSEKEEPING_PERIOD_MS);
      @Override
      public void run()
      
{
         try {
            // refresh timeouts in case they changed via MBean
            connectionTimeout = config.getConnectionTimeout();
            validationTimeout = config.getValidationTimeout();
            leakTask.updateLeakDetectionThreshold(config.getLeakDetectionThreshold());
            final long idleTimeout = config.getIdleTimeout();
            final long now = currentTime();
            // Detect retrograde time, allowing +128ms as per NTP spec.
            if (plusMillis(now, 128)                LOGGER.warn("{} - Retrograde clock change detected (housekeeper delta={}), soft-evicting connections from pool.",
                           poolName, elapsedDisplayString(previous, now));
               previous = now;
               softEvictConnections();
               fillPool();
               return;
            }
            else if (now > 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={}).", poolName, elapsedDisplayString(previous, now));
            }
            previous = now;
            String afterPrefix = "Pool ";
            if (idleTimeout > 0L && config.getMinimumIdle()                logPoolState("Before cleanup ");
               afterPrefix = "After cleanup  ";
               final List notInUse = connectionBag.values(STATE_NOT_IN_USE);
               int removed = 0;
               for (PoolEntry entry : notInUse) {
                  if (elapsedMillis(entry.lastAccessed, now) > idleTimeout && connectionBag.reserve(entry)) {
                     closeConnection(entry, "(connection has passed idleTimeout)");
                     if (++removed > config.getMinimumIdle()) {
                        break;
                     }
                  }
               }
            }
            logPoolState(afterPrefix);
            fillPool(); // Try to maintain minimum connections
         }
         catch (Exception e) {
            LOGGER.error("Unexpected exception in housekeeping task", e);
         }
      }
   }

Read

getConnection

在com.zaxxer.hikari.pool.HikariPool的核心方法getConnection中用到了validationTimeout,我們看一下原始碼,borrow到poolEntry之後,如果不是isMarkedEvicted,則會呼叫isConnectionAlive來判斷連接的有效性,再強調一下hikari是在borrow連接的時候校驗連接的有效性

/**
    * Get a connection from the pool, or timeout after the specified number of milliseconds.
    *
    * @param hardTimeout the maximum time to wait for a connection from the pool
    * @return a java.sql.Connection instance
    * @throws SQLException thrown if a timeout occurs trying to obtain a connection
    */

   public Connection getConnection(final long hardTimeout) throws SQLException
   
{
      suspendResumeLock.acquire();
      final long startTime = currentTime();
      try {
         long timeout = hardTimeout;
         PoolEntry poolEntry = null;
         try {
            do {
               poolEntry = connectionBag.borrow(timeout, MILLISECONDS);
               if (poolEntry == null) {
                  break// We timed out... break and throw exception
               }
               final long now = currentTime();
               if (poolEntry.isMarkedEvicted() || (elapsedMillis(poolEntry.lastAccessed, now) > ALIVE_BYPASS_WINDOW_MS && !isConnectionAlive(poolEntry.connection))) {
                  closeConnection(poolEntry, "(connection is evicted or dead)"); // Throw away the dead connection (passed max age or failed alive test)
                  timeout = hardTimeout - elapsedMillis(startTime);
               }
               else {
                  metricsTracker.recordBorrowStats(poolEntry, startTime);
                  return poolEntry.createProxyConnection(leakTask.schedule(poolEntry), now);
               }
            } while (timeout > 0L);
            metricsTracker.recordBorrowTimeoutStats(startTime);
         }
         catch (InterruptedException e) {
            if (poolEntry != null) {
               poolEntry.recycle(startTime);
            }
            Thread.currentThread().interrupt();
            throw new SQLException(poolName + " - Interrupted during connection acquisition", e);
         }
      }
      finally {
         suspendResumeLock.release();
      }
      throw createTimeoutException(startTime);
   }

我們具體來看一下isConnectionAlive的實現:

   boolean isConnectionAlive(final Connection connection)
   
{
      try {
         try {
            setNetworkTimeout(connection, validationTimeout);
            final int validationSeconds = (int) Math.max(1000L, validationTimeout) / 1000;
            if (isUseJdbc4Validation) {
               return connection.isValid(validationSeconds);
            }
            try (Statement statement = connection.createStatement()) {
               if (isNetworkTimeoutSupported != TRUE) {
                  setQueryTimeout(statement, validationSeconds);
               }
               statement.execute(config.getConnectionTestQuery());
            }
         }
         finally {
            setNetworkTimeout(connection, networkTimeout);
            if (isIsolateInternalQueries && !isAutoCommit) {
               connection.rollback();
            }
         }
         return true;
      }
      catch (Exception e) {
         lastConnectionFailure.set(e);
         LOGGER.warn("{} - Failed to validate connection {} ({})", poolName, connection, e.getMessage());
         return false;
      }
   }
   /**
    * Set the network timeout, if isUseNetworkTimeout is true and the
    * driver supports it.
    *
    * @param connection the connection to set the network timeout on
    * @param timeoutMs the number of milliseconds before timeout
    * @throws SQLException throw if the connection.setNetworkTimeout() call throws
    */

   private void setNetworkTimeout(final Connection connection, final long timeoutMs) throws SQLException
   
{
      if (isNetworkTimeoutSupported == TRUE) {
         connection.setNetworkTimeout(netTimeoutExecutor, (int) timeoutMs);
      }
   }
/**
    * Set the query timeout, if it is supported by the driver.
    *
    * @param statement a statement to set the query timeout on
    * @param timeoutSec the number of seconds before timeout
    */

   private void setQueryTimeout(final Statement statement, final int timeoutSec)
   
{
      if (isQueryTimeoutSupported != FALSE) {
         try {
            statement.setQueryTimeout(timeoutSec);
            isQueryTimeoutSupported = TRUE;
         }
         catch (Throwable e) {
            if (isQueryTimeoutSupported == UNINITIALIZED) {
               isQueryTimeoutSupported = FALSE;
               LOGGER.info("{} - Failed to set query timeout for statement. ({})", poolName, e.getMessage());
            }
         }
      }
   }

從如下代碼可以看到,validationTimeout的預設值是5000毫秒,所以預設情況下validationSeconds的值應該在1-5毫秒之間,又由於validationTimeout的值必須小於connectionTimeout(預設值30000毫秒,如果小於250毫秒,則被重置回30秒),所以預設情況下,調整validationTimeout卻不調整connectionTimeout情況下,validationSeconds的預設峰值應該是30毫秒。

final int validationSeconds = (int) Math.max(1000L, validationTimeout) / 1000;

如果是jdbc4的話,如果使用isUseJdbc4Validation(就是config.getConnectionTestQuery() == null的時候)

this.isUseJdbc4Validation = config.getConnectionTestQuery() == null;

用connection.isValid(validationSeconds)來驗證連接的有效性,否則的話則用connectionTestQuery查詢陳述句來查詢驗證。這裡說一下java.sql.Connection的isValid()和isClosed()的區別:

isValid:如果連接尚未關閉並且仍然有效,則傳回 true。驅動程式將提交一個關於該連接的查詢,或者使用其他某種能確切驗證在呼叫此方法時連接是否仍然有效的機制。由驅動程式提交的用來驗證該連接的查詢將在當前事務的背景關係中執行。
引數:timeout – 等待用來驗證連接是否完成的資料庫操作的時間,以秒為單位。如果在操作完成之前超時期滿,則此方法傳回 false。0 值表示不對資料庫操作應用超時值。
傳回:如果連接有效,則傳回 true,否則傳回 false

isClosed:查詢此 Connection 物件是否已經被關閉。如果在連接上呼叫了 close 方法或者發生某些嚴重的錯誤,則連接被關閉。只有在呼叫了Connection.close 方法之後被呼叫時,此方法才保證傳回true。通常不能呼叫此方法確定到資料庫的連接是有效的還是無效的。通過捕獲在試圖進行某一操作時可能丟擲的異常,典型的客戶端可以確定某一連接是無效的。
傳回:如果此 Connection 物件是關閉的,則傳回 true;如果它仍然處於打開狀態,則傳回 false。

/**
         * Returns true if the connection has not been closed and is still valid.
         * The driver shall submit a query on the connection or use some other
         * mechanism that positively verifies the connection is still valid when
         * this method is called.
         * 


         * The query submitted by the driver to validate the connection shall be
         * executed in the context of the current transaction.
         *
         * @param timeout -             The time in seconds to wait for the database operation
         *                                              used to validate the connection to complete.  If
         *                                              the timeout period expires before the operation
         *                                              completes, this method returns false.  A value of
         *                                              0 indicates a timeout is not applied to the
         *                                              database operation.
         * 


         * @return true if the connection is valid, false otherwise
         * @exception SQLException if the value supplied for timeout
         * is less then 0
         * @since 1.6
         *
         * @see java.sql.DatabaseMetaData#getClientInfoProperties
         */


         boolean isValid(int timeout) throws SQLException;
             /**
     * Retrieves whether this Connection object has been
     * closed.  A connection is closed if the method close
     * has been called on it or if certain fatal errors have occurred.
     * This method is guaranteed to return true only when
     * it is called after the method Connection.close has
     * been called.
     * 


     * This method generally cannot be called to determine whether a
     * connection to a database is valid or invalid.  A typical client
     * can determine that a connection is invalid by catching any
     * exceptions that might be thrown when an operation is attempted.
     *
     * @return true if this Connection object
     *         is closed; false if it is still open
     * @exception SQLException if a database access error occurs
     */


    boolean isClosed() throws SQLException;
   public void acquire()
   
{
      acquisitionSemaphore.acquireUninterruptibly();
   }

newConnection

在com.zaxxer.hikari.pool.PoolBase的newConnection#setupConnection()中,對於validationTimeout超時時間也做了getAndSetNetworkTimeout等的處理

Hikari 2.7.5的故事

從validationTimeout我們剛纔講到了有一個HouseKeeper執行緒乾著不斷掃清的通過MBean調整的connectionTimeout和validationTimeout等值的事情。這就是2.7.4到2.7.5版本的一個很重要的改變,為什麼這麼說?

兩個關鍵的Mbean

首先Hikari有兩個Mbean,分別是HikariPoolMXBean和HikariConfigMXBean,我們看一下代碼,這兩個代碼的功能不言而喻:

/**
 * The javax.management MBean for a Hikari pool instance.
 *
 * @author Brett Wooldridge
 */

public interface HikariPoolMXBean
{
   int getIdleConnections();
   int getActiveConnections();
   int getTotalConnections();
   int getThreadsAwaitingConnection();
   void softEvictConnections();
   void suspendPool();
   void resumePool();
}
img

2.7.5迎來了不可變設計

作者在18年1月5日做了一次代碼提交:

img

導致大多數方法都不允許動態更新了:

img

可以這麼認為,2.7.4是支持的,2.7.5作者搞了一下就變成了不可變設計,sb2.0預設支持2.7.6。

這會帶來什麼影響呢?如果你想運行時動態更新Hikari的Config除非命中可修改引數,否則直接給你拋異常了;當然,你更新代碼寫得不好也可能命中作者的這段拋異常邏輯。作者非常推薦使用Mbean去修改,不過你自己重新創建一個資料源使用CAP(Compare And Swap)也是可行的,所以我就只能如下改了一下,順應了一下SB 2.0的時代:

img

如上圖,左側的欄位都是Hikari在2.7.5以前親測過可以動態更改的,不過jdbcurl不在這個範圍之內,所以這就是為什麼作者要做這麼一個比較安全的不可變樣式的導火索。

且看大神論道

某用戶在1.1日給作者提了一個issue,就是jdbcurl無法動態修改的事情:
https://github.com/brettwooldridge/HikariCP/issues/1053

img

作者予以了回覆,意思就是運行時可以更改的唯一池配置是通過HikariConfigMXBean,並增強的丟擲一個IllegalStateException異常。兩人達成一致,Makes sense,覺得非常Perfect,另外會完善一下JavaDoc。So,Sealed configuration makes it much harder to configure Hikari。

img

然後倆人又開了一個ISSUE:
https://github.com/brettwooldridge/HikariCP/issues/231
但是在這裡,倆人產生了一些設計相關的分歧,很有意思。

img
img

作者表明他的一些改變增加代碼的複雜性,而不是增加它的價值,而作者對於Hikari的初衷是追求極致性能、追求極簡設計。

img

該用戶建議作者提供add the ability to copy the configuration of one HikariDataSource into another的能力。作者予以了反駁:

img
img

作者還是一如既往得追求他大道至簡的思想以及兩個Mbean的主張。

該用戶繼續著他的觀點,

img
img

可是作者貌似還是很堅持他的Hikari觀點,作為吃瓜群眾,看著大神論道,還是非常有意思的。

最後說說我的觀點吧,我覺得作者對於Hikari,既然取名為光,就是追求極致,那些過度設計什麼的他都會儘量擯棄的,我使用Hikari以及閱讀原始碼的過程中也能感覺到,所以我覺得作者不會繼續做這個需求,後續請關註我的真情實感的從實戰及原始碼分析角度的體會《為什麼HikariCP這麼快》(不同於網上的其他文章)。

接下來說,我作為Hikari的使用者,我也是有能力完成Hikari的wrapper工作,我也可以去寫外層的HouseKeeper,所以我覺得這並不是什麼太大的問題,這次2.7.5的更新,很雞肋的一個功能,但是卻讓我,作為一名追光者,走近了作者一點,走近了Hikari一點 :)

666. 彩蛋



如果你對 Dubbo 感興趣,歡迎加入我的知識星球一起交流。

知識星球

目前在知識星球(https://t.zsxq.com/2VbiaEu)更新瞭如下 Dubbo 原始碼解析如下:

01. 除錯環境搭建
02. 專案結構一覽
03. 配置 Configuration
04. 核心流程一覽

05. 拓展機制 SPI

06. 執行緒池

07. 服務暴露 Export

08. 服務取用 Refer

09. 註冊中心 Registry

10. 動態編譯 Compile

11. 動態代理 Proxy

12. 服務呼叫 Invoke

13. 呼叫特性 

14. 過濾器 Filter

15. NIO 服務器

16. P2P 服務器

17. HTTP 服務器

18. 序列化 Serialization

19. 集群容錯 Cluster

20. 優雅停機

21. 日誌適配

22. 狀態檢查

23. 監控中心 Monitor

24. 管理中心 Admin

25. 運維命令 QOS

26. 鏈路追蹤 Tracing


一共 60 篇++

原始碼不易↓↓↓

點贊支持老艿艿↓↓

赞(0)

分享創造快樂