一:背景

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 的內存,真是沒邊的事,截圖如下:

對 .NET FileSystemWatcher引發內存碎片化的 反思_碎片化

為了找出原因,上 windbg 附加,使用 !dumpheap -stat 觀察託管堆,截圖如下:

對 .NET FileSystemWatcher引發內存碎片化的 反思_碎片化_02

從卦中可以看到兩點信息:

  1. Free 獨佔 1.39G,這是經典的內存碎片化。
  2. FileSystemWatcher 高達 1290 個,表明程序存在大量的文件監控。

看到上面兩點信息,一定要有條件反射,是不是 reloadOnChange: true 導致的。

2. 是 reloadOnChange 導致的嗎

要想找到答案,可以深挖 Microsoft.Extensions.Configuration.ConfigurationRoot 類,即代碼 BuildConfiguration(); 的返回類型,為了方便可視化觀察,我用 vs 直接找下給大家看看,截圖如下:

對 .NET FileSystemWatcher引發內存碎片化的 反思_碎片化_03

有了這個脈絡,就可以使用 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 導致的,截圖如下:

對 .NET FileSystemWatcher引發內存碎片化的 反思_System_04

像這種情況可能就需要開啓追蹤了,可以藉助🐂👃的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);
        }
    }

對 .NET FileSystemWatcher引發內存碎片化的 反思_碎片化_05

從卦中可以看到,原來這個 FileSystemWatcher 是我們的用户代碼 BuildConfiguration 搞的哈,這就極大的縮小的包圍圈,從而快速定位禍根。

四:總結

很多的內存碎片化往往都能看到 FileSystemWatcher 的身影,希望這篇的反思和總結能給大家帶來幫助。

對 .NET FileSystemWatcher引發內存碎片化的 反思_ide_06