有贊DB連接池性能優化

很多系統的優化最後往往是對 DB 的優化,比如索引優化、併發控制,但如果提前劇透本次優化過程,其實最終只調整了一個bit,並且性能幾乎翻倍,猜測很多人會覺得這是標題黨在吊胃口,說實話劇情如此翻轉筆者也沒猜到。

背景

應用 T 的數據庫連接池使用了 druid 1.1.20 (https://github.com/alibaba/druid) ,在壓測時碰到 DB 的性能瓶頸,表現是單機 cpu 使用率上不去,增加數據庫連接數不會增加吞吐量,集羣最終吞吐量維持在 1w 左右,其中 T 應用對數據庫的主要操作是 select 和 insert。

排查

首先懷疑是否是 DB 的瓶頸,於是用 mysqlburst (https://github.com/xiezhenye/mysqlburst) 模擬核心請求,併發 500 下寫入(insert) 能達到 4w 左右,應該來說還有比較大的優化空間。不過應用 T 在碰到瓶頸後嘗試過擴容 2 臺服務器後性能沒提升,DB 的確是重點懷疑對象,於是在壓測期間抓包:

sudo tcpdump -i eth0 port $db_port -s 0 -w /tmp/t.pcap

拷貝 t.pcap 到本地用 wireshark 分析,圖1 是其中一條連接的請求詳情。

圖1. 到數據庫的請求

從上圖中看出響應的時間普遍在 1ms 以內,但是上次請求完成後到下次請求的時間間隔平均有 4~5ms,這是連接池最大配置 15 的結果,最大連接數調整爲 30 後發現請求後平均等待時間變爲 9ms 左右,這能解釋爲什麼連接池調整對性能沒什麼效果。

開始懷疑的是獲取連接後需要執行一些監控或者調用鏈的採集,導致沒有立即執行,於是打算用perf 工具查看一下性能,不過由於Java 的方法是jvm 維護的,所以需要先用工具 perf-map-agent 生成方法映射map。其實生成後對本次並沒有多大幫助,後來想了一下 perf 工具一般尋找cpu 瓶頸,但真實壓測cpu 其實水位只有 60% 左右。這裏就順帶介紹這個工具,並且作爲一個反面教材:性能問題分析需要先看看各項指標,分析瓶頸在哪,不能瞎碰運氣。

這個現象其實表明要麼是獲取連接後沒有立即查詢或者是還連接慢,於是用 arthas (https://github.com/alibaba/arthas) 統計返還連接的平均時間:

## 進入 arthas 後使用 monitor 命令查看方法的統計信息
monitor $class_name $method

圖2. 回收連接的耗時統計

連接池配置 30 時和抓包的結果非常吻合,中間有 9ms 左右的空閒連接說明出現在還連接上,歸還連接的等待比較要命,因爲不還回去連接當然其它線程也就獲取不了。接下來查找具體是哪裏慢:

圖3. 連接回收函數耗時詳情

從上圖中可見,主要耗時在其中的一次lock 操作,但是由於 recycle 方法中有多個鎖操作,具體是哪次鎖耗時這麼久還未定位到,於是繼續嘗試查看鎖調用的情況:

圖4. 鎖的耗時統計

觀察圖4 發現 lockInterruptibly 的 rt 明顯大於 lock,查看代碼發現 locakInterruptibly 調用主要集中在 druid 獲取連接中,所以基本上能確定慢的鎖就是 com.alibaba.druid.pool.DruidAbstractDataSource#lock 這個對象。雖然 druid 中一把鎖到處用性能應該會有影響,但這麼差的性能的確大跌眼鏡,第一時間還是覺得是不是哪裏鎖的時間太長,仔細分析了堆棧及業務日誌並沒有驗證自己的想法,不過還是有些新發現,堵住的連接使用的都是公平鎖,具體堆棧如下:

[email protected]
    at sun.misc.Unsafe.park(Native Method)
    -  waiting on [email protected]
    at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199)
    at java.util.concurrent.locks.ReentrantLock$FairSync.lock(ReentrantLock.java:224)
    at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)
    at com.alibaba.druid.pool.DruidDataSource.recycle(DruidDataSource.java:1913)
    at com.alibaba.druid.pool.DruidPooledConnection.recycle(DruidPooledConnection.java:324)
    at com.alibaba.druid.pool.DruidPooledConnection.syncClose(DruidPooledConnection.java:300)
    at com.alibaba.druid.pool.DruidPooledConnection.close(DruidPooledConnection.java:255)
    at org.springframework.jdbc.datasource.DataSourceUtils.doCloseConnection(DataSourceUtils.java:341)

優化

根據上面的觀察猜測可能是公平鎖影響性能,於是將改爲非公平鎖模式,其實 druid 默認配置爲非公平鎖,不過一旦設置了maxWait 之後就會使用公平鎖模式。

// 設置druid 連接池非公平鎖模式
dataSource.setUseUnfairLock(true);

設置爲公平鎖後結果讓人喫驚,簡單的測試在(300併發下,30個連接)在一臺機子上同時跑,非公平鎖能跑到 9k+,公平鎖只有5k 左右。然後小夥伴們立即修改 T 的代碼,發現單機提升不少,見圖5(其中前半部分是公平鎖,後半部分是非公平鎖,18:00 左右的下降是執行了 arthas 命令造成的額外性能損耗)。此時 cpu 已經跑到接近100%,說明本機 cpu 資源已充分使用。

圖5. 公平鎖與非公平鎖的性能對比

小結

最終在只修改一個參數的請求下,單機性能提升接近一倍,集羣的吞吐量也差不多提升 70%。不過公平鎖與非公平鎖有這麼大的性能差距還是比較震驚的,其實單機幾千請求量還真沒想到瓶頸會是在加鎖、釋放鎖這個過程,所以隱隱感覺還有更多的真相等待挖掘。

雖然最終一個小小的改動就達到了目的,其實整個優化過程中還是有些周折,並且是依靠小夥伴們的羣體智慧完成的。還有一個小插曲是順便調研了數據庫連接池 HikariCP(https://github.com/brettwooldridge/HikariCP),使用 HikariCP 替換後發現效果還是非常不錯,單機性能一下從 1.5k(druid 公平鎖) 提升到接近 3k。其實 HikariCP 的一個優勢就是快,當時都想要在公司推一波,不過要整個公司替換一遍也是不小的動作,雖然連接池使用上兩者十分接近,但是配套的監控要重新弄一遍還是比較勞民傷財的。還好最終測試發現大部分情況下 druid 還不至於成爲服務的瓶頸,而且配套的監控也比較全,如果真的追求更高的性能,HikariCP 是一個不錯的選擇。

本文轉載自公衆號有贊coder(ID:youzan_coder)。

原文鏈接

https://mp.weixin.qq.com/s?__biz=MzAxOTY5MDMxNA==&mid=2455761012&idx=1&sn=ff443dedabf6484bcc819194ae0b19e3&chksm=8c687651bb1fff47a30dde82fc2e7e3e9f046fdb1856d8edf6515dff49444805c79dac560dbe&scene=27#wechat_redirect

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