DAS性能優化之Tomcat數據源調優

背景

上一次爲了解決數據庫偶發連接高耗時的問題,我們將參數 minIdle 從0改爲1,在連接池中始終保留一個連接。然後爲了確保連接的有效性,又把參數 testOnBorrow 改爲true。成功解決偶發高延時問題。隨後,我們DAS團隊對Tomcat數據源做了更多的瞭解和研究,包括研讀它的源代碼,發現它仍舊有優化的空間。

我們上次調優主要是將 testOnBorrow 配置爲true。雖然保證了返回連接的有效性,但是這也意味着多了一次數據庫連接執行'SELECT 1'的檢查。我們進一步分析性能消耗時,發現Tomcat數據源並非每次都會做檢查,如果在最近一個時間段內( validationInterval 參數)這個連接已經被檢查過,那麼就不會再做檢查。雖然這個機制減小了檢查操作的機率,但還是存在做檢查可能性,不夠完美。畢竟作爲一款幾百個應用都使用的產品,哪怕一次細小的改進都會有巨大的 積累效應

怎麼樣才能在 testOnBorrow =false的配置下,最大程度保證返回連接的有效性呢?要達到這樣的效果,就需要對Tomcat數據源有更深入的理解。因此本文會從介紹Tomcat數據源的內部構造和原理入手來介紹這次優化工作。

Tomcat數據源的定位

Tomcat數據源是對JDBC DataSource的一個具體實現。根據官方對DataSource的解釋,DataSource除了提供數據庫連接Connect的功能(pooled connections)之外,它還能提供分佈式事務的支持(distributed transaction)。

在實際工作中,我們主要把它作爲connection pool來用,因此它本質是一個 緩存系統 ,它緩存對象的是數據庫連接Connect。

緩存系統的evict

對於一個緩存系統來說,它最 核心功能 是能夠自動剔除(evict)不再需要的緩存對象,否則它只是個簡單的map。所以如果你能搞清楚緩存的evict機制,那麼你對這個緩存系統就有了最核心的理解。那我們就來看看Tomcat數據源作爲緩存系統,它的evict機制是什麼樣的?

先來看evict的策略。一般evict機制有兩種策略:time based和size based。time based是根據緩存對象的過期時間來判斷,譬如規定存在超過1分鐘的對象就需要從緩存中被剔除。size based是一旦緩存對象數目超出閾值系統就開始evict,否則會產生內存或者資源的泄漏。Tomcat數據源同時採用了這種兩種策略。做evict的時候,既要判斷數據庫連接對象存在的時間,同時又要保證數據庫連接的總數保持在閾值之內。

那evict在什麼時機觸發呢?觸發evict可以有兩種做法:

一:在取get或者存put的時候,檢查緩存內容,剔除不再需要的緩存對象。有些本地緩存軟件就用這種做法,它比較簡單,不需要額外線程,但是存取的操作需要花費額外檢查的邏輯。

二:後臺啓動一個檢查緩存的線程,定期檢查剔除不再需要的緩存對象。Tomcat數據源使用的是第二種做法。

Tomcat數據源數據結構

我們再結合Tomcat數據源內部基本的數據結構來看evict機制。

Tomcat數據源內部數據結構大致會分爲兩個集合:idle集合和busy集合,兩個集合裏放的都是數據庫連接。Idle集合就是一個緩存集合,專門存放未被被應用使用的數據庫連接,而busy集合指的是被應用正在使用的連接。一般情況下,當應用從數據源獲取數據庫連接的時候,一個數據庫連接會從idle集合中去獲取,然後進入busy集合。當使用結束後再從busy集合回到idle。

數據庫連接就在這兩個集合之間移動。連接從idle集合移到busy集合叫做borrow,會觸發 testOnBorrow 的事件;反之,連接從busy集合移到idle集合叫做return,會觸發 testOnReturn 的事件。用戶可以利用這兩個事件,對連接的有效性做檢查,將失效連接剔除出數據源。

一圖勝千言:

瞭解了靜態數據結構之外,很重要的是理解它後臺線程做的工作。這個線程的名字是 PoolCleaner ,從這個命名也能猜出它的功能。它會定期檢查idle集合,剔除超過那些時間超出 minEvictableIdleTimeMillis ,以及通不過 testWhileIdle 檢測的連接。

對Tomcat數據源的內部基本結構有了瞭解之後,我們可以看一下這一次我們具體的優化點。

  • 將 testOnBorrow 改爲false: 
    這個修改是這次優化主要的重點。testOnBorrow事件發生在數據庫連接從idle集合移動到busy集合過程中,也就是準備嚮應用提供數據庫連接的時候。文章開頭提到過,如果是true的話,testOnBorrow事件有可能會觸發做一次正真的‘SELECT 1’檢查動作。經過測試,在極端情況下這個檢查動作會產生大約10%的額外時間開銷。當然,由於之前提到的 validationInterval 參數的作用,實際上的額外開銷會小得多。 
    回到我們的問題:既想把 testOnBorrow 設置爲false,又要最大程度保證返回連接的有效性。而問題的答案就是需要其他參數的配合。
  • 減小 maxAge : 
    這是Tomcat數據源獨有一個配置參數。這裏的age指的就是一個數據庫連接從連接開始到目前時間點的時間長度(now - time when connected)。這個參數的設定可以有效防止數據庫連接的生命週期過長導致失效。當一個數據庫連接從idle集合borrow到busy集合,或者從busy集合return回idle集合的時候,它的age會被檢查。如果這個age大於設定的 maxAge 值,那這個連接就會被拋棄。我們將這個參數從默認的7個多小時減小到15分鐘。減小 maxAge 可以有效提高連接池返回有效連接的概率。 
    注意,這個參數在Tomcat數據源不同版本有不同的定義。在7.x的版本中 maxAge 只會在return的時候被檢查,在8.x之後, maxAge 會在return和borrow的時候都檢查。
  • 將 testWhileIdle 改爲true: 
    testOnBorrow =false的副作用就是不能保證返回的數據庫連接的有效性,怎麼辦?Tomcat數據源還提供了另外一個test檢查的配置,叫做 testWhileIdle 。這個test同樣會用SELECT 1查詢的方式來檢查idle集合裏的緩存的數據庫連接的有效性,通不過test的連接會從idle集合裏被剔除。這個動作就是之前提到的那個PoolCleaner線程做的事情。有了 testWhileIdle 的檢查既能提高數據庫連接返回的有效性,又不會影響取連接的性能。
  • 減小 minEvictableIdleTimeMillis : 
    這個參數決定了呆在idle集合裏數據庫連接的時間長度,也就是evict策略中的time based策略。我們將這個時間長度從10分鐘減小到了30秒。減小了緩存連接的時間,就是提高了剩餘連接的有效性。

驗證與上線

和之前的流程一樣,我們先在本地開發環境做了充分的測試,然後上測試環境觀察了一週左右。最後上預發和生產環境觀察連續觀察幾天,沒有發生任何問題,優化成功。

我們這兩次的優化結果都是成功的,但是從另外的角度來看本質又有不同:上一次是被動的用戶問題驅動的優化,這一次是主動技術優化。主動技術優化,我們也是出於兩方面的考量:

首先,從技術上來看,雖然這次做的是比較小的優化,但是作爲一個服務於幾百個應用的組件來說,它的積累效應會被放大。這也是我們中間件團隊有別於其他技術團隊的一個特點。中間件產品的一個細小的優化,爲公司帶來的可能就是成千上萬個服務器上的CPU,內存和網絡開銷的節省。常言道 “勿以善小而不爲”,對於我們中間件團隊來說就是“ 勿以優化小而不爲 ”。其次,從人的學習的規律來看,當第一次進入一個領域研究學習之後,稱熱打鐵也更有利於知識的深度學習和積累,學無止境。這些就是我們再次做優化的動力!

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章