一:背景
1. 講故事
前些天又遇到了一例 FileSystemWatcher 引發的內存碎片化故障,但這個碎片化不是因為經典的 reloadOnChange=true 導致的,所以我覺得有必要做一次深度的反思,供以後遇到類似問題提供技術上的解決方法,這篇我們就來系統的講解下 兩種碎片化方式的調查方法。
二:經典的 FileSystemWatcher 碎片化
1. 測試代碼
這種碎片化是由 reloadOnChange=true 引發的,禍根主要是程序員將 .netframework 讀取配置文件的方式套在了 .net 上,為了方便演示,先上一段測試代碼。
internal class Program
{
static void Main(string[] args)
{
for (int i = 0; i < 100000; i++)
{
IConfiguration configuration = BuildConfiguration();
string appName = configuration["AppName"];
Console.WriteLine($"i={i} 應用名稱: {appName}");
}
Console.ReadLine();
}
static IConfiguration BuildConfiguration()
{
return new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.Build();
}
}
卦中的代碼非常簡單,就是每次讀取 AppName 時都調了一下 BuildConfiguration 方法,僅此而已,但將程序跑起來之後,居然發現程序吃了 2.2G 的內存,真是沒邊的事,截圖如下:
為了找出原因,上 windbg 附加,使用 !dumpheap -stat 觀察託管堆,截圖如下:
從卦中可以看到兩點信息:
- Free 獨佔
1.39G,這是經典的內存碎片化。 - FileSystemWatcher 高達 1290 個,表明程序存在大量的文件監控。
看到上面兩點信息,一定要有條件反射,是不是 reloadOnChange: true 導致的。
2. 是 reloadOnChange 導致的嗎
要想找到答案,可以深挖 Microsoft.Extensions.Configuration.ConfigurationRoot 類,即代碼 BuildConfiguration(); 的返回類型,為了方便可視化觀察,我用 vs 直接找下給大家看看,截圖如下:
有了這個脈絡,就可以使用 windbg 下鑽觀察,最終就找到了 <ReloadOnChange>k__BackingField = 1 的鐵證,參考如下:
0:008> !dumpobj /d 17dd2f41fa0
Name: Microsoft.Extensions.Configuration.ConfigurationRoot
MethodTable: 00007ff9d8707a48
EEClass: 00007ff9d86e97b0
Tracked Type: false
Size: 40(0x28) bytes
File: D:\travels\src\Example\Example_0_1\bin\Debug\net8.0\Microsoft.Extensions.Configuration.dll
Fields:
MT Field Offset Type VT Attr Value Name
00007ff9d8706c48 4000016 8 ...on.Abstractions]] 0 instance 0000017dd2f3e520 _providers
00007ff9d880ba28 4000017 10 ...Private.CoreLib]] 0 instance 0000017dd2f42018 _changeTokenRegistrations
00007ff9d8708940 4000018 18 ...rationReloadToken 0 instance 0000017dd2f41fc8 _changeToken
0:008> !DumpObj /d 0000017dd2f3e520
Name: System.Collections.Generic.List`1[[Microsoft.Extensions.Configuration.IConfigurationProvider, Microsoft.Extensions.Configuration.Abstractions]]
MethodTable: 00007ff9d87069d0
EEClass: 00007ff9d86a10f8
Tracked Type: false
Size: 32(0x20) bytes
File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.22\System.Private.CoreLib.dll
Fields:
MT Field Offset Type VT Attr Value Name
00007ff9d891d1a0 400226e 8 System.__Canon[] 0 instance 0000017dd2f41f68 _items
00007ff9d8551188 400226f 10 System.Int32 1 instance 1 _size
00007ff9d8551188 4002270 14 System.Int32 1 instance 1 _version
00007ff9d891d1a0 4002271 8 System.__Canon[] 0 static dynamic statics NYI s_emptyArray
0:008> !DumpArray /d 0000017dd2f41f68
Name: Microsoft.Extensions.Configuration.IConfigurationProvider[]
MethodTable: 00007ff9d8707cf0
EEClass: 00007ff9d851c440
Size: 56(0x38) bytes
Array: Rank 1, Number of elements 4, Type CLASS
Element Methodtable: 00007ff9d8706938
[0] 0000017dd2f3e540
[1] null
[2] null
[3] null
0:008> !DumpObj /d 0000017dd2f3e540
Name: Microsoft.Extensions.Configuration.Json.JsonConfigurationProvider
MethodTable: 00007ff9d8708200
EEClass: 00007ff9d86e9ab8
Tracked Type: false
Size: 48(0x30) bytes
File: D:\travels\src\Example\Example_0_1\bin\Debug\net8.0\Microsoft.Extensions.Configuration.Json.dll
Fields:
MT Field Offset Type VT Attr Value Name
00007ff9d8708940 4000012 8 ...rationReloadToken 0 instance 0000017dd2f437e0 _reloadToken
00007ff9d8708cf0 4000013 10 ...Private.CoreLib]] 0 instance 0000017dd2f42298 <Data>k__BackingField
00007ff9d8662820 4000005 18 System.IDisposable 0 instance 0000017dd2f3e690 _changeTokenRegistration
00007ff9d8701b98 4000006 20 ...nfigurationSource 0 instance 0000017dd2f3e4b8 <Source>k__BackingField
0:008> !DumpObj /d 0000017dd2f3e4b8
Name: Microsoft.Extensions.Configuration.Json.JsonConfigurationSource
MethodTable: 00007ff9d8701c88
EEClass: 00007ff9d86e7868
Tracked Type: false
Size: 48(0x30) bytes
File: D:\travels\src\Example\Example_0_1\bin\Debug\net8.0\Microsoft.Extensions.Configuration.Json.dll
Fields:
MT Field Offset Type VT Attr Value Name
00007ff9d86d8188 4000007 8 ...ers.IFileProvider 0 instance 0000017dd2f3e230 <FileProvider>k__BackingField
00007ff9d85cec08 4000008 10 System.String 0 instance 0000017d00100510 <Path>k__BackingField
00007ff9d851d070 4000009 24 System.Boolean 1 instance 0 <Optional>k__BackingField
00007ff9d851d070 400000a 25 System.Boolean 1 instance 1 <ReloadOnChange>k__BackingField
00007ff9d8551188 400000b 20 System.Int32 1 instance 250 <ReloadDelay>k__BackingField
00007ff9d8708420 400000c 18 ....FileExtensions]] 0 instance 0000000000000000 <OnLoadException>k__BackingField
三:非經典的 FileSystemWatcher 碎片化
1. 測試代碼
有的時候會出現 FileSystemWatcher 很少,但 overlapped 很多的情況,這種情況很大概率不是 reloadOnChange: true 導致的,截圖如下:
像這種情況可能就需要開啓追蹤了,可以藉助🐂👃的harmony 搞定,那如何做呢?可以鈎住 FileSystemWatcher 的所有構造函數,通過記錄調用棧來觀察到底是什麼代碼調用的,從而尋找禍根,參考代碼如下:
internal class Program
{
static void Main(string[] args)
{
var harmony = new Harmony("com.example.fswatcher");
harmony.PatchAll();
for (int i = 0; i < 5; i++)
{
IConfiguration configuration = BuildConfiguration();
string appName = configuration["AppName"];
Console.WriteLine($"i={i} 應用名稱: {appName}");
}
Console.ReadLine();
}
static IConfiguration BuildConfiguration()
{
return new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.Build();
}
}
[HarmonyPatch]
public class FileSystemWatcherConstructorsPatch
{
[HarmonyTargetMethod]
static IEnumerable<MethodBase> TargetMethods()
{
// 一次性獲取所有公共實例構造函數
return typeof(FileSystemWatcher).GetConstructors(BindingFlags.Public | BindingFlags.Instance);
}
[HarmonyPostfix]
public static void Postfix(FileSystemWatcher __instance)
{
Console.WriteLine($"[Harmony] FileSystemWatcher 構造函數被調用");
Console.WriteLine($"[Harmony] 路徑: '{__instance.Path ?? "null"}', 過濾器: '{__instance.Filter ?? "null"}'");
Console.WriteLine($"[Harmony] 調用棧:");
Console.WriteLine(Environment.StackTrace);
}
}
從卦中可以看到,原來這個 FileSystemWatcher 是我們的用户代碼 BuildConfiguration 搞的哈,這就極大的縮小的包圍圈,從而快速定位禍根。
四:總結
很多的內存碎片化往往都能看到 FileSystemWatcher 的身影,希望這篇的反思和總結能給大家帶來幫助。