記阿里雲 RDS MySQL 的一個大坑

花了一個下午的時間,終於把一個阿里雲 RDS MySQL 的一個大坑填上了,解決方法使人匪夷所思!絕對會讓各位看官感到大吃一驚,阿里雲 RDS MySQL 竟然有這樣 xx 的大坑!html

問題

最近應業務的需求,加了一個定時統計的任務,其中的算法很簡單,只是須要大量的 CRUD 操做。
因爲業務簡單,且時效性要求不高,因此代碼寫起來若行雲流水,一鼓作氣,本地測試一遍經過。
沒料想,當部署到線上測試的時候,卻上演了現場翻車,真是讓人大跌眼鏡……mysql

看了一下錯誤日誌,大體以下所示:git

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?!sql

問題分析

之前歷來沒有遇到過 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

在阿里雲控制檯查看一下這臺 MySQL Server 的配置狀況:服務器

RDS MySQL Configuration

數據庫中查詢一下鏈接數的配置狀況:併發

SELECT @@max_user_connections, @@max_connections, @@wait_timeout, @@interactive_timeout;

查詢結果:框架

| max_user_connections | max_connections | wait_timeout | interactive_timeout |
| -------------------- | --------------- | ------------ | ------------------- |
| 600                  | 1112            | 7200         | 7200                |

max connections query

在控制檯查看一下統計程序運行時的 IOPS 和 鏈接數:

IOPS and Connections 1

數據庫的配置是 max_user_connections = 600,程序運行時,總鏈接數確實超過了這項配置,報異常的緣由就是這個,那麼是什麼引發的呢?

問題排查

一、檢查數據庫打開後是否忘記關閉

程序實現的業務雖簡單,但數據庫的訪問和邏輯計算有太多了,大量的 CRUD 操做,使用到 MySqlCommand 的 ExecuteScalar 、ExecuteReader、ExecuteNonQuery 以及 MySqlDataAdapter 的 Fill 方法。一個一個看方法查下去,遺憾的是,發現全部的數據庫訪問以後都同步執行了 MySqlConnection 的 Close 方法。

雖然最早懷疑的就是這個緣由,但事實證實並非。除非 MySql.Data 內部在調用 Close 後實際上沒有當即 Close? 用 ILSpy 查看了源碼,也沒有發現問題。

二、檢查程序中的併發邏輯

另外一個能夠想到的緣由就是,併發 CRUD 太多?

上面我說過,由於這個程序的時效性要求不高,爲避免給數據庫服務帶來壓力,根本沒有用到併發處理。

那究竟是什麼緣由引發的呢?

三、關於 max_user_connections 的思考

錯誤提示是 用戶 juxxxxxxxxxx 的活動鏈接數已超過 'max_user_connections',注意這裏提示的是活動鏈接數

到官網查看一下配置項 max_user_connections [1]max_connections [2] 的解釋:

max connections

max user connections

max_connections 是容許的最大併發客戶端鏈接數,max_user_connections給定用戶帳號容許的最大併發鏈接數。注意它們都是併發數

報錯日誌中的 活動鏈接數 正好是對應 MySQL​ 官方說的 併發鏈接數

問題是,明明每次執行 CRUD 後都關閉了鏈接,並且程序是單線程運行的,爲何活動鏈接數仍是超出了 max_user_connections 的值 600 呢?

難道……

難道說……

難道說這裏的併發數指的每秒或者每分鐘的累計數???

不可能啊,

官方的文檔中沒有提到 per second 或者 per minute 啊!

Google 了一下,也查到不到相似的說法啊!

那是否是阿里雲改了 MySQL 底層,作了 per second 或者 per minute 的限制呢?

想不出別的緣由了,死馬當活馬醫,試一下吧!

問題解決

原本就是單線程運行的程序,爲了下降 CRUD 操做的頻率,只好在循環執行的邏輯中每次循環後添加 Thread.Sleep 等待。
將代碼改爲大概以下的樣子:

// ...

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

鏈接數竟然降至不到原來的一半了!!!

折騰了半天,最終竟然只是加了個 Sleep 問題便解決了,實在是太出乎意料了!

大跌眼鏡,有沒有?!

好在程序沒有那麼高的時效性要求,否則只能升級 MySQL Server 的配置規格了。

總結

問題雖然是解決了,可是依然有個疑惑,MySQL 官方文檔上明明說的是併發鏈接數限制,爲何在阿里雲 RDS MySQL 中,卻感受是限制了每一個 MySQL 實例每秒或每分的累計鏈接數呢?

不知道有沒有別的朋友遇到過這樣的問題?

無論怎樣,問題解決了,聊以記之,以儆效尤。


  1. https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_max_user_connections max_user_connections ↩︎

  2. https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_max_connections max_connections ↩︎

相關文章
相關標籤/搜索