一次Mysql連接池卡死導致服務無響應問題分析(.Net Mysql.Data 8.0.21)


在線程遞增到106時捕獲dump文件,在windbg中分析到,有七十多個線程被阻塞在創建mysql連接的地方,具體調用堆棧如下圖:

 

 

查看源碼

當看到調用堆棧,可以看源碼分析具體位置做了什么事情。我們只截取重要部分的代碼。

由上圖大概可以看到是創建連接時OpenAsync后創建Tcp連接時導致的鎖。

 

//Open方法
//當開啟連接池時,從池子中拿mysql連接。
if (Settings.Pooling) 
{
  if (FailoverManager.FailoverGroup != null)
  {
    FailoverManager.AttemptConnection(this, Settings.ConnectionString, out string connectionString, true);
    currentSettings.ConnectionString = connectionString;
  }

  MySqlPool pool = MySqlPoolManager.GetPool(currentSettings);
  if (driver == null || !driver.IsOpen)
    driver = pool.GetConnection();
  ProcedureCache = pool.ProcedureCache;
}

//GetPool方法
//靜態變量,也就是說在一個進程間,都使用這個Pools
private static readonly Dictionary<string, MySqlPool> Pools = new Dictionary<string, MySqlPool>();
//通過lock鎖,來獲取是否緩存過連接
public static MySqlPool GetPool(MySqlConnectionStringBuilder settings)
{
  string text = GetKey(settings);

  lock (Pools)
  {
    MySqlPool pool;
    Pools.TryGetValue(text, out pool);

    if (pool == null)
    {
      pool = new MySqlPool(settings);
      Pools.Add(text, pool);
    }
    else
      pool.Settings = settings;

    return pool;
  }
}

//MySqlPool方法
//可以看到一個minsize,針對這個看板服務鏈接字符串中設置為10,也就是說第一次初始換的時候我們需要在一個鎖內創建10個mysql連接。
//這個服務需要連接5數據庫實例,也就是說,初始化的時候需要創建50個連接,恐怖如斯。
//多說一點,其實maxSize沒什么作用,如果實際連接數大於了maxSize,連接池還會繼續創建新的連接,並不會限制其數量。
public MySqlPool(MySqlConnectionStringBuilder settings)
{
  _minSize = settings.MinimumPoolSize;
  _maxSize = settings.MaximumPoolSize;

  _available = (int)_maxSize;
  _autoEvent = new AutoResetEvent(false);

  if (_minSize > _maxSize)
    _minSize = _maxSize;
  this.Settings = settings;
  _inUsePool = new List<Driver>((int)_maxSize);
  _idlePool = new Queue<Driver>((int)_maxSize);

  //看這里初始化最小連接數
  for (int i = 0; i < _minSize; i++)
    EnqueueIdle(CreateNewPooledConnection());

  ProcedureCache = new ProcedureCache((int)settings.ProcedureCacheSize);
}

//CreateNewPooledConnection方法內是創建tcp連接,直接看主要方法。
//我們可以看到在dnsTask.Wait,這個其實執行很快。
//主要是創建Tcp連接時比較慢,它根據連接超時時間等待是否連接完成,默認是60s。
private static Stream GetTcpStream(MySqlConnectionStringBuilder settings, ref MyNetworkStream networkStream)
{
  Task<IPAddress[]> dnsTask = Dns.GetHostAddressesAsync(settings.Server);
  dnsTask.Wait();
  if (dnsTask.Result == null || dnsTask.Result.Length == 0)
    throw new ArgumentException(Resources.InvalidHostNameOrAddress);
  IPAddress addr = dnsTask.Result.FirstOrDefault(c => c.AddressFamily == AddressFamily.InterNetwork);
  if (addr == null)
    addr = dnsTask.Result[0];
  TcpClient client = new TcpClient(addr.AddressFamily);
  Task task = client.ConnectAsync(settings.Server, (int)settings.Port);      
  
  //主要看這里
  if (!task.Wait(((int)settings.ConnectionTimeout * 1000)))
    throw new MySqlException(Resources.Timeout);
  if (settings.Keepalive > 0)
  {
    SetKeepAlive(client.Client, settings.Keepalive);
  }
  networkStream = new MyNetworkStream(client.Client,true);
  var result = client.GetStream();
  GC.SuppressFinalize(result);

  return result;
}

 

 
         

產生原因

看上面的源碼你可能就也能想到,如果使用連接池,我們可以把連接字符串中的minSize設置小一點(比如設置為0)和Connection TimeOut設置小一點(5s),我們再次啟動程序后,可以看到顯著的效果,線程激增的情況會減少,可能重啟多次會有一次這種效果。
在初始化創建連接時,大部分的線程被卡到獲取連接的地方,不斷有請求進來,線程池里面的線程,就被阻塞,需要創建新的線程執行任務,就導致線程一直遞增。

解決辦法
方法一
#修改前 server=mysql.rds.aliyuncs.com;port=3306;uid=;password=;character set=utf8mb4;Initial Catalog=wgcapplyvehicledb;pooling=true;min pool size=10;max pool size=100;connect timeout =10;
#修改后 server=mysql.rds.aliyuncs.com;port=3306;uid=;password=;character set=utf8mb4;Initial Catalog=wgcapplyvehicledb;pooling=true;min pool size=0;max pool size=100;connect timeout =5;
#或者不使用連接池 server=rds.aliyuncs.com;port=3306;uid=;password=;character set=utf8mb4;Initial Catalog=wgcapplyvehicledb;connect timeout =5;
方法二
在上面說的修改連接字符串的方式,雖然減少了出現的情況的幾率,但是實際上還是會有阻塞線程的情況。所以推薦使用MySqlConnector這個包( 源碼地址),支持異步創建連接,就不會出現這個情況了。
 
 
https://www.cnblogs.com/shamork/p/6636305.html
 
 
 


免責聲明!

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



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