找回密码
 立即注册
首页 业界区 业界 聊一聊 .NET超高内存故障分析方法 的反思 ...

聊一聊 .NET超高内存故障分析方法 的反思

轮达 2025-10-1 17:57:21
一:背景

1. 讲故事

前几周分析了一个 40G+ 大内存的dump,这个程序平时最多不到30G,但不知道为啥最近会涨到40G,所以让我帮忙分析下怎么回事,像这种大内存dump,如果用传统的方式分析将会是一场灾难,这篇就来详细的说一说,从 windbg 的最佳分析实践来看,一个dump最好不要超过10G,否则就会遇到

  • dump跨机器分发慢。
  • dump命令处理反馈慢,一个sos命令可能需要数小时,这个是常人无法承受的。
  • 分析过程中容易引发机器内存不足告警,毕竟这种dump远大于 开发者的机器内存。
对于80%的场景都适合本条建议,但也有一些例外,比如有一些程序会使用大量缓存,所以内存常常维持在40G的高位,一旦此体量下的程序又出现了内存意外泄露,这种污水混在净水里,很难将其精准的摘出来。
二:超高内存分析方法

1. 一个简单的案例

由于电脑内存有限,我就以常规的 4G 到 6G 来给大家做个演示,代码的意思非常简单,平时4G是因为有3个缓存实例 MemoryCache,后来不知道什么原因在 _staticCache 中灌入了2G数据,导致了非人意的场景发生,完整的参考代码如下:
  1. public class MemorySpikeSimulator
  2. {
  3.     private static readonly MemoryCache _memoryCache1 = new MemoryCache("Cache1");
  4.     private static readonly MemoryCache _memoryCache2 = new MemoryCache("Cache2");
  5.     private static readonly MemoryCache _memoryCache3 = new MemoryCache("Cache3");
  6.     private static readonly List<byte[]> _staticCache = new List<byte[]>();
  7.     public static void Main()
  8.     {
  9.         Console.WriteLine("模拟内存增长测试...");
  10.         Console.WriteLine("初始内存: " + FormatBytes(GC.GetTotalMemory(false)));
  11.         // 模拟正常业务操作 - 三个MemoryCache共4GB
  12.         SimulateNormalOperations();
  13.         Console.WriteLine("正常操作后内存: " + FormatBytes(GC.GetTotalMemory(false)));
  14.         Console.WriteLine("按Enter进行 意外内存 分配阶段... ");
  15.         Console.ReadLine();
  16.         // 模拟意外的大内存分配 - _staticCache增加2GB
  17.         SimulateUnexpectedAllocation();
  18.         Console.WriteLine("意外分配后内存: " + FormatBytes(GC.GetTotalMemory(false)));
  19.         GC.Collect();
  20.         GC.WaitForPendingFinalizers();
  21.         Console.WriteLine("GC后内存: " + FormatBytes(GC.GetTotalMemory(true)));
  22.         Console.WriteLine("按任意键退出...");
  23.         Console.ReadKey();
  24.     }
  25.     private static void SimulateNormalOperations()
  26.     {
  27.         Console.WriteLine("开始正常内存分配 (三个MemoryCache共4GB)...");
  28.         // 三个MemoryCache实例共同分配约4GB
  29.         for (int i = 0; i < 350; i++) // 增加循环次数以达到4GB
  30.         {
  31.             var buffer1 = new byte[4 * 1024 * 1024]; // 4MB
  32.             _memoryCache1.Add($"cache1_key_{i}", buffer1, DateTimeOffset.Now.AddHours(1));
  33.             var buffer2 = new byte[4 * 1024 * 1024]; // 4MB
  34.             _memoryCache2.Add($"cache2_key_{i}", buffer2, DateTimeOffset.Now.AddHours(1));
  35.             var buffer3 = new byte[4 * 1024 * 1024]; // 4MB
  36.             _memoryCache3.Add($"cache3_key_{i}", buffer3, DateTimeOffset.Now.AddHours(1));
  37.             if (i % 50 == 0)
  38.             {
  39.                 Console.WriteLine($"已分配: {(i + 1) * 12}MB");
  40.             }
  41.             Thread.Sleep(10); // 稍微减慢速度
  42.         }
  43.     }
  44.     private static void SimulateUnexpectedAllocation()
  45.     {
  46.         Console.WriteLine("开始意外内存分配 (_staticCache增加2GB)...");
  47.         // _staticCache意外增加约2GB
  48.         for (int i = 0; i < 200; i++)
  49.         {
  50.             var unexpectedData = new byte[10 * 1024 * 1024]; // 10MB
  51.             _staticCache.Add(unexpectedData);
  52.             if (i % 20 == 0)
  53.             {
  54.                 Console.WriteLine($"已分配: {(i + 1) * 10}MB");
  55.             }
  56.             Thread.Sleep(1);
  57.         }
  58.     }
  59.     private static string FormatBytes(long bytes)
  60.     {
  61.         string[] suffixes = { "B", "KB", "MB", "GB", "TB" };
  62.         int counter = 0;
  63.         decimal number = bytes;
  64.         while (Math.Round(number / 1024) >= 1)
  65.         {
  66.             number /= 1024;
  67.             counter++;
  68.         }
  69.         return $"{number:n2} {suffixes[counter]}";
  70.     }
  71. }
复制代码

2. 分析方法简述

对于一个超大内存的dump,使用常规直接抓dump的方式不是最优方案,所以先需要用微软提供的 vmmap 观察进程的内存地址段布局,看下是托管内存,NTHeap 还是 VirtualAlloc 的泄露,不同的泄露有不同的应灾方案,截图如下:
2.png

从卦中可以清晰的看到,总计 6.6G 的内存,托管堆就吃了 6.3G,所以这个问题就被定性为 托管内存泄露。
问题被定性之后,接下来在生产环境上 正常内存时段 和 异常内存时段 场景下各采1个dump,即 4G 和 6G 场景,这里稍微提醒下,采dump的方式相比dotmemory,perfview 附加进程方式的开销是最小的,dump采到之后,使用 perfview 的 Collect -> Take Heap Snapshot From Dump 或者将 dump 拖到 perfview 里,最终会构建出二个不到1M的 xxx.gcdump 文件,完整的截图如下:
3.png

文件有了之后接下来就是借助 perfview 的gcdump对比功能了,分别打开 xxx.dmp.gcdump  下的 Heap Stacks 子窗口,删除 GroupPats 框中的默认分组,接下来准备用 snapshot2 去对比 snapshot1,选择 Diff -> With Baseline xxx,截图如下:
4.png

在自动打开的新窗口中,可以很明显的看到增长的2G内存都是被 _staticCache 静态变量给吃掉了,截图如下:
5.png

到此真相大白,最后稍微提醒一下,如果发现是 ntheap 泄露,那就可以提前开启 ust 了。
三:总结

分析生产环境下的超大内存程序的故障,还是有一定的挑战的,大家也看到了这需要多工具的灵活运用,才能将不利影响降到最低。
6.jpeg

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册