記阿里雲 RDS MySQL 的一個大坑
阿新 • • 發佈:2020-12-08
花了一個下午的時間,終於把一個阿里雲 RDS MySQL 的一個大坑填上了,解決方法令人匪夷所思!絕對會讓各位看官感到大吃一驚,阿里雲 RDS MySQL 居然有這樣 xx 的大坑!
## 問題
最近應業務的需求,加了一個定時統計的任務,其中的演算法很簡單,只是需要大量的 CRUD 操作。
由於業務簡單,且時效性要求不高,所以程式碼寫起來若行雲流水,一氣呵成,本地測試一遍通過。
沒料想,當部署到線上測試的時候,卻上演了現場翻車,真是讓人大跌眼鏡……
看了一下錯誤日誌,大致如下所示:
```yaml
ERROR [DAL.EvaluateDetails:403] GetCount [(null)] - GetCount Error :Authentication to host 'rdsxxxxxxxxxxxxxxxxx.mysql.rds.aliyuncs.com' for user 'juxxxxxxxxxx' using method 'mysql_native_password' failed with message: User juxxxxxxxxxx already has more than 'max_user_connections' active connections
MySql.Data.MySqlClient.MySqlException (0x80004005): Authentication to host 'rdsxxxxxxxxxxxxxxxxx.mysql.rds.aliyuncs.com' for user 'juxxxxxxxxxx' using method 'mysql_native_password' failed with message: User juxxxxxxxxxx already has more than 'max_user_connections' active connections ---> MySql.Data.MySqlClient.MySqlException (0x80004005): User juxxxxxxxxxx already has more than 'max_user_connections' active connections
在 MySql.Data.MySqlClient.MySqlStream.ReadPacket()
在 MySql.Data.MySqlClient.Authentication.MySqlAuthenticationPlugin.ReadPacket()
在 MySql.Data.MySqlClient.Authentication.MySqlAuthenticationPlugin.AuthenticationFailed(Exception ex)
在 MySql.Data.MySqlClient.Authentication.MySqlAuthenticationPlugin.ReadPacket()
在 MySql.Data.MySqlClient.Authentication.MySqlAuthenticationPlugin.Authenticate(Boolean reset)
在 MySql.Data.MySqlClient.NativeDriver.Open()
在 MySql.Data.MySqlClient.Driver.Open()
在 MySql.Data.MySqlClient.Driver.Create(MySqlConnectionStringBuilder settings)
在 MySql.Data.MySqlClient.MySqlPool.GetPooledConnection()
在 MySql.Data.MySqlClient.MySqlPool.TryToGetDriver()
在 MySql.Data.MySqlClient.MySqlPool.GetConnection()
在 MySql.Data.MySqlClient.MySqlConnection.Open()
在 Utility.MySqlDbHelper.PrepareCommand(MySqlCommand cmd, MySqlConnection conn, MySqlTransaction trans, CommandType cmdType, String cmdText, MySqlParameter[] cmdParms) 位置 D:\Work\git\Utility\MySqlDbHelper.cs:行號 322
在 Utility.MySqlDbHelper.ExecuteReader(String connString, CommandType cmdType, String cmdText, MySqlParameter[] cmdParms) 位置 D:\Work\git\Utility\MySqlDbHelper.cs:行號 101
在 DAL.EvaluateDetails.GetCount(String connStr, Nullable`1 startDate, Nullable`1 endDate, Nullable`1 marketingType) 位置 D:\Work\git\DAL\EvaluateDetails.cs:行號 403
```
User juxxxxxxxxxx already has more than 'max_user_connections' active connections……
What?!
## 問題分析
以前從來沒有遇到過 *max_user_connections* 這樣的錯誤,倒是遇到過幾次 *max_connections*,**根據經驗,這種錯誤基本上都是使用連線後忘記關閉連線導致的。** 是的,我一開始就是這麼想的,儘管作為多年耕耘於一線的資深程式設計老鳥,對於寫的程式碼滿懷信心,認為不可能犯這麼低階的錯誤,但暫時想不到別的問題。於是,開始圍繞這個思路展開復查和求證……
### 基本情況
先簡單介紹一下程式的情況:C# 開發,基於 .NET Framework 4.5.2(嗯~ o(* ̄▽ ̄*)o,古老的執行框架,很多時候不得不這麼做,因為呼叫的類庫太多,且全基於這個框架,升級的成本太大); 資料庫訪問呼叫的是 MySQL 官方提供的 MySql.Data(Version=6.9.7.0, Runtime: v4.0.30319)。
![MySql.Data.dll Version](https://img2020.cnblogs.com/blog/2074831/202012/2074831-20201206213834158-297257695.png#center)
在阿里雲控制檯檢視一下這臺 MySQL Server 的配置情況:
![RDS MySQL Configuration](https://img2020.cnblogs.com/blog/2074831/202012/2074831-20201206213918654-1531383573.png#center)
資料庫中查詢一下連線數的配置情況:
```sql
SELECT @@max_user_connections, @@max_connections, @@wait_timeout, @@interactive_timeout;
```
查詢結果:
```csharp
| max_user_connections | max_connections | wait_timeout | interactive_timeout |
| -------------------- | --------------- | ------------ | ------------------- |
| 600 | 1112 | 7200 | 7200 |
```
![max connections query](https://img2020.cnblogs.com/blog/2074831/202012/2074831-20201206213958237-1351718669.png#center)
在控制檯檢視一下統計程式執行時的 IOPS 和 連線數:
![IOPS and Connections 1](https://img2020.cnblogs.com/blog/2074831/202012/2074831-20201206214025722-737438978.png#center)
資料庫的配置是 max_user_connections = 600,程式執行時,總連線數確實超過了這項配置,報異常的原因就是這個,那麼是什麼引起的呢?
### 問題排查
#### 1、檢查資料庫開啟後是否忘記關閉
程式實現的業務雖簡單,但資料庫的訪問和邏輯計算有太多了,大量的 CRUD 操作,使用到 MySqlCommand 的 ExecuteScalar 、ExecuteReader、ExecuteNonQuery 以及 MySqlDataAdapter 的 Fill 方法。一個一個看方法查下去,遺憾的是,發現所有的資料庫訪問之後都同步執行了 MySqlConnection 的 Close 方法。
雖然最先懷疑的就是這個原因,但事實證明並不是。除非 MySql.Data 內部在呼叫 Close 後實際上沒有立即 Close? 用 ILSpy 查看了原始碼,也沒有發現問題。
#### 2、檢查程式中的併發邏輯
另一個可以想到的原因就是,**併發** CRUD **太多?**
上面我說過,因為這個程式的時效性要求不高,為避免給資料庫服務帶來壓力,根本沒有用到併發處理。
那到底是什麼原因引起的呢?
#### 3、關於 max_user_connections 的思考
錯誤提示是 使用者 juxxxxxxxxxx 的活動連線數已超過 'max_user_connections',注意這裡提示的是***活動連線數***。
到官網檢視一下配置項 [max_user_connections](https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_max_user_connections) [^max_user] 和 [max_connections](https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_max_connections) [^max] 的解釋:
[^max_user]: max_user_connections
[^max]: max_connections
![max connections](https://img2020.cnblogs.com/blog/2074831/202012/2074831-20201206214101878-1702836451.png#center)
![max user connections](https://img2020.cnblogs.com/blog/2074831/202012/2074831-20201206214129809-1409845955.png#center)
`max_connections` 是允許的最大**併發**客戶端連線數,`max_user_connections` 是**給定使用者賬號**允許的最大**併發**連線數。注意它們都是***併發數***。
報錯日誌中的 *活動連線數* 正好是對應 MySQL 官方說的 *併發連線數* 。
問題是,**明明每次執行 CRUD 後都關閉了連線,而且程式是單執行緒執行的,為什麼活動連線數還是超出了 max_user_connections 的值 600 呢?**
難道……
難道說……
***難道說這裡的併發數指的每秒或者每分鐘的累計數???***
不可能啊,
官方的文件中沒有提到 per second 或者 per minute 啊!
Google 了一下,也查到不到類似的說法啊!
那是不是*阿里雲改了 MySQL 底層*,做了 per second 或者 per minute 的限制呢?
想不出別的原因了,死馬當活馬醫,試一下吧!
## 問題解決
本來就是單執行緒執行的程式,為了降低 CRUD 操作的頻率,只好在迴圈執行的邏輯中每次迴圈後新增 `Thread.Sleep` 等待。
將程式碼改成大概如下的樣子:
```csharp
// ...
for (DateTime dt = startDate; dt <= endDate; dt = dt.AddMonths(1))
{
foreach (var shopInfo in list)
{
StatisticOneStore(shopInfo, dt);
Thread.Sleep(1000); //第一處 Sleep
}
}
// ...
private void StatisticOneStore(ShopInfo shopInfo, DateTime statisticDate)
{
// 其他業務邏輯 ...
foreach (var item in normalList)
{
SaveToDb(shopInfo, item, MarketingType.Normal, dbMonth);
Thread.Sleep(50); //第二處 Sleep
}
// 其他業務邏輯 ...
foreach (var item in eventList)
{
SaveToDb(shopInfo, item, MarketingType.Event, dbMonth);
Thread.Sleep(50); //第三處 Sleep
}
//...
}
// ...
```
然後再重新發布到伺服器上進行測試,雖然執行的速度慢了一點兒,但執行完成後檢視執行日誌,驚喜的發現,沒有報錯了!
再在控制檯檢視一下程式執行時的 IOPS 和 連線數:
![IOPS and Connections 2](https://img2020.cnblogs.com/blog/2074831/202012/2074831-20201206214201554-1028918093.png#center)
連線數居然降至不到原來的一半了!!!
折騰了半天,最終居然只是加了個 `Sleep` 問題便解決了,實在是太出乎意料了!
大跌眼鏡,有沒有?!
好在程式沒有那麼高的時效性要求,不然只能升級 MySQL Server 的配置規格了。
## 總結
問題雖然是解決了,但是依然有個疑惑,MySQL 官方文件上明明說的是**併發連線數**限制,為什麼在阿里雲 RDS MySQL 中,卻感覺是限制了每個 MySQL 例項每秒或每分的累計連線數呢?
不知道有沒有別的朋友遇到過這樣的問題?
不管怎樣,問題解決了,聊以記之,以儆效尤。