1. 讲故事
前些天又遇到了一例 FileSystemWatcher 引发的内存碎片化故障,但这个碎片化不是因为经典的 reloadOnChange=true 导致的,所以我觉得有必要做一次深度的反思,供以后遇到类似问题提供技术上的解决方法,这篇我们就来系统的讲解下 两种碎片化方式的调查方法。
二:经典的 FileSystemWatcher 碎片化
1. 测试代码
这种碎片化是由 reloadOnChange=true 引发的,祸根主要是程序员将 .netframework 读取配置文件的方式套在了 .net 上,为了方便演示,先上一段测试代码。
1 2 internal class Program 3 { 4 static void Main(string[] args) 5 { 6 for (int i = 0; i < 100000; i++) 7 { 8 IConfiguration configuration = BuildConfiguration(); 9 string appName = configuration["AppName"]; 10 Console.WriteLine($"i={i} 应用名称: {appName}"); 11 } 12 13 Console.ReadLine(); 14 } 15 16 static IConfiguration BuildConfiguration() 17 { 18 return new ConfigurationBuilder() 19 .SetBasePath(Directory.GetCurrentDirectory()) 20 .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) 21 .Build(); 22 } 23 } 24
卦中的代码非常简单,就是每次读取 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 的铁证,参考如下:
1 20:008> !dumpobj /d 17dd2f41fa0 3Name: Microsoft.Extensions.Configuration.ConfigurationRoot 4MethodTable: 00007ff9d8707a48 5EEClass: 00007ff9d86e97b0 6Tracked Type: false 7Size: 40(0x28) bytes 8File: D:\travels\src\Example\Example_0_1\bin\Debug\net8.0\Microsoft.Extensions.Configuration.dll 9Fields: 10 MT Field Offset Type VT Attr Value Name 1100007ff9d8706c48 4000016 8 ...on.Abstractions]] 0 instance 0000017dd2f3e520 _providers 1200007ff9d880ba28 4000017 10 ...Private.CoreLib]] 0 instance 0000017dd2f42018 _changeTokenRegistrations 1300007ff9d8708940 4000018 18 ...rationReloadToken 0 instance 0000017dd2f41fc8 _changeToken 140:008> !DumpObj /d 0000017dd2f3e520 15Name: System.Collections.Generic.List`1[[Microsoft.Extensions.Configuration.IConfigurationProvider, Microsoft.Extensions.Configuration.Abstractions]] 16MethodTable: 00007ff9d87069d0 17EEClass: 00007ff9d86a10f8 18Tracked Type: false 19Size: 32(0x20) bytes 20File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.22\System.Private.CoreLib.dll 21Fields: 22 MT Field Offset Type VT Attr Value Name 2300007ff9d891d1a0 400226e 8 System.__Canon[] 0 instance 0000017dd2f41f68 _items 2400007ff9d8551188 400226f 10 System.Int32 1 instance 1 _size 2500007ff9d8551188 4002270 14 System.Int32 1 instance 1 _version 2600007ff9d891d1a0 4002271 8 System.__Canon[] 0 static dynamic statics NYI s_emptyArray 270:008> !DumpArray /d 0000017dd2f41f68 28Name: Microsoft.Extensions.Configuration.IConfigurationProvider[] 29MethodTable: 00007ff9d8707cf0 30EEClass: 00007ff9d851c440 31Size: 56(0x38) bytes 32Array: Rank 1, Number of elements 4, Type CLASS 33Element Methodtable: 00007ff9d8706938 34[0] 0000017dd2f3e540 35[1] null 36[2] null 37[3] null 380:008> !DumpObj /d 0000017dd2f3e540 39Name: Microsoft.Extensions.Configuration.Json.JsonConfigurationProvider 40MethodTable: 00007ff9d8708200 41EEClass: 00007ff9d86e9ab8 42Tracked Type: false 43Size: 48(0x30) bytes 44File: D:\travels\src\Example\Example_0_1\bin\Debug\net8.0\Microsoft.Extensions.Configuration.Json.dll 45Fields: 46 MT Field Offset Type VT Attr Value Name 4700007ff9d8708940 4000012 8 ...rationReloadToken 0 instance 0000017dd2f437e0 _reloadToken 4800007ff9d8708cf0 4000013 10 ...Private.CoreLib]] 0 instance 0000017dd2f42298 <Data>k__BackingF https://www.jiwenlaw.com/ield 4900007ff9d8662820 4000005 18 System.IDisposable 0 instance 0000017dd2f3e690 _changeTokenRegistration 5000007ff9d8701b98 4000006 20 ...nfigurationSource 0 instance 0000017dd2f3e4b8 <Source>k__BackingField 510:008> !DumpObj /d 0000017dd2f3e4b8 52Name: Microsoft.Extensions.Configuration.Json.JsonConfigurationSource 53MethodTable: 00007ff9d8701c88 54EEClass: 00007ff9d86e7868 55Tracked Type: false 56Size: 48(0x30) bytes 57File: D:\travels\src\Example\Example_0_1\bin\Debug\net8.0\Microsoft.Extensions.Configuration.Json.dll 58Fields: 59 MT Field Offset Type VT Attr Value Name 6000007ff9d86d8188 4000007 8 ...ers.IFileProvider 0 instance 0000017dd2f3e230 <FileProvider>k__BackingField 6100007ff9d85cec08 4000008 10 System.String 0 instance 0000017d00100510 <Path>k__BackingField 6200007ff9d851d070 4000009 24 System.Boolean 1 instance 0 <Optional>k__BackingField 6300007ff9d851d070 400000a 25 System.Boolean 1 instance 1 <ReloadOnChange>k__BackingField 6400007ff9d8551188 400000b 20 System.Int32 1 instance 250 <ReloadDelay>k__BackingField 6500007ff9d8708420 400000c 18 ....FileExtensions]] 0 instance 0000000000000000 <OnLoadException>k__BackingField 66
三:非经典的 FileSystemWatcher 碎片化
1. 测试代码
有的时候会出现 FileSystemWatcher 很少,但 overlapped 很多的情况,这种情况很大概率不是 reloadOnChange: true 导致的,截图如下:
像这种情况可能就需要开启追踪了,可以借助🐂👃的harmony 搞定,那如何做呢?可以钩住 FileSystemWatcher 的所有构造函数,通过记录调用栈来观察到底是什么代码调用的,从而寻找祸根,参考代码如下:
1 2 internal class Program 3 { 4 static void Main(string[] args) 5 { 6 var harmony = new Harmony("com.example.fswatcher"); 7 harmony.PatchAll(); 8 9 for (int i = 0; i < 5; i++) 10 { 11 IConfiguration configuration = BuildConfiguration(); 12 string appName = configuration["AppName"]; 13 Console.WriteLine($"i={i} 应用名称: {appName}"); 14 } 15 16 Console.ReadLine(https://www.jiwenlaw.com/ ); 17 } 18 19 static IConfiguration BuildConfiguration() 20 { 21 return new ConfigurationBuilder() 22 .SetBasePath(Directory.GetCurrentDirectory()) 23 .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) 24 .Build(); 25 } 26 } 27 28 [HarmonyPatch] 29 public class FileSystemWatcherConstructorsPatch 30 { 31 [HarmonyTargetMethod] 32 static IEnumerable<MethodBase> TargetMethods() 33 { 34 // 一次性获取所有公共实例构造函数 35 return typeof(FileSystemWatcher).GetConstructors(BindingFlags.Public | BindingFlags.Instance); 36 } 37 38 [HarmonyPostfix] 39 public static void Postfix(FileSystemWatcher __instance) 40 { 41 Console.WriteLine($"[Harmony] FileSystemWatcher 构造函数被调用"); 42 Console.WriteLine($"[Harmony] 路径: https://www.jiwenlaw.com/ '{__instance.Path ?? "null"}', 过滤器: '{__instance.Filter ?? "null"}'"); 43 Console.WriteLine($"[Harmony] 调用栈:"); 44 Console.WriteLine(Environment.StackTrace); 45 } 46 } 47
从卦中可以看到,原来这个 FileSystemWatcher 是我们的用户代码 BuildConfiguration 搞的哈,这就极大的缩小的包围圈,从而快速定位祸根。
四:总结
很多的内存碎片化往往都能看到 FileSystemWatcher 的身影,希望这篇的反思和总结能给大家带来帮助。
