MySQL 非同步驅動淺析 (一):效能分析

ScalaCool發表於2017-04-22

本文由 Jilen 發表在 ScalaCool 團隊部落格。

Mysql Async 是一個 Scala 編寫的,基於 Netty 實現的非阻塞非同步資料庫驅動。在本系列文章中我們將逐步分析:

  • 與傳統的 JDBC 驅動相比有何優勢
  • Mysql Async 非同步驅動存在什麼問題,該如何優化

專案設計目標

專案官網設計目標如下

  • 快、快、更快
  • 低記憶體開銷
  • 儘量避免記憶體拷貝(也是為了更快,更節約記憶體)
  • 易於使用,呼叫方法,返回 Future
  • 從不阻塞
  • 所有功能都被測試覆蓋
  • 很小的依賴

可以看出作者是希望通過非同步非阻塞能讓驅動更快(注意此處我們不討論是真非同步或者偽非同步)。
接下來本文將具體分析與傳統的 mysql-connector/j 相比究竟是不是更快,快在哪裡。

網路 IO

MysqlAsync 的 IO

  • 專案使用 Netty 的 NIO 來實現,在網路 IO 這一點上確實是非阻塞的。
  • 協議實現過程也沒用使用 synchronizedLock
  • Netty 預設情況下執行緒數為 CPU 核數2倍

Mysql JDBC 驅動 的 IO

mysql-connector/j 使用的還是 Blocking IO ,這要求處理請求時必需有足夠多的執行緒,否則吞吐量將受很大限制。

例如同樣基於 Blocking IO 的 Tomcat7 預設就配置了 200 執行緒。

連線池

MysqlAsync 的連結池

MySQL 非同步驅動淺析 (一):效能分析
Mysql Async Pool

專案還提供一個連線池,採用分割槽設計,一個 PartitionedAsyncObjectPool 包含多個 SingleThreadedAsyncObjectPool

PartitionedAsyncObjectPool

流程十分簡單,根據執行緒的 id 選擇 SingleThreadedAsyncObjectPool,然後從中獲取資料庫連結。不存在阻塞的可能

SingleThreadedAsyncObjectPool

顧名思義,這是一個單執行緒的物件池。當請求獲取連結時,如果有多餘連結則直接返回,如果沒有則加入佇列,等待有連結通過 giveBack 方法釋放時返回給佇列裡的某個請求。
這裡用了 Scala 的 FuturePromise 實現,也不存在阻塞的情況。

分析原始碼後發現此處使用只有一個執行緒的 ThreadPoolExecutor 來確保同一時間只有一個執行緒請求連結。


  // Worker.scala
  def action(f: => Unit) {
    this.executionContext.execute(new Runnable {
      def run() {
        ...
      }
    })
  }複製程式碼

上述程式碼中this.executionContext.execute 最終會執行 TreadPoolExecutor.execute
TreadPoolExecutor.execute 並不是完全非阻塞的。

這帶來了一個問題:當多個執行緒同時要獲取連結時,只有一個執行緒可以獲得連結,其他執行緒全部處於 blocked 狀態。

由於是分割槽設計,並且 Play 這樣的全非同步框架主執行緒數預設非常少,所以這個問題在某些場合下並不嚴重。

Hikaricp

HikariCP 也許是目前優化得最好 JDBC 連線池。
該專案 Wiki 中的幾篇文章也值得一看。

我們無法從理論上直接得出何者效能更優的答案,後續將通過具體測試來估計何者更優。

效能測試

為了驗證上述觀點,我進行了簡單的效能測試,主要測試了簡單查詢和事務兩個方面。

簡單查詢

SELECT 1複製程式碼

事務

update user set remain = remain + ? where id = ?
update user set remain = remain - ? where id = ?複製程式碼

簡單查詢(1000qps)

MysqlAsync (64連結,預設16執行緒)

MySQL 非同步驅動淺析 (一):效能分析
MysqlAsync-select

JDBC (64連結,64執行緒)

MySQL 非同步驅動淺析 (一):效能分析
Hikaricp-select

事務(1000tps,針對100條 user 記錄)

MysqlAsync (64連結,預設16執行緒)

MySQL 非同步驅動淺析 (一):效能分析
MysqlAsync-trans

JDBC (64連結,64執行緒)

MySQL 非同步驅動淺析 (一):效能分析
MysqlAsync-trans

結論

  • 在查詢非常簡單,速度很快的情況下兩者效能相當,Mysql Async 有微弱的優勢。
  • 在併發競爭更新,並且存在事務情況下(資料庫存在大量鎖):
    • 基於 Hikaricp 連線池的程式在一段時間後直接失去響應,大量請求超時。
    • 基於 MysqlAsync 的程式仍舊在執行,大部分失敗是因為事務中存在死鎖或者系統繁忙。
  • 通過調整連線數和執行緒數,hikaricp + mysql-connector/j 方案也許可以提升效能,但這套方案的問題是你永遠不知道多少執行緒和連結數才是合適的。

下表是結合上述測試和定性分析得出的結果

專案 MysqlAsync HikariCP + mysql-connector/j
程式設計模型 非同步 同步
網路IO NIO BIO
連結池 非同步實現 同步實現
過載防護 通過調節佇列長度實現 需要額外實現 (例如指定執行緒池任務佇列長度)
可伸縮性 只需要設定合理連線數(例如幾十個) 需要測試最佳執行緒數和連結數
執行緒數

總得來說 MysqlAsync 通過減少了執行緒數確實達到了以下效果

  • 更少記憶體佔用
  • 減少不必要等待,從而減少執行緒上下文切換
  • 與 Play 這樣的全非同步框架更契合,不用反覆除錯執行緒數量和連結數量

相關文章