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

一:背景

1. 讲故事

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

  • dump跨机器分发慢。
  • dump命令处理反馈慢,一个sos命令可能需要数小时,这个是常人无法承受的。
  • 分析过程中容易引发机器内存不足告警,毕竟这种dump远大于 开发者的机器内存。

对于80%的场景都适合本条建议,但也有一些例外,比如有一些程序会使用大量缓存,所以内存常常维持在40G的高位,一旦此体量下的程序又出现了内存意外泄露,这种污水混在净水里,很难将其精准的摘出来。

二:超高内存分析方法

1. 一个简单的案例

由于电脑内存有限,我就以常规的 4G6G 来给大家做个演示,代码的意思非常简单,平时4G是因为有3个缓存实例 MemoryCache,后来不知道什么原因在 _staticCache 中灌入了2G数据,导致了非人意的场景发生,完整的参考代码如下:

C# 复制代码
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 了。

三:总结

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