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


《記 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 連接池,連接池雖好,但不可亂用喲,切記切記!

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM