記一次 Druid 超時設定的問題 → 引發對 Druid 時間設定項的探究

2022-07-11 12:02:19

開心一刻

  一天在路邊看到一個街頭採訪

  記者:請問,假如你兒子娶媳婦,給多少彩禮合適呢

  大爺:一百萬吧,再給一套房,一輛車

  大爺沉思一下,繼續說到:如果有能力的話再給老丈人配一輛車,畢竟他把女兒養這麼大也不容易

  記者:那你兒子多大了?

  大爺:我沒有兒子,有兩個女兒

  上圖的意思是:執行 select * from tbl_user 之前,需要從 druid 連線池中獲取一個 connect 

    而此時連線池的狀態是:一共 10 個啟用的 connect ,連線池最大建立 10 個 connect ,正在執行 sql 的 connect 也是 10 個

    所以不能建立新的 connect ,那就等唄,一共等了 1010 毫秒,還是拿不到 connect ,就丟擲 GetConnectionTimeoutException 異常

  簡單點說就是是連線池中連線數不夠,在規定的時間內拿不到 connect 

  那有人就說了:連線池的最大數量設定大一點,問題不就解決了嗎

  最大連線數設定大一點只能說可以降低問題發生的概率,不能完全杜絕,因為網路情況、硬體資源的使用情況等等都是不穩定因素

  今天要講的不是連線池大小問題,而是超時設定問題,我們慢慢往下看

問題復現

  我們先來模擬下上述問題

   MySQL 版本: 5.7.21 ,隔離級別:RR

   Druid 版本: 1.1.12 

   spring-jdbc 版本: 5.2.3.RELEASE 

  DruidDataSource 初始化

  為了方便演示,就手動初始化了

  多執行緒查詢

  執行緒數多於連線池中 connect 數

  模擬慢查詢

  如果查詢飛快,15 個查詢,可能都用不上 10 個 connect ,所以我們需要簡單處理下

  很簡單,給表加寫鎖唄: LOCK TABLES tbl_user WRITE 

  給表 tbl_user 加上寫鎖,然後跑執行緒去查詢 tbl_user 的資料

  異常演示

  先鎖表,再啟動程式

  可以看到,15 個執行緒中,有 5 個執行緒獲取 connect 失敗

Thread-13 Failed to obtain JDBC Connection; nested exception is com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 10004, active 10, maxActive 10, creating 0, runningSqlCount 10 : select * from tbl_user
Thread-5 Failed to obtain JDBC Connection; nested exception is com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 10004, active 10, maxActive 10, creating 0, runningSqlCount 10 : select * from tbl_user
Thread-10 Failed to obtain JDBC Connection; nested exception is com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 10004, active 10, maxActive 10, creating 0, runningSqlCount 10 : select * from tbl_user
Thread-7 Failed to obtain JDBC Connection; nested exception is com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 10004, active 10, maxActive 10, creating 0, runningSqlCount 10 : select * from tbl_user
Thread-8 Failed to obtain JDBC Connection; nested exception is com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 10004, active 10, maxActive 10, creating 0, runningSqlCount 10 : select * from tbl_user

  範例程式碼:druid-timeout

時間設定項

   Druid 中關於時間的設定項有很多,我們我們重點來看下如下幾個

  maxWait

  最大等待時長,單位是毫秒,-1 表示無限制

  從連線池獲取 connect ,如果有空閒的 connect ,則直接獲取到,如果沒有則最長等待 maxWait 毫秒,如果還獲取不到,則丟擲 GetConnectionTimeoutException 異常

  removeAbandonedTimeout

  設定 druid 強制回收連線的時限,單位是秒

  從連線池獲取到 connect 開始算起,超過此值後, Druid 將強制回收該連線

  官網也有說明:連線洩漏監測

  validationQueryTimeout

  檢測連線是否有效的超時時間,單位是秒,-1 表示無限制

   Druid 內部的一個檢測 connect 是否有效的超時時間,需要結合 validationQuery 來設定

  timeBetweenEvictionRunsMillis

  檢查空閒連線的頻率,單位是毫秒, 非正整數表示不進行檢查

  空閒連線檢查的間隔時間, Druid 池中的 connect 數量是一個動態從 minIdle 到 maxActive 擴張與收縮的過程

  connect 使用高峰期,數量會從 minIdle 擴張到 maxActive ,使用低峰期, connect 數量會從 maxActive 收縮到 minIdle 

  收縮的過程會回收一些空閒的 connect ,而 timeBetweenEvictionRunsMillis 就是檢查空閒連線的間隔時間

  queryTimeout

  執行查詢的超時時間,單位是秒,-1 表示無限制

  最終會應用到 Statement 物件上,執行時如果超過此時間,則丟擲 SQLException 

  transactionQueryTimeout

  執行一個事務的超時時間,單位是秒

  minEvictableIdleTimeMillis

  最小空閒時間,單位是毫秒,預設 30 分鐘

  如果連線池中非執行中的連線數大於 minIdle ,並且某些連線的非執行時間大於 minEvictableIdleTimeMillis ,則連線池會將這部分連線設定成 Idle 狀態並關閉

  maxEvictableIdleTimeMillis

  最大空閒時間,單位是毫秒,預設 7 小時

  如果 minIdle 設定的比較大,連線池中的空閒連線數一直沒有超過 minIdle ,那麼那些空閒連線是不是一直不用關閉?

  當然不是,如果連線太久沒用,資料庫也會把它關閉(MySQL 預設 8 小時),這時如果連線池不把這條連線關閉,程式就會拿到一條已經被資料庫關閉的連線

  為了避免這種情況, Druid 會判斷池中的連線,如果非執行時間大於 maxEvictableIdleTimeMillis ,也會強行把它關閉,而不用判斷空閒連線數是否小於 minIdle 

再看問題

  其實前面的範例中設定了

  獲取 connect 的最大等待時長是 10000 毫秒,也就是 10 秒

  而 removeAbandonedTimeout 設定是 7 秒

  照理來說 connect 如果 7 秒未執行完 SQL 查詢,就會被 Druid 強制回收進連線池,那麼等待 10 秒應該能夠獲取到 connect ,為什麼會丟擲 GetConnectionTimeoutException 異常了?

  這也就是文章標題中的超時設定問題

原始碼探究

  很顯然,我們從 dataSource.init(); 開始跟原始碼

  會看到如下一塊程式碼

  我們繼續跟 createAndStartDestroyThread(); 

  重點來了,我們看下 DestroyTask 到底是怎麼樣一個邏輯

  我們接著跟進 removeAbandoned ,關鍵程式碼

  如果 connect 正在執行中是不會被強制回收進連線池的

  回到我們的範例,connect 都是在執行中,只是都在進行慢查詢,所以是無法被強制回收進連線池的,那麼其他執行緒自然在 maxWait 時間內無法獲取到 connect 

  至此文章標題中的問題的原因就找到了

  那麼問題又來了: removeAbandonedTimeout 作用在哪?

  我們再仔細閱讀下:連線洩漏監測

   Druid 提供了 RemoveAbandanded 相關設定,目的是監測連線洩露,回收那些長時間遊離在連線池之外的空閒 connect 

  可能因為程式問題,導致申請的 connect 在處理完 sql 查詢後,不能回到連線池的懷抱,那麼這個 connect 處理遊離態,它真實存在,但後續誰也申請不到它,這就是連線洩露

  而 removeAbandoned 的設計就是為了幫助這些洩露的 connect 回到連線池的懷抱

解決問題

  開啟 removeAbandoned 對效能有影響,官方不建議在生產環境使用

  那麼我們接受官方的建議,不開啟 removeAbandoned (不設定即可,預設是關閉的)

  為了不讓慢查詢佔用整個連線池,而拖垮整個應用,我們設定查詢超時時間 queryTimeout 

  有兩種方式,一個是設定 DataSource 的 queryTimeout ,另一個是設定 JdbcTemplate 的 queryTimeout 

  如果兩個都設定,最終生效的是哪個,為什麼?大家自己去分析,權當是給大家留個一個作業

  這裡就設定 DataSource 的 queryTimeout ,給大家演示下效果

  可以看到,所有執行緒都獲取到了 connect 

總結

  1、 Druid 的 removeAbandoned 對效能有影響,不建議開啟

     removeAbandoned 的開啟後的作用要捋清楚,而非簡單的過期強制回收

  2、 Druid 的時間設定項有很多,不侷限於文中所講,但常用的就那麼幾個,其他的保持預設值就好

    設定的時候一定要弄清楚各個設定項的具體作業,不要去猜!

  3、查詢超時 queryTimeout 即可在 DataSource 設定,也可在 JdbcTemplate 設定