首頁

目前文章總數:197 篇

  

最後更新:2025年 09月 13日

0005. Mysql 連線異常 - See wait_timeout and interactive_timeout for configuring this behavior. 與連線池效能說明

日期:2025年 10月 11日

標籤: Mysql C# Kafka

摘要:效能議題


相關參考:Mysql官網
探討問題:1. 工作環境中,看到不可思議的錯誤 Mysql 預設 8 小時才視為超時連線,正常不會保持 8 小時
     2. 此問題實務上是如何出現,與 Mysql 連線池的關係
基本介紹:本篇分為四大部分。
第一部分:說明問題
第二部分:重現此問題代碼說明
第三部分:工作環境中如何出現
第四部分:分析結果 & 結論






第一部分:說明問題

Step 1:原因 - 收到此錯誤信

關鍵字如下:

Unhandled exception. MySql.Data.MySqlClient.MySqlException (0x80004005): 
The client was disconnected by the server because of inactivity. 
See wait_timeout and interactive_timeout for configuring this behavior.


Mysql 預設是連線後超過 8 個小時候會斷線,但實務上沒有軟體工程師會將連線與 Mysql 保持連線後,超過 8 小時都不執行 SQL


Step 2:原因 - 檢查 Mysql 資料庫設定

檢查連線閒置時間是否太短,wait_timeoutinteractive_timeout

SHOW VARIABLES LIKE 'wait_timeout';
SHOW VARIABLES LIKE 'interactive_timeout';


可以確定都是預設值,那麼問題的原因多半在代碼層面




第二部分:重現此問題代碼說明

Step 1: 測試代碼

以下是測試用的範例代碼,有 5 個部分

描述 補充
1. 設定超時為 10 秒 (為了重現問題,8 小時太久)  
2. 首次查詢  
3. 等待 15 秒,為了閒置超時  
4. 第二次查詢 這時就會重現問題
5. 關閉  
using MySql.Data.MySqlClient;

class Program
{
    static async Task Main()
    {
        // 替換成你的 MySQL 連線字串
        string connStr = "Server=192.168.51.93;Port=3800;Database=demo;User ID=XXXX;Password=XXXX;max pool size=100";

        try
        {
            using (var conn = new MySqlConnection(connStr))
            {
                await conn.OpenAsync();
                Console.WriteLine("連線成功");

                // 1. 設定 SESSION wait_timeout = 10 秒
                using (var cmd = new MySqlCommand("SET SESSION wait_timeout=10;", conn))
                {
                    await cmd.ExecuteNonQueryAsync();
                    Console.WriteLine("SESSION wait_timeout 設為 10 秒");
                }

                // 2. 第一次查詢 (應該正常)
                using (var cmd = new MySqlCommand("SELECT NOW();", conn))
                {
                    var result = await cmd.ExecuteScalarAsync();
                    Console.WriteLine($"第一次查詢結果: {result}");
                }

                // 3. 等待 15 秒,超過 wait_timeout
                Console.WriteLine("等待 15 秒,模擬閒置超時...");
                await Task.Delay(TimeSpan.FromSeconds(15));

                try
                {
                    // 4. 第二次查詢 (此時連線應該已被 MySQL 斷掉)
                    using (var cmd = new MySqlCommand("SELECT NOW();", conn))
                    {
                        var result = await cmd.ExecuteScalarAsync();
                        Console.WriteLine($"第二次查詢結果: {result}");
                    }
                }
                catch (MySqlException ex)
                {
                    Console.WriteLine("捕捉到 MySqlException:");
                    Console.WriteLine(ex);
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
        }
        // 5. 結束
        Console.WriteLine("程式結束");
    }
}



Step 2: 首次查詢 - 開始閒置

我們使用測試代碼,進入閒置狀態 (執行 Open 連線後的等待)


Step 3: 二次查詢 - 報錯

因為我們 Session Timeout 設為 10 秒,閒置 15 秒後再次繼續執行 SQL 因此報錯,重現了此問題
工作環境上竟然會有 8 小時等待,這相當匪夷所思




第三部分:工作環境中如何出現

Step 1:問題代碼1 - 服務生命週期宣告

在自己的工作環境中在 C# Dotnet Worker 服務啟動後,會讓 Kafka 持續監聽,以便執行消費者處理
這段 scope 生命週期錯誤,在消費者外部執行 CreateScope();

【問題點 1】 在消費者外部建立一次 Scope 接著整個生命周期都用相同的 DbConnection 處理(參考 Step 2. 連線方式)
public class MYWorkerService
{
    private IUnitOfWorkFactory? mywork;

    public async Task ExecuteAsync(CancellationToken cancellationToken)
    {
       
        try
        {
            // 略...

             // 【問題點 1】
            using var scope = scopeFactory.CreateScope();
            mywork = scope.ServiceProvider.GetRequiredService<IUnitOfWorkFactory>(); 

            await kafkaConsumer.SubscribeAsync<MyKafkaConsumer>(async (data) =>
            {
                try
                {                    
                    await Process();                 
                }
                catch (Exception ex)
                {
                    // 略...
                }
            });
        }
        catch (Exception ex)
        {
           // 略...
        }
    }

    private void Process()
    {
         // 【問題點 1】
        mywork.SqlQuery();
    }
}



Step 2:問題代碼2 - 資料庫實例造成

並且 mywork.SqlQuery() 真正執行底層的資料庫操作時,就會發生此問題了。

【問題點 2】 將資料庫的 MySqlConnection 設定字典變數放在緩存中
【問題點 3】 若已存在緩存裡,就用相同的 MySqlConnection


最大的謬誤:直覺地看,用 Kafka 會持續監聽,可能會有高併發突然數百萬的消費者,用同個連線應該很合理?

但實際上生命週期中,沒有在 Kafka 每次執行消費者時觸發 scopeFactory.CreateScope(); 從而導致此緩存會讓每次都 使用相同的 DbConnection
最大的併發量是不可能提高的,用的都是同一個連線池

public class DbConnectionFactory(MyOptions myOptions, ILogger<DbConnectionFactory> logger)
    : FactoryBase(new MySqlConnection(DbConnectionString)), IDbConnectionFactory
{
    // 【問題點 2】
    private readonly ConcurrentDictionary<(int AId, int BId, int ControlId, CategoryEnum ResourceType), DbConnection> _dbConnectionDict = [];

    // 大量的 Mysql Method 略...

    private DbConnection GetConnection((int AId, int BId, int ControlId, CategoryEnum ResourceType) key)
    {
        if (!_dbConnectionDict.TryGetValue(key, out var dbConnection))
        {
            //略...
         
            var connectionString = GetDbConnectionStringAsync(
                key.ControlId,
                key.AId, key.BId, key.ResourceType).Result;

            dbConnection = new MySqlConnection(connectionString);

            // 略...

            // 【問題點 3】
            _dbConnectionDict.TryAdd(key, dbConnection);

            // 略...
        }

        return dbConnection;
    }
}



Step 3:問題出現解釋 - 圖解說明

現在已定位出資料庫緩存與生命週期的影響,現在回到此錯誤不難理解
如圖,第一次 Kafka 觸發消費者,並且記錄了此 DbConnection 緩存

只要 8 小時內都沒有觸發任何 DB 操作,這時再進行 DB 操作,就會出現此錯誤了。
※此狀況很容易出現,只要下班後到隔天都沒有人操作系統,產生 Kakfa 消費者事件。

 See wait_timeout and interactive_timeout for configuring this behavior






第四部分:分析結果 & 結論

Step 1:分析結果 - DB連線緩存的影響 & 連線池

暴力一點的做法可以將 8 小時,延長至很長,但真正的危險在於影響 Mysql 管理連線池
資料庫連線如下,使用了 Max Pool size = 100 ,意思是這個連線可以用到最大 100 個連線,交給 Mysql 幫我管理

string connStr = "Server=192.168.51.93;Port=3800;Database=demo;User ID=XXXX;Password=XXXX;max pool size=100";


但是如果用延長 wait_timeoutinteractive_timeout 的設定方式,並且都是用同個生命週期的 Scope + DB緩存

那面臨的就是資料庫永遠都是用 1 個 Connection 連線,在高併發的 Kafka 消費情境中,都是單執行緒處理消費

DB緩存 + 不使用新的生命週期 ==> 相當於 **max pool size=1**



Step 2:結論

如果真的正視效能問題,那麼合適的解決方案是:Worker 負責一次性處理 Kafka 每次消費,完成後 scope dispose,連帶釋放 DbContext。
讓 Mysql 連線池管理連線量