問題:
進程啟動后,線程數迅速上升至最小線程數后,緩慢上升(線程池限制)到數千,然后由於線程過多,CPU飆升到90%。
對外表現為Api無響應或連接超時。
背景
有些數據存在於另一個機房,通過內網專線連接。一個服務程序有4個數據庫,其中3個在本地機房,1個在外地。
各種排查,沒有解決。
最終的處理方法
Dump進程
- 使用進程管理器,創建進程Dump文件。
- 使用VisualStudio打開該Dump文件並進行托管調試
- 查看並行堆棧,發現大部分線程均處於MySql.Data.MySqlClient.MySqlPoolManager.GetPool這個函數的調用中。並在此處進入了本機代碼。處於其他調用堆棧的線程屈指可數。
代碼分析
- 由於Mysql.Data.dll沒有對應的pdb文件(Oracle沒有提供),所以在Visual Studio中不能進入其中的代碼,因此直接反編譯,找到該函數,代碼如下:
函數中,第一句的GetKey函數如下,其中有一個lock。其中代碼僅僅是賦值,或是在集成認證的情況下才執行。所以卡住的可能性不大。
第二句是個賦值,且MysqlPoolManager.pools是個字段(field),理論上不會卡住。
第二個lock中,如果指定key對應的緩存已存在,則lock會很快返回。如果不存在,則執行new MysqlPool(setttings);函數代碼如下:
其主要功能有
- 創建一個事件,用於獲取連接時的異步等待
- 根據settings持久化設置
- 初始化池驅動列表、隊列
- 按照配置的minSize創建指定數量的連接。
-
創建一個過程緩存,代碼如下
這5個步驟中,最可能耗時較久的是步驟d。其他步驟理論上不會有問題。
步驟d中的代碼,雖然就一個函數,但是代碼很多。
經過不停的查看代碼,發現其主要功能是根據連接字符串中的設置,創建一個指定類型的連接。其底層創建代碼如下:
可以看到,任何創建Stream失敗的情況都會拋出異常,最終導致連接池創建失敗。
其中第一句,GetStream的底層代碼如下:
開始連接(BeginConnect)后,即開始了等待。等待的超時默認值如下:
2147483s,即596h。如果有連接到數據庫服務器的網絡有問題或其他原因導致連接不成功,而也未觸發其他導致失敗的情況,則會一直等下去。如果推斷正確,那么所有線程中,一定有線程的調用堆棧在如下位置:
對Dump文件中的所有線程堆棧排序,有且僅有一個線程處於該調用堆棧處。高亮行正是上述堆棧的函數名CreateSocketStream。上面一行就是WaiteOne。之后進入本機代碼。
那么根本原因也就清楚了:一個連接的創建卡住了數據庫連接創建,間接卡住了連接池的鎖,又間接卡住了其他連接池的使用和創建。導致所有數據庫連接不可用。所以,所有進入的請求經過運行,全部堆在GetPool這里。
解決方法:
- 保證網絡正常(跨機房專線穩定性不可控,有人搖晃光纖玩 o(∩_∩)o 或者其他原因導致流量堵塞)
- 容易卡的數據庫連接分離出去到單獨的進程。這樣由於不共享鎖,所以不會卡住其他線程池的使用和創建。
- 需要跨機房的業務,在數據所在機房單獨提供api,內網失效時可以走外網。
- 容易卡住的線程池連接字符串中設置minPoolSize=0。這樣創建連接池時,不預創建連接而影響其他連接池。但是,對於突發流量增長的情況,響應可能不夠及時。
- 設置一個合理的ConnectionTimeout。可以有效避免連接創建時卡住,導致api無響應和其他副作用。
其他在源代碼中發現的需要注意的地方
- 連接池中空閑連接的空閑時間是180s。
- 清理周期第一次是188s,之后保持180s。
- 如果連接池中的空閑連接數大於設置的minPoolSize,則清理空閑連接直到minPoolSize。
-
ConnectionTimeout 用於幾個地方
- ) 連接socket時的等待超時
- ) 連接之后,連接上的讀寫超時。
- ) 從已空且總數達上限的連接池中,等待可用連接時的等待超時
以上所有信息基於.Net版本Mysql.Data 6.9.9版本反編譯分析。