連線數從異常到 300 到 5(RDS MySQL 的一個大坑•後記)

技術譯民發表於2020-12-09

《記 RDS MySQL 的一個大坑》 中,我提到遇到 User juxxxxxxxxxx already has more than 'max_user_connections' active connections…… 這樣的錯誤,最終通過在迴圈中使用 Thread.Sleep,降低 CRUD 操作的頻率,讓連線數下降至不到原來的一半,從而解決了這個棘手的問題,有興趣的朋友可以點選連結回顧一下

今天又看了一下新增 Thread.Sleep 後,程式執行時的 IOPS 和 連線數:

iops-connections-3

執行結果:連線數:300,執行時間:68 分鐘,IOPS:7

昨天在部落格園中發出上篇文章後,熱心的朋友(@沈贇@不知道風往哪兒吹)對此問題提出了寶貴的意見和想法,激發了我對此問題繼續深究的決定。

下午經過幾個小時的分析和測試,終於找到了該問題的真正原因和更好的解決方法,在此做個補充。

真正的原因在於:使用 MySQL 官方提供的 MySql.Data 作為驅動程式連線 MySQL 資料庫的時候,預設使用了連線池,才引發了這個問題。錯怪了阿里雲(上篇中提到懷疑阿里雲改了 MySQL 底層做了限制),在此對自己的不嚴謹表示誠懇的道歉???,凡事要自己多思考多研究多求證,不可輕易懷疑權威的力量,切記切記!!!

下面聊一聊該問題出現的真正原因和更優的解決方法。

MySQL 連線池

根據官方介紹:MySQL Connector/NET 中(即 MySql.Data 中)連線池的工作的機制是,當客戶端配置 MySqlConnection 時,連線池通過保持一組與伺服器的本地連線使其處於活動狀態,隨後,如果開啟一個新的 MySqlConnection 物件,它將從連線池中建立連線,而不是重新建立一個新的本地連線。這樣便可以重用資料庫連線,避免了頻繁建立、釋放連線引起的大量效能開銷,這有助於縮短響應時間、統一管理、提高執行效能等等。

在軟體開發中,大多數情況下,資料庫連線都有複用的可能,即便永不復用,連線池也有自己的回收機制在適當的時候釋放資源,這有點像帶有過期時間的快取資料,也像 .NET 的 GC 回收機制。正因為在大多數情況下,它可以提高執行的效能,也有完善且可配置的回收機制。所以在沒有提供任何連線池選項的情況下,MySQL Connector/NET 預設啟用連線池,也就是說,建立 MySqlConnection 時使用下面的連線字串:

server=xxx;port=3306;userid=myuserid;password=pwd123;database=db125;charset=utf8;

等同於使用:

server=xxx;port=3306;userid=myuserid;password=pwd123;database=db125;charset=utf8;Pooling=true;

而我原來的程式碼中恰好用的就是前者,也就是說預設啟用了連線池。

為什麼使用連線池反而出問題了

上面說到連線池有那麼多的好處,為什麼我用了連線池反倒出問題了呢?我們來看一下

連線池對資源的利用情況:

官方文件:
Connector/NET runs a background job every three minutes and removes connections from pool that have been idle (unused) for more than three minutes. The pool cleanup frees resources on both client and server side. This is because on the client side every connection uses a socket, and on the server side every connection uses a socket and a thread.

譯文:
Connector/NET 每三分鐘執行一次後臺作業,從連線池中刪除閒置(未使用)超過三分鐘的連線。連線池清理會釋放客戶端和伺服器端的資源。這是因為在客戶端,每個連線使用一個套接字,而在伺服器端,每個連線都使用一個套接字和一個執行緒。

上一篇中有介紹過我的程式的基本情況,這裡有必要再補充一下關鍵的使用場景:

我們的 MySql 服務例項有很多臺,每臺例項上有很多個資料庫,只有其中一臺 MySql 服務例項出現了超出 max_user_connections 的異常,這臺例項最大的連線數限制在 600,但是這臺例項上的資料庫就有 700 多個。

聰明的朋友看到這裡,估計已經明白為什麼使用了連線池會出現問題了。為什麼呢?就因為上面提到的連線池每三分鐘執行一次清理操作唄。迴圈語句執行的速度是很快的,有的小庫瞬間就執行完了,但是在連線池中卻保持了一個連線,還沒有到每隔三分鐘的資源回收時間(這也是我在上篇中新增了 Thread.Sleep 後連線數減少的原因)。當這臺例項的 600 個連線被全部佔滿時,再連線同一例項上另一個連線池中沒有快取的資料庫時,就報了超出 max_user_connections 的異常。

解決方法

怎麼解決呢?最簡單的解決方法就是,判斷請求的是這臺 MySQL 服務例項時,不使用連線池,這樣就會在呼叫 MySqlConnection 的 Close 方法時,立即釋放客戶端和服務端所佔用的資源。因此,在資料庫連線字串中加上 Pooling=false,改成下面這樣:

server=xxx;port=3306;userid=myuserid;password=pwd123;database=db125;charset=utf8;Pooling=false;

然後釋出到伺服器上進行測試,檢視一下程式執行時的 IOPS 和 連線數:

pooling-false-result

驚不驚喜,意不意外,嚯嚯嚯~~~???

執行結果:連線數只有 5 個,執行時間縮短到了 8 分鐘,IOPS 為 36,與之前新增 Thread.Sleep 的測試結果相比,天壤之別呀……

最後,用一張圖來描述一下兩種解決方法的執行效果比較:

activity-diagram

結論

阿里雲 RDS MySQL 沒有問題,問題出在,在不恰當的場景使用了 MySQL 連線池,連線池雖好,但不可亂用喲,切記切記!

作者 : 技術譯民
出品 : 技術譯站

相關文章