一次Mysql鏈接池卡死致使服務無響應問題分析(.Net Mysql.Data 6.9.9)

問題:

進程啓動後,線程數迅速上升至最小線程數後,緩慢上升(線程池限制)到數千,而後因爲線程過多,CPU飆升到90%sql

對外表現爲Api無響應或鏈接超時。數據庫

 

背景

有些數據存在於另外一個機房,經過內網專線鏈接。一個服務程序有4個數據庫,其中3個在本地機房,1個在外地。    api

 

各類排查,沒有解決。緩存

 

最終的處理方法

 

Dump進程

  1. 使用進程管理器,建立進程Dump文件。
  2. 使用VisualStudio打開該Dump文件並進行託管調試
  3. 查看並行堆棧,發現大部分線程均處於MySql.Data.MySqlClient.MySqlPoolManager.GetPool這個函數的調用中。並在此處進入了本機代碼。處於其餘調用堆棧的線程屈指可數。

 

 

 

代碼分析

  1. 因爲Mysql.Data.dll沒有對應的pdb文件(Oracle沒有提供),因此在Visual Studio中不能進入其中的代碼,所以直接反編譯,找到該函數,代碼以下:

 

 

函數中,第一句的GetKey函數以下,其中有一個lock。其中代碼僅僅是賦值,或是在集成認證的狀況下才執行。因此卡住的可能性不大。服務器

 

 

第二句是個賦值,且MysqlPoolManager.pools是個字段(field),理論上不會卡住。網絡

第二個lock中,若是指定key對應的緩存已存在,則lock會很快返回。若是不存在,則執行new MysqlPool(setttings);函數代碼以下:
異步

 

其主要功能有socket

  1. 建立一個事件,用於獲取鏈接時的異步等待
  2. 根據settings持久化設置
  3. 初始化池驅動列表、隊列
  4. 按照配置的minSize建立指定數量的鏈接。
  5. 建立一個過程緩存,代碼以下

     

     

5個步驟中,最可能耗時較久的是步驟d。其餘步驟理論上不會有問題。函數

步驟d中的代碼,雖然就一個函數,可是代碼不少。spa

通過不停的查看代碼,發現其主要功能是根據鏈接字符串中的設置,建立一個指定類型的鏈接。其底層建立代碼以下:

 

 

能夠看到,任何建立Stream失敗的狀況都會拋出異常,最終致使鏈接池建立失敗。

其中第一句,GetStream的底層代碼以下:

 

 

開始鏈接(BeginConnect)後,即開始了等待。等待的超時默認值以下:

2147483s,即596h。若是有鏈接到數據庫服務器的網絡有問題或其餘緣由致使鏈接不成功,而也未觸發其餘致使失敗的狀況,則會一直等下去。若是推斷正確,那麼全部線程中,必定有線程的調用堆棧在以下位置:

 

 Dump文件中的全部線程堆棧排序,有且僅有一個線程處於該調用堆棧處。高亮行正是上述堆棧的函數名CreateSocketStream上面一行就是WaiteOne。以後進入本機代碼。

 

那麼根本緣由也就清楚了:一個鏈接的建立卡住了數據庫鏈接建立,間接卡住了鏈接池的鎖,又間接卡住了其餘鏈接池的使用和建立。致使全部數據庫鏈接不可用。因此,全部進入的請求通過運行,所有堆在GetPool這裏。

 

解決方法:

  1. 保證網絡正常(跨機房專線穩定性不可控,有人搖晃光纖玩 o(_)o 或者其餘緣由致使流量堵塞)
  2. 容易卡的數據庫鏈接分離出去到單獨的進程。這樣因爲不共享鎖,因此不會卡住其餘線程池的使用和建立。
  3. 須要跨機房的業務,在數據所在機房單獨提供api,內網失效時能夠走外網。
  4. 容易卡住的線程池鏈接字符串中設置minPoolSize=0。這樣建立鏈接池時,不預建立鏈接而影響其餘鏈接池。可是,對於突發流量增加的狀況,響應可能不夠及時。
  5. 設置一個合理的ConnectionTimeout。能夠有效避免鏈接建立時卡住,致使api無響應和其餘反作用。

 

其餘在源代碼中發現的須要注意的地方

  1. 鏈接池中空閒鏈接的空閒時間是180s
  2. 清理週期第一次是188s,以後保持180s
  3. 若是鏈接池中的空閒鏈接數大於設置的minPoolSize,則清理空閒鏈接直到minPoolSize
  4. ConnectionTimeout 用於幾個地方
    1. ) 鏈接socket時的等待超時
    2. ) 鏈接以後,鏈接上的讀寫超時。
    3. ) 從已空且總數達上限的鏈接池中,等待可用鏈接時的等待超時

以上全部信息基於.Net版本Mysql.Data 6.9.9版本反編譯分析。

相關文章
相關標籤/搜索