一:背景
1. 講故事
中秋國(guó)慶長(zhǎng)假結(jié)束,哈哈,在老家拍了很多的短視頻,有興趣的可以上B站觀看:https://space.bilibili.com/409524162 ,今天繼續(xù)給大家分享各種奇奇怪怪的.NET生產(chǎn)事故,希望能幫助大家在未來(lái)的編程之路上少踩坑。
話不多說(shuō),這篇看一個(gè).NET程序集泄露導(dǎo)致的CLR私有堆泄露的案例,這個(gè)泄露和 JsonConvert 有關(guān),哈哈,相信你肯定比較驚訝!
二:WinDbg 分析
1. 到底是哪里的泄露
首先觀察一下進(jìn)程的提交內(nèi)存的大小,即通過 !address -summary 觀察。
0:000> !address -summary
--- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
Free 390 7dfa`63fa8000 ( 125.978 TB) 98.42%
<unknown> 13628 205`32974000 ( 2.020 TB) 99.92% 1.58%
Heap 8143 0`4042b000 ( 1.004 GB) 0.05% 0.00%
Stack 186 0`1f8e0000 ( 504.875 MB) 0.02% 0.00%
Image 1958 0`09775000 ( 151.457 MB) 0.01% 0.00%
Other 9 0`001d7000 ( 1.840 MB) 0.00% 0.00%
TEB 62 0`0007c000 ( 496.000 kB) 0.00% 0.00%
PEB 1 0`00001000 ( 4.000 kB) 0.00% 0.00%
--- Type Summary (for busy) ------ RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_MAppED 312 200`00a06000 ( 2.000 TB) 98.92% 1.56%
MEM_PRIVATE 21717 5`91ecd000 ( 22.280 GB) 1.08% 0.02%
MEM_IMAGE 1958 0`09775000 ( 151.457 MB) 0.01% 0.00%
--- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_FREE 390 7dfa`63fa8000 ( 125.978 TB) 98.42%
MEM_RESERVE 4509 205`0fc14000 ( 2.020 TB) 99.89% 1.58%
MEM_COMMIT 19478 0`8c434000 ( 2.192 GB) 0.11% 0.00%
當(dāng)前的提交內(nèi)存占用了 2.19G,進(jìn)程堆占用 1G ,差不多占了一半,但不能說(shuō)明就是非托管內(nèi)存泄露,接下來(lái)繼續(xù)觀察下托管堆。
0:000> !eeheap -gc
Number of GC Heaps: 8
------------------------------
Heap 7 (000001C4971013A0)
generation 0 starts at 0x000001C817D201A0
generation 1 starts at 0x000001C817C878D8
generation 2 starts at 0x000001C817261000
ephemeral segment allocation context: none
segment begin allocated size
000001C817260000 000001C817261000 000001C819013F98 0x1db2f98(31141784)
Large object heap starts at 0x000001C907261000
segment begin allocated size
000001C907260000 000001C907261000 000001C907261018 0x18(24)
Pinned object heap starts at 0x000001C987261000
000001C987260000 000001C987261000 000001C9872ABA50 0x4aa50(305744)
Heap Size: Size: 0x1dfda00 (31447552) bytes.
------------------------------
GC Heap Size: Size: 0xba26488 (195191944) bytes.
從卦中可以看到當(dāng)前的托管堆占用僅 195M,這就更好的驗(yàn)證當(dāng)前確實(shí)存在非托管內(nèi)存泄露,由于非托管內(nèi)存沒有開啟 ust,也沒有 perfview 的etw文件,所以沒有好的方式進(jìn)一步挖掘,到這里可能就止步不前了。
2. 到底是哪里的泄露
在 C# 所處的 windows 進(jìn)程中,其實(shí)有很多的堆,比如:crt堆,ntheap堆,gc堆,clr私有堆,堆外(VirtualAlloc),調(diào)試沒有標(biāo)準(zhǔn)答案,不斷的假設(shè),試探,摸著石頭過河,言外之意就是這個(gè)堆沒問題,不代表其他堆也沒有問題,這樣想思路就比較順暢了,我們可以看看其他的堆,比如這里的 CLR私有堆,使用 !eeheap -loader 觀察。
0:000> !eeheap -loader
Loader Heap:
--------------------------------------
...
Module 00007ff846e034c0: Size: 0x0 (0) bytes.
Module 00007ff846e03930: Size: 0x0 (0) bytes.
Module 00007ff846e04180: Size: 0x0 (0) bytes.
Module 00007ff846e047e0: Size: 0x0 (0) bytes.
Module 00007ff846e04e40: Size: 0x0 (0) bytes.
Total size: Size: 0x0 (0) bytes.
--------------------------------------
Total LoaderHeap size: Size: 0x47252000 (1193615360) bytes total, 0x1f68000 (32931840) bytes wasted.
=======================================
從卦中可以看到有非常多的 module 迸射出來(lái),估計(jì)有幾萬(wàn)個(gè),并且可以看到總的大小是 1.19G,到這里基本就搞清楚了,然來(lái)是 程序集泄露。
這里稍微補(bǔ)充一下,像這種問題早期可以使用 dotnet-counter 或者 Windows 的程序集指標(biāo) 監(jiān)控一下,或許你就能輕松找出原因,截圖如下:
PS C:UsersAdministratorDesktop> dotnet-counters monitor -n WebApplication2
圖片
而且 dotnet-counter 還是跨平臺(tái)的,非常實(shí)用,大家可以琢磨琢磨,接下來(lái)抽一個(gè)module 用命令 !dumpmodule -mt 00007ff846e034c0 觀察下,內(nèi)部到底有哪些類型。
0:000> !dumpmodule -mt 00007ff846e034c0
Name: Unknown Module
Attributes: Reflection IsDynamic IsInMemory
Assembly: 000001c9e193b9e0
BaseAddress: 0000000000000000
...
Types defined in this module
MT TypeDef Name
------------------------------------------------------------------------------
00007ff846e03db0 0x02000002
Types referenced in this module
MT TypeRef Name
------------------------------------------------------------------------------
00007ff820ff5748 0x02000002 xxx.xxx.Json.Converters.PolymorphismConverter`1
00007ff820e710f8 0x02000003 xxx.xxx.Models.IApiResult
0:000> !dumpmt -md 00007ff846e03db0
Number of IFaces in IFaceMap: 0
--------------------------------------
MethodDesc Table
Entry MethodDesc JIT Name
00007FF822F05FA8 00007ff823285b50 NONE xxx.Json.Converters.PolymorphismConverter`1
00007FF822EFD5E8 00007ff82323b1b8 NONE System.Text.Json.Serialization.JsonConverter`1
00007FF822EFD5F0 00007ff82323b1c8 NONE System.Text.Json.Serialization.JsonConverter`1
00007FF8414CB978 00007ff846e03d88 JIT IApiResultDynamicJsonConverter..ctor()
仔細(xì)分析卦中信息,可以很明顯的看到。
- Json.Converters.PolymorphismConverter
看樣子和牛頓有關(guān)系,并且還是一個(gè)自定義的 JsonConvert。
- IApiResult 和 IApiResultDynamicJsonConverter
看樣子是一個(gè)接口的返回協(xié)議類,需要在代碼中重點(diǎn)關(guān)注。
有了這些信息,接下來(lái)就是重點(diǎn)關(guān)注代碼中的 PolymorphismConverter 類,果然就找到了一處。
圖片
從類的定義來(lái)看,一般這種東西都是在 ConfigureServices 方法中做 初始化定義 的,按理說(shuō)問題不大,那為什么會(huì)有問題呢?還得要查下它的引用,終于給找到了,截圖如下:
圖片
這是一個(gè)低級(jí)錯(cuò)誤哈,每次讀取 ApiResult.Data 的時(shí)候都要 jsonSerializerOptions.AddPolymorphism(); 操作,也就每次都會(huì)創(chuàng)建程序集,終于真相大白。
三:總結(jié)
這種程序集泄露導(dǎo)致的生產(chǎn)事故不應(yīng)該哈,反應(yīng)了團(tuán)隊(duì)中多人協(xié)作的時(shí)候還是有待提高!