分享程式代碼相關筆記
目前文章總數:197 篇
最後更新:2025年 09月 13日
關鍵字如下:
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
檢查連線閒置時間是否太短,wait_timeout 或 interactive_timeout
SHOW VARIABLES LIKE 'wait_timeout';
SHOW VARIABLES LIKE 'interactive_timeout';
可以確定都是預設值,那麼問題的原因多半在代碼層面上
以下是測試用的範例代碼,有 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("程式結束");
}
}
我們使用測試代碼,進入閒置狀態 (執行 Open 連線後的等待)
因為我們 Session Timeout 設為 10 秒,閒置 15 秒後再次繼續執行 SQL 因此報錯,重現了此問題
工作環境上竟然會有 8 小時等待,這相當匪夷所思
在自己的工作環境中在 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();
}
}
並且 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;
}
}
現在已定位出資料庫緩存與生命週期的影響,現在回到此錯誤不難理解
如圖,第一次 Kafka 觸發消費者,並且記錄了此 DbConnection 緩存
只要 8 小時內都沒有觸發任何 DB 操作,這時再進行 DB 操作,就會出現此錯誤了。
※此狀況很容易出現,只要下班後到隔天都沒有人操作系統,產生 Kakfa 消費者事件。
See wait_timeout and interactive_timeout for configuring this behavior
暴力一點的做法可以將 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_timeout 或 interactive_timeout 的設定方式,並且都是用同個生命週期的 Scope + DB緩存
那面臨的就是資料庫永遠都是用 1 個 Connection 連線,在高併發的 Kafka 消費情境中,都是單執行緒處理消費
DB緩存 + 不使用新的生命週期 ==> 相當於 **max pool size=1**
如果真的正視效能問題,那麼合適的解決方案是:Worker 負責一次性處理 Kafka 每次消費,完成後 scope dispose,連帶釋放 DbContext。
讓 Mysql 連線池管理連線量