对 .NET FileSystemWatcher引发内存碎片化的 反思

一:背景

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引发内存碎片化的 反思

从卦中可以看到两点信息:

  1. Free 独占 1.39G,这是经典的内存碎片化。
  2. FileSystemWatcher 高达 1290 个,表明程序存在大量的文件监控。

看到上面两点信息,一定要有条件反射,是不是 reloadOnChange: true 导致的。

2. 是 reloadOnChange 导致的吗

要想找到答案,可以深挖 Microsoft.Extensions.Configuration.ConfigurationRoot 类,即代码 BuildConfiguration(); 的返回类型,为了方便可视化观察,我用 vs 直接找下给大家看看,截图如下:

对 .NET FileSystemWatcher引发内存碎片化的 反思

有了这个脉络,就可以使用 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:travelssrcExampleExample_0_1binDebugnet8.0Microsoft.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 FilesdotnetsharedMicrosoft.NETCore.App8.0.22System.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:travelssrcExampleExample_0_1binDebugnet8.0Microsoft.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:travelssrcExampleExample_0_1binDebugnet8.0Microsoft.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引发内存碎片化的 反思

像这种情况可能就需要开启追踪了,可以借助🐂👃的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引发内存碎片化的 反思

从卦中可以看到,原来这个 FileSystemWatcher 是我们的用户代码 BuildConfiguration 搞的哈,这就极大的缩小的包围圈,从而快速定位祸根。

四:总结

很多的内存碎片化往往都能看到 FileSystemWatcher 的身影,希望这篇的反思和总结能给大家带来帮助。

对 .NET FileSystemWatcher引发内存碎片化的 反思

发表评论

评论已关闭。

相关文章

  • 0