萬盛學電腦網

 萬盛學電腦網 >> 數據庫 >> mysql教程 >> mysql driver的bug的深入分析

mysql driver的bug的深入分析

mysql driver指的是數據庫連接驅動了,我們下面來為各位深入分析mysql driver的bug了,此文章非常的專題我們一起來看看吧。

1 問題描述:

下面是應用上線後的線程個數的曲線圖:

 

通過觀察線程數:從0.2k增加到1.4k,然後突然又變成0.2k。 所有增加的線程均為Damon的線程。

有如下幾個疑問:

   1:為什麼線程數會一直增加,出現了線程洩露?

   2:在某個時間點線程數突然降了1.2k,why?

1.1 線程洩露排查
   通過jstack打印棧信息,發現大部分的線程描述如下。(省略了部分棧信息)


"MySQL
 Statement Cancellation Timer"
#2952daemonin
 Object.wait()

java.lang.Thread.State: TIMED_WAITING (on object monitor)

at java.lang.Object.wait(Native Method)

  at
 java.util.TimerThread.run(Timer.java:505)
 
通過線程名字可知該線程為mysql的線程(這個也說明了自定義線程的時候,起一個容易識別的名字是多麼重要)。

從信息描述可知是JDK的一個Timer線程,並且該線程處於TIMED_WAITING的狀態。

疑問:

    Statement Cancellation Timer 線程是干嘛的,為什麼是一個Timer的線程?

1.2 線程數突降排查

首先去觀察當前系統的各種資源消耗(比如 cpu,內存,jvm信息),看能否從中得到一些信息,通過查看GC時發現,YGC的時間點和線程數突降的時間點完全一致。如下圖(GC的曲線圖)

疑問:

    難道YGC會把TIMED_WAITING狀態的線程給GC掉? 非常挑戰之前對GC的理解。


2 基礎知識
2.1 數據庫連接池的心跳檢測
 1:數據連接池是對連接進行管理和維護,保證連接的可用性。為了保證連接的可用性 ,需要對連接進行心跳。

 2:心跳一般分為兩個層面:tcp層面的心跳,應用層面的心跳。 tcp層面是使用keepalive進行心跳,而應用層面是通過應用層協議進行心跳。

 3:有同學會問:TCP有keepalive的心跳機制,為什麼還需要上層應用的心跳。(這裡筆者推薦使用應用層的心跳來進行保活)

keepalive的局限:

    保證TCP層面的保活,如果應用掛掉,但是端口還正常,則連接還是有效。

    對於IO應用,如果TCP層保活失敗,會關閉掉socket,但是上層應用感知不到,會認為socket還是有效的。

    操控性比較弱。

應用層保活優點:

     靈活,可操控性強。

     可以針對業務特性設置心跳策略。

     可以保證連接是有效的。

 4:數據庫連接池采用的是應用層心跳來進行保活,主要有如下三種心跳方式。

 Statement

     數據庫的查詢方式進行心跳。連接池會提供心跳檢查的sql配置,比如配置為:select 1 。那麼心跳的時候,就會執行statement.execute("select 1") 。

 Ping

      mysql driver提供了一個ping的方法。mysql協議提供了一個ping的命令(類似於select命令),專門用於心跳檢測。

 isValid

       jdbc提供了isValid接口,mysql driver對該接口實現,是采用mysql原生的ping命令。

 對比

  statement ping接口 isValid接口
參數 select 1 無參 有timeout參數
性能 低 高 高
接口提供 JDBC原生 mysql driver私有 JDBC4原生
返回值 boolean void boolean
心跳失敗 拋SQLException 拋SQLException 返回false
總結:

   注意:ping協議是指mysql的協議, ping接口是driver提供的接口內部用來實現ping協議的發送

    1:mysql原生ping協議的性能是statement的將近2倍(筆者進行過測試),推薦使用原生的ping協議

    2:ping接口並不是JDBC接口,內部實現了發送ping協議的功能。如果出錯,會將錯誤異常拋出

    3:isValid的內部實現也是ping方法,只是進行了try catch的封裝。如果try通過,則返回true,在catch處返回false,同時會將當前Connection的狀態置為false(該處後面會重點講解)。

2.2 業務超時處理方案
一般訪問第三方接口或者可能執行會比較長的接口,一般都會設置一個超時時間,進行容錯的處理。

比如:

    1:訪問http接口,設置超時時間,在規定的時間未返回數據,則會報timeout異常。

    2:執行數據庫命令,設置超時(調用statement的setQueryTimeout接口),在規定的時間未返回數據,則會報timeout異常。

2.2.1 實現超時控制流程:
    1:訪問接口前,將當前狀態封裝為一個定時任務,通過定時框架設定任務的啟動時間。

    2:收到返回數據後,則會取掉定時任務。

    3:如果在規定的時間內,數據還未返回,定時任務便會啟動。處理策略:(1)業務處理 (2)拋出timeout的異常。

    4:業務處理一般為:記錄日志,發送取消命令到服務端(mysql driver便是發送kill query connectionid到數據庫端取消執行的sql)。

2.2.2 定時框架 
定時框架JDK實現的主要有兩種可選擇:Timer和ScheduledExecutorService

(1) Timer 

主要有兩個方法:schedule和scheduleAtFixedRate,其中參數均為:(TimerTask task, long delay, long period) 。

區別: 比如period為5s,schedule是當前任務執行完成後再過5s開始執行下一次。scheduleAtFixedRate為任務間隔為固定的5s,包含任務執行的時間。

主要包含三個組件:

    TimerThread:用於運行task的線程

    TaskQueue:存放所有的task,該隊列會保證存放的task順序是按照執行時間先後存放

    TimerTask:運行的task,是一個Runnable,主要控制任務的狀態和執行的時間點。

 


上圖僅大概描述Timer的結構,邏輯並不是很嚴謹。

  1:調用schedule方法進行任務的調度。將任務添加到queue中,queue會對隊列進行重排,保證最先執行的任務在隊列的最前面。

  2:對task進行cancel時,僅僅是設置task的狀態為cancel,在thread線程讀取queue的時候,如果發現task狀態為cancel時,則從queue中移除掉。(很多取消操作均是這樣設計)

  3:timerThread異步讀取queue(任務是按照執行的先後進行順序存放),如果queue為empty,則wait。否則執行任務。

  4:調用Timer的cancel,則會設置一個標記位,TimerThread執行完成(即TimerThread線程的退出)。如果執行任務的時候出現異常,則TimerThread線程也會進行退出(使用Timer一定要注意任務的異常處理)

  5:對於?處的信息,後面會詳細描述。

 

 (2) ScheduledExecutorService

ScheduledExecutorService 具體原理本文將不做描述(其實定時實現的原理和Timer差不多)。

下面主要描述一下兩者的區別:

   1:線程模型:Timer是單線程,一個線程輪詢執行所有的任務。 ScheduledExecutorService是多線程,其實本質是一個Executor。

   2:異常檢查:Timer未對運行的任務進行異常檢查,如果任務出現異常,Timer線程會運行結束。ScheduledExecutorService會對異常捕獲處理

   如果需要設置定時任務,筆者這裡還是推薦ScheduledExecutorService 。

  

 2.3 Caelus連接池
唯品會平台架構部自研的分布式數據庫連接池。主要解決如下問題:

    1:高性能:基於無鎖的連接池設計模型來提升連接池性能;

    2:在分庫較多的場景下,減少線程數。 假如有128個分庫,現有連接池模型下則需要使用128個獨立的連接池,每個連接池都需要線程(1-4個,不同的連接池不同)處理任務。則總共需要維護128到128*4個線程,開銷巨大。而Caelus連接池會大大減少線程數。

    3:連接復用。 對於 一個mysql 的instance上面有多個schema場景下。現有連接池不同的schema的連接不可復用。而Caelus可以復用不同schema的連接,提升性能。

    4:過多的事務指令。如果是事務語句,則從連接池拿到連接後,需要先開啟事務(set autocommit=0),歸還時需要再設置(set autocommit=1)。每使用一次連接,均需要額外執行兩條事務指令。Caelus能有效減少事務指令。

    具體Caelus細節在這裡先不做描述。

3 線程洩露解析
    下面將解析線程不斷增加的原因。

3.1 問題描述
     1:使用JDBC接口時,如果設置queryTimeout,則driver會啟動一個Timer線程,來進行超時控制。Timer線程便是前面通過jstack抓取到一直在增加的線程:MySQL Statement Cancellation Timer 。

     2:可以推測應用端肯定設置了queryTimeout。 通過查看用戶配置,確定了在每個查詢的時候,均設置了queryTimeout。

     3:Timer線程是連接維度的。每新建一個連接,如果進行了超時控制,則會新建一個線程;在連接物理關閉時,再將該線程給close掉。(一般系統連接超時控制均是全局的設置,只需要開啟一個Timer線程運行超時任務)。mysql driver是每一個連接開啟一個Timer線程,效率較低,存在頻繁的開啟和關閉線程的操作。如果連接數過多,也會帶來線程數過多的問題。 此處可以考慮進行優化。

     4:連接物理關閉的方式為:調用Connection.close方法,該方法會先判斷當前Connection不是close狀態,不是close狀態,才會cancel掉Timer線程。

     既然連接關閉的時候,會將Timer線程給cancel掉,那為什麼Timer線程數一直在增長,並且所有的線程都處於等待狀態。下面先分析一下那些操作會對連接進行關閉。

3.2 連接關閉的場景
    1:連接池一般會設置minIdle,空閒連接超過minIdle的話,則會關閉連接。(關於連接池的配置可參考:http://blog.csdn.net/hetaohappy/article/details/51861015)

    2:連接池設置minEvictableIdleTimeMillis,即如果在規定的時間內一直空閒,則會關閉連接。

    3:如果訪問發現是IO異常,則會關閉掉連接。mysql會設定wait_timeout,也就是mysql對連接的空閒超時時間。如果空閒超時,mysql則會關閉掉連接。 由於訪問端是io機制,無法感知socket關閉,所以當拿到該socket進行訪問時,便會報IO的異常。

   前面兩個場景並未復現bug;第三個場景可以復現bug。 復現的流程為;

    1:新建連接,設置queryTimeout,然後訪問數據庫。

    2:在數據庫端把對應的連接給kill掉(類似空閒超時的處理)

    3: 客戶端使用該連接再進行訪問。發現多了一個Timer的線程。

3.3 原因分析
    執行業務sql之前,會先進行心跳檢查。心跳采用的是isValid接口(具體可參考前面描述) ,isValid方法先進行心跳,發現心跳失敗,會將Connection的連接狀態置為close。

    連接池發現心跳失敗,則會調用Connection的close接口,由於之前已經將Connection狀態設置為close 。close接口發現連接狀態已經是close,則直接返回。導致沒有cancel 掉Timer線程。

現有連接池排查:

連接池 是否有線程洩露
DBCP 有
Druid 無
HicariCP 有
Caelus 有
 有線程洩露的連接池,均是使用了isValid來進行心跳檢查。而Druid是采用的mysql driver的ping方法,所以沒有問題。這個可以更改為和Druid一樣的機制(ping方法)進行心跳檢查。

4 線程數突降解析
    在YGC的時候,線程數突然下降很多。理論上GC和線程是沒有關系,應該是YGC觸發了什麼條件,然後該條件將線程給關閉了。

4.1 Timer線程關閉場景
     1:調用Timer的cancel方法;

     2:任務運行異常(Timer未對異常做封裝處理)。

   通過排查確定YGC和上述兩個場景沒有關閉。

4.2 原因分析
    先分析一下GC和應用主要有哪些交互的場景:

        1:weak引用:GC後,會將weak引用進行回收掉;

        2:finalize:GC的時候會調用需要被GC對象的finalize 方法。

   通過分析Timer源碼,未發現有weak引用,卻發現Timer有一個 threadReaper 屬性實現了finalize 方法,該方法便是操作讓Timer線程運行結束(也就是關閉)。這裡也就清楚為什麼YGC的時候,線程數會跟著下降。(可見JDK源碼對各種異常情況考慮的確實很多,研究JDK源碼的設計思想,能給我們寫代碼帶來很大的借鑒)

    YGC導致線程關閉的流程:

        YGC-->回收Connection對象->回收Timer對象-->回收threadReaper 對象->執行threadReaper的finalize 方法->設定線程結束狀態,並進行notify->Timer線程運行結束。


5 總結

問題出現在兩個地方:

   1:mysql driver的isValid方法存在bug,導致Timer線程未關閉。

   2:連接池選擇了isValid方法進行心跳處理,可以考慮使用mysql driver的ping方法。

   該問題也是可以通過配置進行避免的。

copyright © 萬盛學電腦網 all rights reserved