一次Mysql连接池卡死导致服务无响应问题分析(.Net Mysql.Data 8.0.21)

在线程递增到106时捕获dump文件,在windbg中分析到,有七十多个线程被阻塞在创建mysql连接的地方,具体调用堆栈如下图:

一次Mysql连接池卡死导致服务无响应问题分析(.Net Mysql.Data 8.0.21)

 

 

查看源码

当看到调用堆栈,可以看源码分析具体位置做了什么事情。我们只截取重要部分的代码。

由上图大概可以看到是创建连接时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      
上一篇:数据结构与算法学习 栈


下一篇:使用-robot【使用外部库】【random】