记一次 .NET 某流媒体独角兽 API 句柄泄漏分析
一:背景
1. 講故事
上上周有位朋友找到我,說他的程序CPU和句柄都在不斷的增長,無回頭趨勢,查了好些天也沒什么進展,特加wx尋求幫助,截圖如下:
看的出來這位朋友也是非常郁悶,出問題還出兩個,氣人哈,關(guān)于 cpu 爆高的問題我準備單獨用一篇文章去偵讀,這篇就先聊聊 句柄泄漏 的問題,畢竟寫了20多篇,也是第一次聊到 handle 泄露,有點意思哈。
2. 什么是句柄
我個人理解的句柄:就是在托管層持有了一個對非托管層資源的引用,有了這個引用,我們就可以強制回收非托管資源,那什么是非托管資源?我個人的理解是 gc 管不到的地方都是 非托管資源。
通常包含這種句柄的類有:FileStream, Socket 等,如果大家有這個前置基礎(chǔ),接下來就可以用 windbg 去分析啦!
二:windbg 分析
1. 看問題表象
朋友從 任務管理器 中看到 handle =8770,那就說明程序中有 8770 個對非托管資源持有句柄,那怎么去看呢? 在說這個之前,大家有沒有遇到這種現(xiàn)象,就是不管程序怎么泄漏,只要我們退出exe,那么所有的資源都會被神奇的 釋放, 不管是托管資源還是非托管資源,這樣說相信有很有朋友好奇這是怎么實現(xiàn)的???大家可以先想 10s。
揭曉答案啦!簡單的說, CLR 在內(nèi)部維護了一張句柄表,當程序關(guān)閉時,CLR會強制釋放句柄表中的所有句柄,那問題就簡單了,既然 CLR 能觸達,我相信通過 windbg 也能做到,對,就是通過 !gchandles 命令。
2. 查看句柄表
這里提醒一下,!gchandles 的作用域是 AppDomain,而不是 Process,接下來看一下命令輸出:
0:000>?!gchandles?-stat Statistics:MT????Count????TotalSize?Class?Name ... 00007ffccc1d2360????????3???????262280?System.Byte[] 00007ffccc116610???????72???????313224?System.Object[] 00007ffccc3814a0?????8246???????593712?System.Threading.OverlappedData Total?10738?objectsHandles:Strong?Handles:???????312Pinned?Handles:???????18Async?Pinned?Handles:?8246Ref?Count?Handles:????1Weak?Long?Handles:????2080Weak?Short?Handles:???59Dependent?Handles:????22從輸出看,有一組數(shù)據(jù)特別刺眼,那就是:Async Pinned Handles = 8246 [System.Threading.OverlappedData],這是什么意思呢?從英文名就能看出這是一個和 異步IO 相關(guān)的句柄,有些朋友應該知道,在異步IO的過程中,會有一個 byte[] 被 pinned 住,同時還有一個異步IO的上下文對象 OverlappedData。
接下來的一個問題是:既然是異步IO,那它的 handle 是什么類型,如前面所說是 FileStream 還是 Socket ?要想找出答案,就需要深挖 OverlappedData 對象,相關(guān)的命令是:!dumpheap -mt xxx & !do ... ,參考如下:
0:000>?!DumpHeap?/d?-mt?00007ffccc3814a0Address???????????????MT?????Size 000001aa2acb39c8?00007ffccc3814a0???????72????? 000001aa2acb3fd8?00007ffccc3814a0???????72????? 000001aa2ad323d0?00007ffccc3814a0???????72????? ... 0:000>?!do?000001aa2acb39c8 Name:????????System.Threading.OverlappedData MethodTable:?00007ffccc3814a0 EEClass:?????00007ffccc37ca18 Size:????????72(0x48)?bytes File:????????C:\xxx\xxx\vms_210819\System.Private.CoreLib.dll Fields:MT????Field???Offset?????????????????Type?VT?????Attr????????????Value?Name 00007ffccc21f508??40006b2????????8??System.IAsyncResult??0?instance?0000000000000000?_asyncResult 00007ffccc110ae8??40006b3???????10????????System.Object??0?instance?000001aa2acb4020?_callback 00007ffccc381150??40006b4???????18?...eading.Overlapped??0?instance?000001aa2acb3980?_overlapped 00007ffccc110ae8??40006b5???????20????????System.Object??0?instance?000001aa2acb9fe8?_userObject 00007ffccc11f130??40006b6???????28??????????????????PTR??0?instance?000001aa2a9bd830?_pNativeOverlapped 00007ffccc11ecc0??40006b7???????30????????System.IntPtr??1?instance?0000000000000000?_eventHandle 0:000>?!DumpObj?/d?000001aa2acb3980 Name:????????System.Threading.ThreadPoolBoundHandleOverlapped MethodTable:?00007ffccc3812a0 EEClass:?????00007ffccc37c9a0 Size:????????72(0x48)?bytes File:????????C:\xxx\xxx\vms_210819\System.Private.CoreLib.dll Fields:MT????Field???Offset?????????????????Type?VT?????Attr????????????Value?Name 00007ffccc3814a0??40006ba????????8?...ng.OverlappedData??0?instance?000001aa2acb39c8?_overlappedData 00007ffccc34fcd0??40006a4???????10?...ompletionCallback??0?instance?000001aa2acb3920?_userCallback 00007ffccc110ae8??40006a5???????18????????System.Object??0?instance?000001aa2acb38c8?_userState 00007ffccc380120??40006a6???????20?...locatedOverlapped??0?instance?000001aa2acb3960?_preAllocated 00007ffccc11f130??40006a7???????30??????????????????PTR??0?instance?000001aa2a9bd830?_nativeOverlapped 00007ffccc380eb8??40006a8???????28?...adPoolBoundHandle??0?instance?000001aa2acb3900?_boundHandle 00007ffccc1171c8??40006a9???????38???????System.Boolean??1?instance????????????????0?_completed 00007ffccc34fcd0??40006a3??????458?...ompletionCallback??0???static?000001aa2acb4020?s_completionCallback 0:000>?!DumpObj?/d?000001aa2acb3900 Name:????????System.Threading.ThreadPoolBoundHandle MethodTable:?00007ffccc380eb8 EEClass:?????00007ffccc37c870 Size:????????32(0x20)?bytes File:????????C:\xxx\xxx\vms_210819\System.Private.CoreLib.dll Fields:MT????Field???Offset?????????????????Type?VT?????Attr????????????Value?Name 00007ffccc1d76b0??40006a1????????8?...rvices.SafeHandle??0?instance?000001aa2acb1d30?_handle 00007ffccc1171c8??40006a2???????10???????System.Boolean??1?instance????????????????0?_isDisposed0:000>?!DumpObj?/d?000001aa2acb1d30 Name:????????Microsoft.Win32.SafeHandles.SafeFileHandle MethodTable:?00007ffccc3807c8 EEClass:?????00007ffccc37c548 Size:????????48(0x30)?bytes File:????????C:\xxx\xxx\xxx\System.Private.CoreLib.dll Fields:MT????Field???Offset?????????????????Type?VT?????Attr????????????Value?Name 00007ffccc11ecc0??4000bb4????????8????????System.IntPtr??1?instance?0000000000000428?handle 00007ffccc11b1e8??4000bb5???????10?????????System.Int32??1?instance????????????????4?_state 00007ffccc1171c8??4000bb6???????14???????System.Boolean??1?instance????????????????1?_ownsHandle 00007ffccc1171c8??4000bb7???????15???????System.Boolean??1?instance????????????????1?_fullyInitialized 00007ffccc2f1ae0??4001c3d???????20?...Private.CoreLib]]??1?instance?000001aa2acb1d50?_isAsync 00007ffccc380eb8??4001c3e???????18?...adPoolBoundHandle??0?instance?0000000000000000?<ThreadPoolBinding>k__BackingField上面倒數(shù)第五行的 0000000000000428 就是具體的 handle 值,接下來就可以用 !handle 命令查看其值的具體信息。
0:000>?!handle?0000000000000428?7 Handle?428Type??????????FileAttributes????0GrantedAccess?0x100081:SynchRead/List,ReadAttrHandleCount???2PointerCount??65489從 Type:File 可以看出,原來這 8000 多都是文件句柄哈。。。
寫到這里貌似就到了死胡同了????????????,雖然挖了一些信息,但這些信息還不足以讓我找到問題根源,從引用鏈上來說,gchandles 中的這些對象是處于引用鏈的頂端,換句話說,我需要找到這條引用鏈下游的一些數(shù)據(jù)對象,一個好的入口點就是到 heap 中去挖。
3. 從托管堆找 OverlappedData 的徒孫輩
首先我們用 !dumpheap -stat 查看下托管堆。
0:000>?!dumpheap?-stat Statistics:MT????Count????TotalSize?Class?Name ...00007ffccc3c5e18???939360?????52604160?System.Collections.Generic.SortedSet`1+Node[[System.Collections.Generic.KeyValuePair`2[[System.String,?System.Private.CoreLib],[System.String,?System.Private.CoreLib]],?System.Private.CoreLib]] 00007ffccc1d2360????16492?????69081162?System.Byte[] 000001aa2a99af00????10365?????76689384??????Free 00007ffccc1d1e18??1904987????116290870?System.String既然是找引用鏈下游,那就從基礎(chǔ)類型 System.String 或者 System.Byte[] 入手,這里我就選擇前者,寫了一個對 mt 下所有 address 進行分組統(tǒng)計的腳本,畢竟人肉是不可能的,從腳本的輸出中我抽了幾個 address 查看 !gcroot,大概都是類似這樣的內(nèi)容。
0:000>?!gcroot?000001aa47a0c030 HandleTable:000001AA4469C090?(async?pinned?handle)->?000001AA491EB908?System.Threading.OverlappedData->?000001AA491EB8C0?System.Threading.ThreadPoolBoundHandleOverlapped->?000001AA491EB860?System.Threading.IOCompletionCallback->?000001AA491EAF30?System.IO.FileSystemWatcher->?000001AA491EB458?System.IO.FileSystemEventHandler...->?000001AA47A0C030?System.String0:000>?!gcroot?000001aa2d3ea480 HandleTable:000001AA28FE9930?(async?pinned?handle)->?000001AA2DD68220?System.Threading.OverlappedData->?000001AA2DD681D8?System.Threading.ThreadPoolBoundHandleOverlapped->?000001AA2DD68178?System.Threading.IOCompletionCallback->?000001AA2DD67848?System.IO.FileSystemWatcher...->?000001AA2D3EA480?System.String????從整個引用鏈來看,里面都有一個 System.IO.FileSystemWatcher ,這和前面分析的 handle= File 是一致的,然后就是導出這些 string ,發(fā)現(xiàn)大部分都是和 appSettings 相關(guān),如下所示:
string:?appSettings:RabbitMQLogQueue string:?appSettings:MedicalMediaServerIP string:?appSettings:UseHttps ...然后用 !strings 命令進行了模糊匹配,發(fā)現(xiàn)這樣的string 高達 61w 。。。
到這里基本就能斷定:appsettings 被 watch 了,但 watch 的方式有問題。。。
4. 尋找最終答案
將調(diào)查結(jié)果給了朋友之后,讓朋友著重觀察下對 appsetting 進行 watch 的方式是否有問題?幾個小時后,朋友終于找到了。
大概意思是說:本身已經(jīng)通過設置 reloadOnChange=true 對 appsetings 進行了監(jiān)控,但寫碼的人對這一塊不熟悉,又通過每10s一次輪詢對appsettings進行數(shù)據(jù)感知,問題就出現(xiàn)在這里。。。
三:總結(jié)
其實本次事故的主要原因還是在于對如何實時感知 appsettings 中最新數(shù)據(jù)的玩法不熟悉,一邊用了 .netcore 自帶的 reloadOnChange 監(jiān)控,一邊還用輪詢的方式進行數(shù)據(jù)感知,所以說基礎(chǔ)還是很重要的,不要想當然的去寫!????????????
END
工作中的你,是否已遇到 ...?
1. CPU爆高
2. 內(nèi)存暴漲
3. 資源泄漏
4. 崩潰死鎖
5. 程序呆滯
等緊急事件,全公司都指望著你能解決...? 危難時刻才能展現(xiàn)你的技術(shù)價值,作為專注于.NET高級調(diào)試的技術(shù)博主,歡迎微信搜索: 一線碼農(nóng)聊技術(shù),免費協(xié)助你分析Dump文件,希望我能將你的踩坑經(jīng)驗分享給更多的人。
總結(jié)
以上是生活随笔為你收集整理的记一次 .NET 某流媒体独角兽 API 句柄泄漏分析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【招聘(上海)】美团酒店招聘 .NET
- 下一篇: EF Core使用Simple Logg