在線程遞增到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這個包( 源碼地址),支持異步創建連接,就不會出現這個情況了。