本文由 Jilen 發表在 ScalaCool 團隊部落格。
Mysql Async 是一個 Scala 編寫的,基於 Netty 實現的非阻塞非同步資料庫驅動。在本系列文章中我們將逐步分析:
- 與傳統的 JDBC 驅動相比有何優勢
- Mysql Async 非同步驅動存在什麼問題,該如何優化
專案設計目標
專案官網設計目標如下
- 快、快、更快
- 低記憶體開銷
- 儘量避免記憶體拷貝(也是為了更快,更節約記憶體)
- 易於使用,呼叫方法,返回
Future
- 從不阻塞
- 所有功能都被測試覆蓋
- 很小的依賴
可以看出作者是希望通過非同步非阻塞能讓驅動更快(注意此處我們不討論是真非同步或者偽非同步)。
接下來本文將具體分析與傳統的 mysql-connector/j
相比究竟是不是更快,快在哪裡。
網路 IO
MysqlAsync 的 IO
- 專案使用 Netty 的 NIO 來實現,在網路 IO 這一點上確實是非阻塞的。
- 協議實現過程也沒用使用
synchronized
和Lock
- Netty 預設情況下執行緒數為 CPU 核數2倍
Mysql JDBC 驅動 的 IO
mysql-connector/j
使用的還是 Blocking IO ,這要求處理請求時必需有足夠多的執行緒,否則吞吐量將受很大限制。
例如同樣基於 Blocking IO 的 Tomcat7
預設就配置了 200 執行緒。
連線池
MysqlAsync 的連結池
專案還提供一個連線池,採用分割槽設計,一個 PartitionedAsyncObjectPool
包含多個 SingleThreadedAsyncObjectPool
。
PartitionedAsyncObjectPool
流程十分簡單,根據執行緒的 id 選擇 SingleThreadedAsyncObjectPool
,然後從中獲取資料庫連結。不存在阻塞的可能
SingleThreadedAsyncObjectPool
顧名思義,這是一個單執行緒的物件池。當請求獲取連結時,如果有多餘連結則直接返回,如果沒有則加入佇列,等待有連結通過 giveBack
方法釋放時返回給佇列裡的某個請求。
這裡用了 Scala 的 Future
和 Promise
實現,也不存在阻塞的情況。
分析原始碼後發現此處使用只有一個執行緒的 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執行緒)
JDBC (64連結,64執行緒)
事務(1000tps,針對100條 user 記錄)
MysqlAsync (64連結,預設16執行緒)
JDBC (64連結,64執行緒)
結論
- 在查詢非常簡單,速度很快的情況下兩者效能相當,
Mysql Async
有微弱的優勢。 - 在併發競爭更新,並且存在事務情況下(資料庫存在大量鎖):
- 基於 Hikaricp 連線池的程式在一段時間後直接失去響應,大量請求超時。
- 基於 MysqlAsync 的程式仍舊在執行,大部分失敗是因為事務中存在死鎖或者系統繁忙。
- 通過調整連線數和執行緒數,
hikaricp + mysql-connector/j
方案也許可以提升效能,但這套方案的問題是你永遠不知道多少執行緒和連結數才是合適的。
下表是結合上述測試和定性分析得出的結果
專案 | MysqlAsync | HikariCP + mysql-connector/j |
---|---|---|
程式設計模型 | 非同步 | 同步 |
網路IO | NIO | BIO |
連結池 | 非同步實現 | 同步實現 |
過載防護 | 通過調節佇列長度實現 | 需要額外實現 (例如指定執行緒池任務佇列長度) |
可伸縮性 | 只需要設定合理連線數(例如幾十個) | 需要測試最佳執行緒數和連結數 |
執行緒數 | 少 | 多 |
總得來說 MysqlAsync 通過減少了執行緒數確實達到了以下效果
- 更少記憶體佔用
- 減少不必要等待,從而減少執行緒上下文切換
- 與 Play 這樣的全非同步框架更契合,不用反覆除錯執行緒數量和連結數量