一次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=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这个包(源码地址),支持异步创建连接,就不会出现这个情况了。
在上面说的修改连接字符串的方式,虽然减少了出现的情况的几率,但是实际上还是会有阻塞线程的情况。所以推荐使用MySqlConnector这个包(源码地址),支持异步创建连接,就不会出现这个情况了。