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

B站影视 港台电影 2025-09-19 08:39 1

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

1. 讲故事

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

dump跨机器分发慢。

dump命令处理反馈慢,一个sos命令可能需要数小时,这个是常人无法承受的。

分析过程中容易引发机器内存不足告警,毕竟这种dump远大于 开发者的机器内存。

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

由于电脑内存有限,我就以常规的4G到6G来给大家做个演示,代码的意思非常简单,平时4G是因为有3个缓存实例 MemoryCache,后来不知道什么原因在 _staticCache 中灌入了2G数据,导致了非人意的场景发生,完整的参考代码如下:
public classMemorySpikeSimulator
{
privatestaticreadonly MemoryCache _memoryCache1 = new MemoryCache("Cache1");
privatestaticreadonly MemoryCache _memoryCache2 = new MemoryCache("Cache2");
privatestaticreadonly MemoryCache _memoryCache3 = new MemoryCache("Cache3");

privatestaticreadonly LisTByte> _staticCache = new Listbyte>;

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 = newbyte[4 * 1024 * 1024]; // 4MB
_memoryCache1.Add($"cache1_key_{i}", buffer1, DateTimeOffset.Now.AddHours(1));
var buffer2 = newbyte[4 * 1024 * 1024]; // 4MB
_memoryCache2.Add($"cache2_key_{i}", buffer2, DateTimeOffset.Now.AddHours(1));
var buffer3 = newbyte[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 = newbyte[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,选择在自动打开的新窗口中,可以很明显的看到增长的2G内存都是被_staticCache静态变量给吃掉了,截图如下:

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

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

来源:opendotnet

相关推荐