本文由 Jilen 發表在 ScalaCool 團隊部落格。
上一篇文章分析了 Mysql 非同步驅動的一些缺點,大部分已經在我們內部版本中修復了。
其中分割槽設計的連結池在實際使用過程中會產生一些非常嚴重的問題。
連線池中的鎖阻塞
前文中曾經提到 SingleThreadedAsyncObjectPool
這個單執行緒的連線池實現並不是完全非阻塞的,再多個執行緒請求連結情況下仍舊會產生鎖阻塞。
同時文章中也提到 Play!Framework 這樣的框架主執行緒數可以非常少,所以不用過分擔憂。
事實證明這是錯誤的,因為 PartitionedAsyncObjectPool
預設使用了 Executors.newCachedThreadPool
, 這就導致不論主執行緒數多少,高併發情況下會建立大量執行緒同時去獲取連結。
而 SingleThreadedAsyncObjectPool
使用了 Executors.newFixedThreadPool
,顯然這意味著每次入隊都會產生一個鎖阻塞,在系統併發非常高的情況下,這會極大加劇鎖競爭,一旦獲得鎖執行緒被中斷,則所有的執行緒都會處於
頻繁的執行緒切換
驅動中預設情況下,存在多個 ExecutionContext
,憑空增加了記憶體消耗和上下文切換
難以定位的記憶體洩漏
在實際使用過程中,我們經歷了執行一段時間後 JVM 瘋狂 FGC 的情況。
經分析發現存在連結洩漏,連線池存在大量未被回收的 MySQLConnection
物件,並且非常詭異的是我們無法定位到底是誰持有了這些未釋放的 Connection。
考慮到上述問題,我開始著手設計一個全新的連結池,名字就叫 NewPool
設計一個完全無鎖無阻塞的連線池
這種全新連線池實現主要依賴以下設計
- 使用兩個
ConcurrentLinkedQueue
儲存等待列表和空閒連結,全程不存在鎖 - 使用
Semaphore
保證連線數和佇列長度不超過限制
主要程式碼如下(部分)
val conns: ConcurrentLinkedQueue[Future[Connection]] = ...
val queue: ConcurrentLinkedQueue[Promise[Connection]] = ...
val createSemaphore: Semaphore = ...
val queueSemaphore: Semaphore = ...
def withConnection[A](f: Connection => Future[A]): Future[A] = {
val c = acquire()
c.flatMap { cc =>
f(cc).andThen { //此處可能需要 try catch 處理不按套路丟擲異常的情況
case _ => release(c)
}
}
}
private def acquire(): Future[Connection] = {
val conn = conns.poll()
if (conn != null) { //有空餘連結,則返回這個連結
reconnectIfDead(conn)
} else if (createSemaphore.tryAcquire()) { //連線數少於最大連結數,建立一個
createNew()
} else if (queueSemaphore.tryAcquire()) { //佇列未滿,入隊
val p = Promise[Connection]
enqueueTask(p)
p.future
} else { //返回佇列已滿
Future.failed(QueueIsFull)
}
}
private def release(c: Future[Connection]) = {
val wait = queue.poll()
if (wait == null) {
conns.offer(c)
} else {
wait.completeWith(c)
queueSemaphore.release()
}
}複製程式碼
Semaphore
的 trypAcquire 操作和 ConcurrentLinkedQueue
都不會產生鎖,確實做到了 Lock-Free。
效能測試
為了驗證上述猜測,我基於 scalameter 做了簡單的效能測試。結果如下
簡單查詢(SELECT 1)
新的方案(圖中藍色線條)對非常簡單的查詢,仍舊有 100% 左右的效能提升
簡單事務(SELECT + UPDATE)
執行 SQL 如下
for {
u <- c.sendQuery(s"SELECT * FROM user WHERE id = ${id}")
r <- c.sendQuery(s"UPDATE user SET remain = remain + 100 WHERE id = ${id}")
} yield r複製程式碼
可以看到新方案(圖中綠色線條)有非常大幅度提升