主頁 > 資料庫 > MySQL 連接為什么掛死了

MySQL 連接為什么掛死了

2020-12-06 07:21:24 資料庫

宣告:本文為博主原創文章,由于已授權部分平臺發表該文章(知乎、云社區),可能造成發布時間方面的困擾,

一、背景

近期由測驗反饋的問題有點多,其中關于系統可靠性測驗提出的問題令人感到頭疼,一來這類問題有時候屬于“偶發”現象,難以在環境上快速復現;二來則是可靠性問題的定位鏈條有時候變得很長,極端情況下可能要從 A 服務追蹤到 Z 服務,或者是從應用代碼追溯到硬體層面,

本次分享的是一次關于 MySQL 高可用問題的定位程序,其中曲折頗多但問題本身卻比較有些代表性,遂將其記錄以供參考,

架構

首先,本系統以 MySQL 作為主要的資料存盤部件,整一個是典型的微服務架構(SpringBoot + SpringCloud),持久層則采用了如下幾個組件:

  • mybatis,實作 SQL <-> Method 的映射
  • hikaricp,實作資料庫連接池
  • mariadb-java-client,實作 JDBC 驅動

在 MySQL 服務端部分,后端采用了雙主架構,前端以 keepalived 結合浮動IP(VIP)做一層高可用,如下:

說明

  • MySQL 部署兩臺實體,設定為互為主備的關系,

  • 為每臺 MySQL 實體部署一個 keepalived 行程,由 keepalived 提供 VIP 高可用的故障切換,

    實際上,keepalived 和 MySQL 都實作了容器化,而 VIP 埠則映射到 VM 上的 nodePort 服務埠上,

  • 業務服務一律使用 VIP 進行資料庫訪問,

Keepalived 是基于 VRRP 協議實作了路由層轉換的,在同一時刻,VIP 只會指向其中的一個虛擬機(master),當主節點發生故障時,其他的 keepalived 會檢測到問題并重新選舉出新的 master,此后 VIP 將切換到另一個可用的 MySQL 實體節點上,這樣一來,MySQL 資料庫就擁有了基礎的高可用能力,

另外一點,Keepalived 還會對 MySQL 實體進行定時的健康檢查,一旦發現 MySQL 實體不可用會將自身行程殺死,進而再觸發 VIP 的切換動作,

問題現象

本次的測驗用例也是基于虛擬機故障的場景來設計的:

持續以較小的壓力向業務服務發起訪問,隨后將其中一臺 MySQL 的容器實體(master)重啟,
按照原有的評估,業務可能會產生很小的抖動,但其中斷時間應該保持在秒級,

然而經過多次的測驗后發現,在重啟 MySQL 主節點容器之后,有一定的概率會出現業務卻再也無法訪問的情況!

二、分析程序

在發生問題之后,開發同學的第一反應是 MySQL 的高可用機制出了問題,由于此前曾經出現過由于 keepalived 配置不當導致 VIP 未能及時切換的問題,因此對其已經有所戒備,

先是經過一通的排查,然后并沒有找到 keepalived 任何配置上的毛病,

然后在沒有辦法的情況下,重新測驗了幾次,問題又復現了,

緊接著,我們提出了幾個疑點:

  1. Keepalived 會根據 MySQL 實體的可達性進行判斷,會不會是健康檢查出了問題?

但在本次測驗場景中,MySQL 容器銷毀會導致 keepalived 的埠探測產生失敗,這同樣會導致 keepalived 失效,如果 keepalived 也發生了中止,那么 VIP 應該能自動發生搶占,而通過對比兩臺虛擬機節點的資訊后,發現 VIP 的確發生了切換,

  1. 業務行程所在的容器是否發生了網路不可達的問題?

    嘗試進入容器,對當前發生切換后的浮動IP、埠執行 telnet 測驗,發現仍然能訪問成功,

連接池排查

在排查前面兩個疑點之后,我們只能將目光轉向了業務服務的DB客戶端上,

從日志上看,在產生故障的時刻,業務側的確出現了一些例外,如下:

Unable to acquire JDBC Connection [n/a]
java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30000ms.
	at com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:669) ~[HikariCP-2.7.9.jar!/:?]
	at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:183) ~[HikariCP-2.7.9.jar!/:?] 
	...

這里提示的是業務操作獲取連接超時了(超過了30秒),那么,會不會是連接數不夠用呢?

業務接入采用的是 hikariCP 連接池,這也是市面上流行度很高的一款組件了,

我們隨即檢查了當前的連接池配置,如下:

//最小空閑連接數
spring.datasource.hikari.minimum-idle=10
//連接池最大大小
spring.datasource.hikari.maximum-pool-size=50
//連接最大空閑時長
spring.datasource.hikari.idle-timeout=60000
//連接生命時長
spring.datasource.hikari.max-lifetime=1800000
//獲取連接的超時時長
spring.datasource.hikari.connection-timeout=30000

其中 注意到 hikari 連接池配置了 minimum-idle = 10,也就是說,就算在沒有任何業務的情況下,連接池應該保證有 10 個連接,更何況當前的業務訪問量極低,不應該存在連接數不夠使用的情況,

除此之外,另外一種可能性則可能是出現了“僵尸連接”,也就是說在重啟的程序中,連接池一直沒有釋放這些不可用的連接,最終造成沒有可用連接的結果,

開發同學對"僵尸鏈接"的說法深信不疑,傾向性的認為這很可能是來自于 HikariCP 組件的某個 BUG..

于是開始走讀 HikariCP 的原始碼,發現應用層向連接池請求連接的一處代碼如下:

public class HikariPool{

   //獲取連接物件入口
   public Connection getConnection(final long hardTimeout) throws SQLException
   {
      suspendResumeLock.acquire();
      final long startTime = currentTime();

      try {
	     //使用預設的30s 超時時間
         long timeout = hardTimeout;
         do {
		    //進入回圈,在指定時間內獲取可用連接
			//從 connectionBag 中獲取連接
            PoolEntry 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) > aliveBypassWindowMs && !isConnectionAlive(poolEntry.connection))) {
               closeConnection(poolEntry, poolEntry.isMarkedEvicted() ? EVICTED_CONNECTION_MESSAGE : DEAD_CONNECTION_MESSAGE);
               timeout = hardTimeout - elapsedMillis(startTime);
            }
			//成功獲得連接物件
            else {
               metricsTracker.recordBorrowStats(poolEntry, startTime);
               return poolEntry.createProxyConnection(leakTaskFactory.schedule(poolEntry), now);
            }
         } while (timeout > 0L);

		 //超時了,拋出例外
         metricsTracker.recordBorrowTimeoutStats(startTime);
         throw createTimeoutException(startTime);
      }
      catch (InterruptedException e) {
         Thread.currentThread().interrupt();
         throw new SQLException(poolName + " - Interrupted during connection acquisition", e);
      }
      finally {
         suspendResumeLock.release();
      }
   }
}

getConnection() 方法展示了獲取連接的整個流程,其中 connectionBag 是用于存放連接物件的容器物件,如果從 connectionBag 獲得的連接不再滿足存活條件,那么會將其手動關閉,代碼如下:

   void closeConnection(final PoolEntry poolEntry, final String closureReason)
   {
      //移除連接物件
      if (connectionBag.remove(poolEntry)) {
         final Connection connection = poolEntry.close();
		 //異步關閉連接
         closeConnectionExecutor.execute(() -> {
            quietlyCloseConnection(connection, closureReason);
			//由于可用連接變少,將觸發填充連接池的任務
            if (poolState == POOL_NORMAL) {
               fillPool();
            }
         });
      }
   }

注意到,只有當連接滿足下面條件中的其中一個時,會被執行 close,

  • isMarkedEvicted() 的回傳結果是 true,即標記為清除

    如果連接存活時間超出最大生存時間(maxLifeTime),或者距離上一次使用超過了idleTimeout,會被定時任務標記為清除狀態,清除狀態的連接在獲取的時候才真正 close,

  • 500ms 內沒有被使用,且連接已經不再存活,即 isConnectionAlive() 回傳 false

由于我們把 idleTimeout 和 maxLifeTime 都設定得非常大,因此需重點檢查 isConnectionAlive 方法中的判斷,如下:

public class PoolBase{

   //判斷連接是否存活
   boolean isConnectionAlive(final Connection connection)
   {
      try {
         try {
		    //設定 JDBC 連接的執行超時
            setNetworkTimeout(connection, validationTimeout);

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

			//如果沒有設定 TestQuery,使用 JDBC4 的校驗介面
            if (isUseJdbc4Validation) {
               return connection.isValid(validationSeconds);
            }

			//使用 TestQuery(如 select 1)陳述句對連接進行探測
            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 {} ({}). Possibly consider using a shorter maxLifetime value.",
                     poolName, connection, e.getMessage());
         return false;
      }
   }

}

我們看到,在PoolBase.isConnectionAlive 方法中對連接執行了一系列的探測,如果發生例外還會將例外資訊記錄到當前的執行緒背景關系中,隨后,在 HikariPool 拋出例外時會將最后一次檢測失敗的例外也一同收集,如下:

private SQLException createTimeoutException(long startTime)
{
   logPoolState("Timeout failure ");
   metricsTracker.recordConnectionTimeout();

   String sqlState = null;
   //獲取最后一次連接失敗的例外
   final Throwable originalException = getLastConnectionFailure();
   if (originalException instanceof SQLException) {
      sqlState = ((SQLException) originalException).getSQLState();
   }
   //拋出例外
   final SQLException connectionException = new SQLTransientConnectionException(poolName + " - Connection is not available, request timed out after " + elapsedMillis(startTime) + "ms.", sqlState, originalException);
   if (originalException instanceof SQLException) {
      connectionException.setNextException((SQLException) originalException);
   }

   return connectionException;
}

這里的例外訊息和我們在業務服務中看到的例外日志基本上是吻合的,即除了超時產生的 "Connection is not available, request timed out after xxxms" 訊息之外,日志中還伴隨輸出了校驗失敗的資訊:

	Caused by: java.sql.SQLException: Connection.setNetworkTimeout cannot be called on a closed connection
	at org.mariadb.jdbc.internal.util.exceptions.ExceptionMapper.getSqlException(ExceptionMapper.java:211) ~[mariadb-java-client-2.2.6.jar!/:?]
	at org.mariadb.jdbc.MariaDbConnection.setNetworkTimeout(MariaDbConnection.java:1632) ~[mariadb-java-client-2.2.6.jar!/:?]
	at com.zaxxer.hikari.pool.PoolBase.setNetworkTimeout(PoolBase.java:541) ~[HikariCP-2.7.9.jar!/:?]
	at com.zaxxer.hikari.pool.PoolBase.isConnectionAlive(PoolBase.java:162) ~[HikariCP-2.7.9.jar!/:?]
	at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:172) ~[HikariCP-2.7.9.jar!/:?]
	at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:148) ~[HikariCP-2.7.9.jar!/:?]
	at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:128) ~[HikariCP-2.7.9.jar!/:?]

到這里,我們已經將應用獲得連接的代碼大致梳理了一遍,整個程序如下圖所示:

從執行邏輯上看,連接池的處理并沒有問題,相反其在許多細節上都考慮到位了,在對非存活連接執行 close 時,同樣呼叫了 removeFromBag 動作將其從連接池中移除,因此也不應該存在僵尸連接物件的問題,

那么,我們之前的推測應該就是錯誤的!

陷入焦灼

在代碼分析之余,開發同學也注意到當前使用的 hikariCP 版本為 3.4.5,而環境上出問題的業務服務卻是 2.7.9 版本,這仿佛預示著什么.. 讓我們再次假設 hikariCP 2.7.9 版本存在某種未知的 BUG,導致了問題的產生,

為了進一步分析連接池對于服務端故障的行為處理,我們嘗試在本地機器上進行模擬,這一次使用了 hikariCP 2.7.9 版本進行測驗,并同時將 hikariCP 的日志級別設定為 DEBUG,

模擬場景中,會由 由本地應用程式連接本機的 MySQL 資料庫進行操作,步驟如下:

1. 初始化資料源,此時連接池 min-idle 設定為 10;
2. 每隔50ms 執行一次SQL操作,查詢當前的元資料表;
3. 將 MySQL 服務停止一段時間,觀察業務表現;
4. 將 MySQL 服務重新啟動,觀察業務表現,

最終產生的日志如下:

//初始化程序,建立10個連接
DEBUG -HikariPool.logPoolState - Pool stats (total=1, active=1, idle=0, waiting=0)
DEBUG -HikariPool$PoolEntryCreator.call- Added connection MariaDbConnection@71ab7c09
DEBUG -HikariPool$PoolEntryCreator.call- Added connection MariaDbConnection@7f6c9c4c
DEBUG -HikariPool$PoolEntryCreator.call- Added connection MariaDbConnection@7b531779
...
DEBUG -HikariPool.logPoolState- After adding stats (total=10, active=1, idle=9, waiting=0)

//執行業務操作,成功
execute statement: true
test time -------1
execute statement: true
test time -------2

...
//停止MySQL
...
//檢測到無效連接
WARN  -PoolBase.isConnectionAlive - Failed to validate connection MariaDbConnection@9225652 ((conn=38652) 
Connection.setNetworkTimeout cannot be called on a closed connection). Possibly consider using a shorter maxLifetime value.
WARN  -PoolBase.isConnectionAlive - Failed to validate connection MariaDbConnection@71ab7c09 ((conn=38653) 
Connection.setNetworkTimeout cannot be called on a closed connection). Possibly consider using a shorter maxLifetime value.
//釋放連接
DEBUG -PoolBase.quietlyCloseConnection(PoolBase.java:134) - Closing connection MariaDbConnection@9225652: (connection is dead) 
DEBUG -PoolBase.quietlyCloseConnection(PoolBase.java:134) - Closing connection MariaDbConnection@71ab7c09: (connection is dead)

//嘗試創建連接失敗
DEBUG -HikariPool.createPoolEntry - Cannot acquire connection from data source
java.sql.SQLNonTransientConnectionException: Could not connect to address=(host=localhost)(port=3306)(type=master) : 
Socket fail to connect to host:localhost, port:3306. Connection refused: connect
Caused by: java.sql.SQLNonTransientConnectionException: Socket fail to connect to host:localhost, port:3306. Connection refused: connect
	at internal.util.exceptions.ExceptionFactory.createException(ExceptionFactory.java:73) ~[mariadb-java-client-2.6.0.jar:?]
	...

//持續失敗.. 直到MySQL重啟

//重啟后,自動創建連接成功
DEBUG -HikariPool$PoolEntryCreator.call -Added connection MariaDbConnection@42c5503e
DEBUG -HikariPool$PoolEntryCreator.call -Added connection MariaDbConnection@695a7435
//連接池狀態,重新建立10個連接
DEBUG -HikariPool.logPoolState(HikariPool.java:421) -After adding stats (total=10, active=1, idle=9, waiting=0)
//執行業務操作,成功(已經自愈)
execute statement: true

從日志上看,hikariCP 還是能成功檢測到壞死的連接并將其踢出連接池,一旦 MySQL 重新啟動,業務操作又能自動恢復成功了,根據這個結果,基于 hikariCP 版本問題的設想也再次落空,研發同學再次陷入焦灼,

撥開云霧見光明

多方面求證無果之后,我們最終嘗試在業務服務所在的容器內進行抓包,看是否能發現一些蛛絲馬跡,

進入故障容器,執行 tcpdump -i eth0 tcp port 30052 進行抓包,然后對業務介面發起訪問,

此時令人詭異的事情發生了,沒有任何網路包產生!而業務日志在 30s 之后也出現了獲取連接失敗的例外,

我們通過 netstat 命令檢查網路連接,發現只有一個 ESTABLISHED 狀態的 TCP 連接,

也就是說,當前業務實體和 MySQL 服務端是存在一個建好的連接的,但為什么業務還是報出可用連接呢?

推測可能原因有二:

  • 該連接被某個業務(如定時器)一直占用,
  • 該連接實際上還沒有辦法使用,可能處于某種僵死的狀態,

對于原因一,很快就可以被推翻,一來當前服務并沒有什么定時器任務,二來就算該連接被占用,按照連接池的原理,只要沒有達到上限,新的業務請求應該會促使連接池進行新連接的建立,那么無論是從 netstat 命令檢查還是 tcpdump 的結果來看,不應該一直是只有一個連接的狀況,

那么,情況二的可能性就很大了,帶著這個思路,繼續分析 Java 行程的執行緒堆疊,

執行 kill -3 pid 將執行緒堆疊輸出后分析,果不其然,在當前 thread stack 中發現了如下的條目:

"HikariPool-1 connection adder" #121 daemon prio=5 os_prio=0 tid=0x00007f1300021800 nid=0xad runnable [0x00007f12d82e5000]
   java.lang.Thread.State: RUNNABLE
	at java.net.SocketInputStream.socketRead0(Native Method)
	at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
	at java.net.SocketInputStream.read(SocketInputStream.java:171)
	at java.net.SocketInputStream.read(SocketInputStream.java:141)
	at java.io.FilterInputStream.read(FilterInputStream.java:133)
	at org.mariadb.jdbc.internal.io.input.ReadAheadBufferedStream.fillBuffer(ReadAheadBufferedStream.java:129)
	at org.mariadb.jdbc.internal.io.input.ReadAheadBufferedStream.read(ReadAheadBufferedStream.java:102)
	- locked <0x00000000d7f5b480> (a org.mariadb.jdbc.internal.io.input.ReadAheadBufferedStream)
	at org.mariadb.jdbc.internal.io.input.StandardPacketInputStream.getPacketArray(StandardPacketInputStream.java:241)
	at org.mariadb.jdbc.internal.io.input.StandardPacketInputStream.getPacket(StandardPacketInputStream.java:212)
	at org.mariadb.jdbc.internal.com.read.ReadInitialHandShakePacket.<init>(ReadInitialHandShakePacket.java:90)
	at org.mariadb.jdbc.internal.protocol.AbstractConnectProtocol.createConnection(AbstractConnectProtocol.java:480)
	at org.mariadb.jdbc.internal.protocol.AbstractConnectProtocol.connectWithoutProxy(AbstractConnectProtocol.java:1236)
	at org.mariadb.jdbc.internal.util.Utils.retrieveProxy(Utils.java:610)
	at org.mariadb.jdbc.MariaDbConnection.newConnection(MariaDbConnection.java:142)
	at org.mariadb.jdbc.Driver.connect(Driver.java:86)
	at com.zaxxer.hikari.util.DriverDataSource.getConnection(DriverDataSource.java:138)
	at com.zaxxer.hikari.pool.PoolBase.newConnection(PoolBase.java:358)
	at com.zaxxer.hikari.pool.PoolBase.newPoolEntry(PoolBase.java:206)
	at com.zaxxer.hikari.pool.HikariPool.createPoolEntry(HikariPool.java:477)

這里顯示 HikariPool-1 connection adder 這個執行緒一直處于 socketRead 的可執行狀態,從命名上看該執行緒應該是 HikariCP 連接池用于建立連接的任務執行緒,socket 讀操作則來自于 MariaDbConnection.newConnection() 這個方法,即 mariadb-java-client 驅動層建立 MySQL 連接的一個操作,其中 ReadInitialHandShakePacket 初始化則屬于 MySQL 建鏈協議中的一個環節,

簡而言之,上面的執行緒剛好處于建鏈的一個程序態,關于 mariadb 驅動和 MySQL 建鏈的程序大致如下:

MySQL 建鏈首先是建立 TCP 連接(三次握手),客戶端會讀取 MySQL 協議的一個初始化握手訊息包,內部包含 MySQL 版本號,鑒權演算法等等資訊,之后再進入身份鑒權的環節,

這里的問題就在于 ReadInitialHandShakePacket 初始化(讀取握手訊息包)一直處于 socket read 的一個狀態,

如果此時 MySQL 遠端主機故障了,那么該操作就會一直卡住,而此時的連接雖然已經建立(處于 ESTABLISHED 狀態),但卻一直沒能完成協議握手和后面的身份鑒權流程,即該連接只能算一個半成品(無法進入 hikariCP 連接池的串列中),從故障服務的 DEBUG 日志也可以看到,連接池持續是沒有可用連接的,如下:

DEBUG HikariPool.logPoolState --> Before cleanup stats (total=0, active=0, idle=0, waiting=3)

另一個需要解釋的問題則是,這樣一個 socket read 操作的阻塞是否就造成了整個連接池的阻塞呢?

經過代碼走讀,我們再次梳理了 hikariCP 建立連接的一個流程,其中涉及到幾個模塊:

  • HikariPool,連接池實體,由該物件連接的獲取、釋放以及連接的維護,
  • ConnectionBag,連接物件容器,存放當前的連接物件串列,用于提供可用連接,
  • AddConnectionExecutor,添加連接的執行器,命名如 "HikariPool-1 connection adder",是一個單執行緒的執行緒池,
  • PoolEntryCreator,添加連接的任務,實作創建連接的具體邏輯,
  • HouseKeeper,內部定時器,用于實作連接的超時淘汰、連接池的補充等作業,

HouseKeeper 在連接池初始化后的 100ms 觸發執行,其呼叫 fillPool() 方法完成連接池的填充,例如 min-idle 是10,那么初始化就會創建10個連接,ConnectionBag 維護了當前連接物件的串列,該模塊還維護了請求連接者(waiters)的一個計數器,用于評估當前連接數的需求,

其中,borrow 方法的邏輯如下:

 public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException
   {
      // 嘗試從 thread-local 中獲取
      final List<Object> list = threadList.get();
      for (int i = list.size() - 1; i >= 0; i--) {
         ...
      }

      // 計算當前等待請求的任務
      final int waiting = waiters.incrementAndGet();
      try {
         for (T bagEntry : sharedList) {
            if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
               //如果獲得了可用連接,會觸發填充任務
               if (waiting > 1) {
                  listener.addBagItem(waiting - 1);
               }
               return bagEntry;
            }
         }

		 //沒有可用連接,先觸發填充任務
         listener.addBagItem(waiting);

		 //在指定時間內等待可用連接進入
         timeout = timeUnit.toNanos(timeout);
         do {
            final long start = currentTime();
            final T bagEntry = handoffQueue.poll(timeout, NANOSECONDS);
            if (bagEntry == null || bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
               return bagEntry;
            }

            timeout -= elapsedNanos(start);
         } while (timeout > 10_000);

         return null;
      }
      finally {
         waiters.decrementAndGet();
      }
   }

注意到,無論是有沒有可用連接,該方法都會觸發一個 listener.addBagItem() 方法,HikariPool 對該介面的實作如下:

   public void addBagItem(final int waiting)
   {
      final boolean shouldAdd = waiting - addConnectionQueueReadOnlyView.size() >= 0; // Yes, >= is intentional.
      if (shouldAdd) {
         //呼叫 AddConnectionExecutor 提交創建連接的任務
         addConnectionExecutor.submit(poolEntryCreator);
      }
      else {
         logger.debug("{} - Add connection elided, waiting {}, queue {}", poolName, waiting, addConnectionQueueReadOnlyView.size());
      }
   }

PoolEntryCreator 則實作了創建連接的具體邏輯,如下:

public class PoolEntryCreator{
     @Override
      public Boolean call()
      {
         long sleepBackoff = 250L;
		 //判斷是否需要建立連接
         while (poolState == POOL_NORMAL && shouldCreateAnotherConnection()) {
		    //創建 MySQL 連接
            final PoolEntry poolEntry = createPoolEntry();
			
            if (poolEntry != null) {
			   //建立連接成功,直接回傳,
               connectionBag.add(poolEntry);
               logger.debug("{} - Added connection {}", poolName, poolEntry.connection);
               if (loggingPrefix != null) {
                  logPoolState(loggingPrefix);
               }
               return Boolean.TRUE;
            }
            ...
         }

         // Pool is suspended or shutdown or at max size
         return Boolean.FALSE;
      }
} 

由此可見,AddConnectionExecutor 采用了單執行緒的設計,當產生新連接需求時,會異步觸發 PoolEntryCreator 任務進行補充,其中 PoolEntryCreator. createPoolEntry() 會完成 MySQL 驅動連接建立的所有事情,而我們的情況則恰恰是 MySQL 建鏈程序產生了永久性阻塞,因此無論后面怎么獲取連接,新來的建鏈任務都會一直排隊等待,這便導致了業務上一直沒有連接可用,

下面這個圖說明了 hikariCP 的建鏈程序:

好了,讓我們在回顧一下前面關于可靠性測驗的場景:

首先,MySQL 主實體發生故障,而緊接著 hikariCP 則檢測到了壞的連接(connection is dead)并將其釋放,在釋放關閉連接的同時又發現連接數需要補充,進而立即觸發了新的建鏈請求,而問題就剛好出在這一次建鏈請求上,TCP 握手的部分是成功了(客戶端和 MySQL VM 上 nodePort 的完成連接),但在接下來由于當前的 MySQL 容器已經停止(此時 VIP 也切換到了另一臺 MySQL 實體上),因此客戶端再也無法獲得原 MySQL 實體的握手包回應(該握手屬于MySQL應用層的協議),此時便陷入了長時間的阻塞式 socketRead 操作,而建鏈請求任務恰恰好采用了單執行緒運作,進一步則導致了所有業務的阻塞,

三、解決方案

在了解了事情的來龍去脈之后,我們主要考慮從兩方面進行優化:

  • 優化一,增加 HirakiPool 中 AddConnectionExecutor 執行緒的數量,這樣即使第一個執行緒出現掛死,還有其他的執行緒能參與建鏈任務的分配,
  • 優化二,出問題的 socketRead 是一種同步阻塞式的呼叫,可通過 SO_TIMEOUT 來避免長時間掛死,

對于優化點一,我們一致認為用處并不大,如果連接出現了掛死那么相當于執行緒資源已經泄露,對服務后續的穩定運行十分不利,而且 hikariCP 在這里也已經將其寫死了,因此關鍵的方案還是避免阻塞式的呼叫,

查閱了 mariadb-java-client 官方檔案后,發現可以在 JDBC URL 中指定網路IO 的超時引數,如下:

Parameter Description
socketTimeout Defined the network socket timeout (SO_TIMEOUT) in milliseconds. Value of 0 disables this timeout.Default: 0 (standard configuration) or 10000ms (using "aurora" failover configuration). since 1.1.7

具體參考:https://mariadb.com/kb/en/about-mariadb-connector-j/

如描述所說的,socketTimeout 可以設定 socket 的 SO_TIMEOUT 屬性,從而達到控制超時時間的目的,默認是 0,即不超時,

我們在 MySQL JDBC URL 中加入了相關的引數,如下:

spring.datasource.url=jdbc:mysql://10.0.71.13:33052/appdb?socketTimeout=60000&connectTimeout=30000&serverTimezone=UTC

此后對 MySQL 可靠性場景進行多次驗證,發現連接掛死的現象已經不再出現,此時問題得到解決,

四、小結

本次分享了一次關于 MySQL 連接掛死問題排查的心路歷程,由于環境搭建的作業量巨大,而且該問題復現存在偶然性,整個分析程序還是有些坎坷的(其中也踩了坑),的確,我們很容易被一些表面的現象所迷惑,而覺得問題很難解決時,更容易帶著偏向性思維去處理問題,例如本例中曾一致認為連接池出現了問題,但實際上卻是由于 MySQL JDBC 驅動(mariadb driver)的一個不嚴謹的配置所導致,

從原則上講,應該避免一切可能導致資源掛死的行為,如果我們能在前期對代碼及相關配置做好充分的排查作業,相信 996 就會離我們越來越遠,

轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/230562.html

標籤:MySQL

上一篇:求問一下,我錯哪了,定義一個存盤程序 proc1,更新所有訂單(含稅折扣價)的總價,執行這個存盤程序。

下一篇:java連接MySQL時報錯,unable to find valid certification path to requested target

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • GPU虛擬機創建時間深度優化

    **?桔妹導讀:**GPU虛擬機實體創建速度慢是公有云面臨的普遍問題,由于通常情況下創建虛擬機屬于低頻操作而未引起業界的重視,實際生產中還是存在對GPU實體創建時間有苛刻要求的業務場景。本文將介紹滴滴云在解決該問題時的思路、方法、并展示最終的優化成果。 從公有云服務商那里購買過虛擬主機的資深用戶,一 ......

    uj5u.com 2020-09-10 06:09:13 more
  • 可編程網卡芯片在滴滴云網路的應用實踐

    **?桔妹導讀:**隨著云規模不斷擴大以及業務層面對延遲、帶寬的要求越來越高,采用DPDK 加速網路報文處理的方式在橫向縱向擴展都出現了局限性。可編程芯片成為業界熱點。本文主要講述了可編程網卡芯片在滴滴云網路中的應用實踐,遇到的問題、帶來的收益以及開源社區貢獻。 #1. 資料中心面臨的問題 隨著滴滴 ......

    uj5u.com 2020-09-10 06:10:21 more
  • 滴滴資料通道服務演進之路

    **?桔妹導讀:**滴滴資料通道引擎承載著全公司的資料同步,為下游實時和離線場景提供了必不可少的源資料。隨著任務量的不斷增加,資料通道的整體架構也隨之發生改變。本文介紹了滴滴資料通道的發展歷程,遇到的問題以及今后的規劃。 #1. 背景 資料,對于任何一家互聯網公司來說都是非常重要的資產,公司的大資料 ......

    uj5u.com 2020-09-10 06:11:05 more
  • 滴滴AI Labs斬獲國際機器翻譯大賽中譯英方向世界第三

    **桔妹導讀:**深耕人工智能領域,致力于探索AI讓出行更美好的滴滴AI Labs再次斬獲國際大獎,這次獲獎的專案是什么呢?一起來看看詳細報道吧! 近日,由國際計算語言學協會ACL(The Association for Computational Linguistics)舉辦的世界最具影響力的機器 ......

    uj5u.com 2020-09-10 06:11:29 more
  • MPP (Massively Parallel Processing)大規模并行處理

    1、什么是mpp? MPP (Massively Parallel Processing),即大規模并行處理,在資料庫非共享集群中,每個節點都有獨立的磁盤存盤系統和記憶體系統,業務資料根據資料庫模型和應用特點劃分到各個節點上,每臺資料節點通過專用網路或者商業通用網路互相連接,彼此協同計算,作為整體提供 ......

    uj5u.com 2020-09-10 06:11:41 more
  • 滴滴資料倉庫指標體系建設實踐

    **桔妹導讀:**指標體系是什么?如何使用OSM模型和AARRR模型搭建指標體系?如何統一流程、規范化、工具化管理指標體系?本文會對建設的方法論結合滴滴資料指標體系建設實踐進行解答分析。 #1. 什么是指標體系 ##1.1 指標體系定義 指標體系是將零散單點的具有相互聯系的指標,系統化的組織起來,通 ......

    uj5u.com 2020-09-10 06:12:52 more
  • 單表千萬行資料庫 LIKE 搜索優化手記

    我們經常在資料庫中使用 LIKE 運算子來完成對資料的模糊搜索,LIKE 運算子用于在 WHERE 子句中搜索列中的指定模式。 如果需要查找客戶表中所有姓氏是“張”的資料,可以使用下面的 SQL 陳述句: SELECT * FROM Customer WHERE Name LIKE '張%' 如果需要 ......

    uj5u.com 2020-09-10 06:13:25 more
  • 滴滴Ceph分布式存盤系統優化之鎖優化

    **桔妹導讀:**Ceph是國際知名的開源分布式存盤系統,在工業界和學術界都有著重要的影響。Ceph的架構和演算法設計發表在國際系統領域頂級會議OSDI、SOSP、SC等上。Ceph社區得到Red Hat、SUSE、Intel等大公司的大力支持。Ceph是國際云計算領域應用最廣泛的開源分布式存盤系統, ......

    uj5u.com 2020-09-10 06:14:51 more
  • es~通過ElasticsearchTemplate進行聚合~嵌套聚合

    之前寫過《es~通過ElasticsearchTemplate進行聚合操作》的文章,這一次主要寫一個嵌套的聚合,例如先對sex集合,再對desc聚合,最后再對age求和,共三層嵌套。 Aggregations的部分特性類似于SQL語言中的group by,avg,sum等函式,Aggregation ......

    uj5u.com 2020-09-10 06:14:59 more
  • 爬蟲日志監控 -- Elastc Stack(ELK)部署

    傻瓜式部署,只需替換IP與用戶 導讀: 現ELK四大組件分別為:Elasticsearch(核心)、logstash(處理)、filebeat(采集)、kibana(可視化) 下載均在https://www.elastic.co/cn/downloads/下tar包,各組件版本最好一致,配合fdm會 ......

    uj5u.com 2020-09-10 06:15:05 more
最新发布
  • day02-2-商鋪查詢快取

    功能02-商鋪查詢快取 3.商鋪詳情快取查詢 3.1什么是快取? 快取就是資料交換的緩沖區(稱作Cache),是存盤資料的臨時地方,一般讀寫性能較高。 快取的作用: 降低后端負載 提高讀寫效率,降低回應時間 快取的成本: 資料一致性成本 代碼維護成本 運維成本 3.2需求說明 如下,當我們點擊商店詳 ......

    uj5u.com 2023-04-20 08:33:24 more
  • MySQL中binlog備份腳本分享

    關于MySQL的二進制日志(binlog),我們都知道二進制日志(binlog)非常重要,尤其當你需要point to point災難恢復的時侯,所以我們要對其進行備份。關于二進制日志(binlog)的備份,可以基于flush logs方式先切換binlog,然后拷貝&壓縮到到遠程服務器或本地服務器 ......

    uj5u.com 2023-04-20 08:28:06 more
  • day02-短信登錄

    功能實作02 2.功能01-短信登錄 2.1基于Session實作登錄 2.1.1思路分析 2.1.2代碼實作 2.1.2.1發送短信驗證碼 發送短信驗證碼: 發送驗證碼的介面為:http://127.0.0.1:8080/api/user/code?phone=xxxxx<手機號> 請求方式:PO ......

    uj5u.com 2023-04-20 08:27:27 more
  • 快取與資料庫雙寫一致性幾種策略分析

    本文將對幾種快取與資料庫保證資料一致性的使用方式進行分析。為保證高并發性能,以下分析場景不考慮執行的原子性及加鎖等強一致性要求的場景,僅追求最終一致性。 ......

    uj5u.com 2023-04-20 08:26:48 more
  • sql陳述句優化

    問題查找及措施 問題查找 需要找到具體的代碼,對其進行一對一優化,而非一直把關注點放在服務器和sql平臺 降低簡化每個事務中處理的問題,盡量不要讓一個事務拖太長的時間 例如檔案上傳時,應將檔案上傳這一步放在事務外面 微軟建議 4.啟動sql定時執行計劃 怎么啟動sqlserver代理服務-百度經驗 ......

    uj5u.com 2023-04-20 08:26:35 more
  • 云時代,MySQL到ClickHouse資料同步產品對比推薦

    ClickHouse 在執行分析查詢時的速度優勢很好的彌補了MySQL的不足,但是對于很多開發者和DBA來說,如何將MySQL穩定、高效、簡單的同步到 ClickHouse 卻很困難。本文對比了 NineData、MaterializeMySQL(ClickHouse自帶)、Bifrost 三款產品... ......

    uj5u.com 2023-04-20 08:26:29 more
  • sql陳述句優化

    問題查找及措施 問題查找 需要找到具體的代碼,對其進行一對一優化,而非一直把關注點放在服務器和sql平臺 降低簡化每個事務中處理的問題,盡量不要讓一個事務拖太長的時間 例如檔案上傳時,應將檔案上傳這一步放在事務外面 微軟建議 4.啟動sql定時執行計劃 怎么啟動sqlserver代理服務-百度經驗 ......

    uj5u.com 2023-04-20 08:25:13 more
  • Redis 報”OutOfDirectMemoryError“(堆外記憶體溢位)

    Redis 報錯“OutOfDirectMemoryError(堆外記憶體溢位) ”問題如下: 一、報錯資訊: 使用 Redis 的業務介面 ,產生 OutOfDirectMemoryError(堆外記憶體溢位),如圖: 格式化后的報錯資訊: { "timestamp": "2023-04-17 22: ......

    uj5u.com 2023-04-20 08:24:54 more
  • day02-2-商鋪查詢快取

    功能02-商鋪查詢快取 3.商鋪詳情快取查詢 3.1什么是快取? 快取就是資料交換的緩沖區(稱作Cache),是存盤資料的臨時地方,一般讀寫性能較高。 快取的作用: 降低后端負載 提高讀寫效率,降低回應時間 快取的成本: 資料一致性成本 代碼維護成本 運維成本 3.2需求說明 如下,當我們點擊商店詳 ......

    uj5u.com 2023-04-20 08:24:03 more
  • day02-短信登錄

    功能實作02 2.功能01-短信登錄 2.1基于Session實作登錄 2.1.1思路分析 2.1.2代碼實作 2.1.2.1發送短信驗證碼 發送短信驗證碼: 發送驗證碼的介面為:http://127.0.0.1:8080/api/user/code?phone=xxxxx<手機號> 請求方式:PO ......

    uj5u.com 2023-04-20 08:23:11 more