一:背景
1. 讲故事
前几周分析了一个 40G+ 大内存的dump,这个程序平时最多不到30G,但不知道为啥最近会涨到40G,所以让我帮忙分析下怎么回事,像这种大内存dump,如果用传统的方式分析将会是一场灾难,这篇就来详细的说一说,从 windbg 的最佳分析实践来看,一个dump最好不要超过10G,否则就会遇到
- dump跨机器分发慢。
- dump命令处理反馈慢,一个sos命令可能需要数小时,这个是常人无法承受的。
- 分析过程中容易引发机器内存不足告警,毕竟这种dump远大于 开发者的机器内存。
对于80%的场景都适合本条建议,但也有一些例外,比如有一些程序会使用大量缓存,所以内存常常维持在40G的高位,一旦此体量下的程序又出现了内存意外泄露,这种污水混在净水里,很难将其精准的摘出来。
二:超高内存分析方法
1. 一个简单的案例
由于电脑内存有限,我就以常规的 4G 到 6G 来给大家做个演示,代码的意思非常简单,平时4G是因为有3个缓存实例 MemoryCache,后来不知道什么原因在 _staticCache 中灌入了2G数据,导致了非人意的场景发生,完整的参考代码如下:- public class MemorySpikeSimulator
- {
- private static readonly MemoryCache _memoryCache1 = new MemoryCache("Cache1");
- private static readonly MemoryCache _memoryCache2 = new MemoryCache("Cache2");
- private static readonly MemoryCache _memoryCache3 = new MemoryCache("Cache3");
- private static readonly List<byte[]> _staticCache = new List<byte[]>();
- public static void Main()
- {
- Console.WriteLine("模拟内存增长测试...");
- Console.WriteLine("初始内存: " + FormatBytes(GC.GetTotalMemory(false)));
- // 模拟正常业务操作 - 三个MemoryCache共4GB
- SimulateNormalOperations();
- Console.WriteLine("正常操作后内存: " + FormatBytes(GC.GetTotalMemory(false)));
- Console.WriteLine("按Enter进行 意外内存 分配阶段... ");
- Console.ReadLine();
- // 模拟意外的大内存分配 - _staticCache增加2GB
- SimulateUnexpectedAllocation();
- Console.WriteLine("意外分配后内存: " + FormatBytes(GC.GetTotalMemory(false)));
- GC.Collect();
- GC.WaitForPendingFinalizers();
- Console.WriteLine("GC后内存: " + FormatBytes(GC.GetTotalMemory(true)));
- Console.WriteLine("按任意键退出...");
- Console.ReadKey();
- }
- private static void SimulateNormalOperations()
- {
- Console.WriteLine("开始正常内存分配 (三个MemoryCache共4GB)...");
- // 三个MemoryCache实例共同分配约4GB
- for (int i = 0; i < 350; i++) // 增加循环次数以达到4GB
- {
- var buffer1 = new byte[4 * 1024 * 1024]; // 4MB
- _memoryCache1.Add($"cache1_key_{i}", buffer1, DateTimeOffset.Now.AddHours(1));
- var buffer2 = new byte[4 * 1024 * 1024]; // 4MB
- _memoryCache2.Add($"cache2_key_{i}", buffer2, DateTimeOffset.Now.AddHours(1));
- var buffer3 = new byte[4 * 1024 * 1024]; // 4MB
- _memoryCache3.Add($"cache3_key_{i}", buffer3, DateTimeOffset.Now.AddHours(1));
- if (i % 50 == 0)
- {
- Console.WriteLine($"已分配: {(i + 1) * 12}MB");
- }
- Thread.Sleep(10); // 稍微减慢速度
- }
- }
- private static void SimulateUnexpectedAllocation()
- {
- Console.WriteLine("开始意外内存分配 (_staticCache增加2GB)...");
- // _staticCache意外增加约2GB
- for (int i = 0; i < 200; i++)
- {
- var unexpectedData = new byte[10 * 1024 * 1024]; // 10MB
- _staticCache.Add(unexpectedData);
- if (i % 20 == 0)
- {
- Console.WriteLine($"已分配: {(i + 1) * 10}MB");
- }
- Thread.Sleep(1);
- }
- }
- private static string FormatBytes(long bytes)
- {
- string[] suffixes = { "B", "KB", "MB", "GB", "TB" };
- int counter = 0;
- decimal number = bytes;
- while (Math.Round(number / 1024) >= 1)
- {
- number /= 1024;
- counter++;
- }
- return $"{number:n2} {suffixes[counter]}";
- }
- }
复制代码
2. 分析方法简述
对于一个超大内存的dump,使用常规直接抓dump的方式不是最优方案,所以先需要用微软提供的 vmmap 观察进程的内存地址段布局,看下是托管内存,NTHeap 还是 VirtualAlloc 的泄露,不同的泄露有不同的应灾方案,截图如下:
从卦中可以清晰的看到,总计 6.6G 的内存,托管堆就吃了 6.3G,所以这个问题就被定性为 托管内存泄露。
问题被定性之后,接下来在生产环境上 正常内存时段 和 异常内存时段 场景下各采1个dump,即 4G 和 6G 场景,这里稍微提醒下,采dump的方式相比dotmemory,perfview 附加进程方式的开销是最小的,dump采到之后,使用 perfview 的 Collect -> Take Heap Snapshot From Dump 或者将 dump 拖到 perfview 里,最终会构建出二个不到1M的 xxx.gcdump 文件,完整的截图如下:
文件有了之后接下来就是借助 perfview 的gcdump对比功能了,分别打开 xxx.dmp.gcdump 下的 Heap Stacks 子窗口,删除 GroupPats 框中的默认分组,接下来准备用 snapshot2 去对比 snapshot1,选择 Diff -> With Baseline xxx,截图如下:
在自动打开的新窗口中,可以很明显的看到增长的2G内存都是被 _staticCache 静态变量给吃掉了,截图如下:
到此真相大白,最后稍微提醒一下,如果发现是 ntheap 泄露,那就可以提前开启 ust 了。
三:总结
分析生产环境下的超大内存程序的故障,还是有一定的挑战的,大家也看到了这需要多工具的灵活运用,才能将不利影响降到最低。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |