You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET 6 Windows服务无法捕获并记录内存不足异常的问题排查

.NET 6 Windows服务内存不足崩溃无法捕获日志问题

问题背景

我将.NET 6应用程序作为Windows服务运行,启动命令如下:

"C:\Program Files (x86)\dotnet\dotnet.exe" c:\MyApp\MyApp.dll

应用启动代码:

var builder = WebApplication.CreateBuilder(args);
builder.UseSerilog();
// 配置其他内容
var app = builder.Build();
app.MapControllers();

try
{
    app.Run();
}
catch (Exception ex)
{
    app.Logger.LogError(ex, "error");
}

使用Serilog进行日志记录。近期服务在不同硬件设备上因第三方DLL重复打开/关闭操作导致内存泄漏,运行6-7小时后因**内存不足(Out of Memory)**崩溃。但崩溃时Serilog未记录任何错误,仅生成模糊的应用程序事件日志,Report.wer中仅能获取异常代码0xe0434352。

我尝试订阅AppDomain.CurrentDomain.UnhandledException事件,通过Serilog或系统事件日志记录错误,但实际崩溃场景下无效。相关处理代码:

// Handler 1
using (var eventLog = new EventLog("Application"))
{
    eventLog.Source = "Application";
    eventLog.WriteEntry($"Unhandled exception {(e.ExceptionObject as Exception)}", EventLogEntryType.Error, 1000);
}

// Handler 2 (via Serilog)
AppDomain.CurrentDomain.UnhandledException += (o, e) =>
{
    app.Logger.LogError((e.ExceptionObject as Exception), "Unhandled exception");
};

测试时用string value = new string('f', int.MaxValue);模拟内存不足异常,两种处理方式均能正常工作,但实际场景无效。请问遗漏了什么?是否需要特殊配置Serilog,或更换日志方案?


解决方案

1. 区分托管/非托管内存耗尽的差异

你模拟的OOM是托管代码抛出的OutOfMemoryException,CLR能正常触发UnhandledException事件;但实际崩溃是第三方DLL的非托管内存泄漏导致进程资源耗尽,此时CLR可能已无法正常执行托管代码(连日志写入的内存都无法分配),直接被系统终止,因此UnhandledException事件根本没机会触发。

2. 调整Serilog配置,强制同步写入关键日志

默认Serilog可能使用异步日志队列,OOM时队列无法处理或内存不足无法完成写入。需配置**审计日志(AuditTo)**强制同步写入,确保致命日志能落地:

Log.Logger = new LoggerConfiguration()
    .WriteTo.EventLog("MyAppService", manageEventSource: true)
    .AuditTo.File("D:\\CriticalLogs\\fatal-errors.txt", rollingInterval: RollingInterval.Day)
    .CreateLogger();

// 在UnhandledException事件中使用静态Log类,并强制刷新
AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
{
    Log.Fatal((Exception)e.ExceptionObject, "致命未处理异常");
    Log.CloseAndFlush(); // 强制清空日志缓冲区
};

3. 提前预留应急内存缓冲区

在应用启动时预分配一小块内存(如1MB),专门用于崩溃时的日志写入,避免OOM时连日志写入的内存都没有:

// 放在Program.cs最顶部,作为静态变量保留
private static byte[] _emergencyLogBuffer = new byte[1024 * 1024];

4. 注册事件的时机提前到应用初始化最早期

确保UnhandledException事件在WebApplication.CreateBuilder之前注册,避免因初始化顺序问题导致事件未被订阅:

// Program.cs入口第一行就注册
AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
{
    Log.Fatal((Exception)e.ExceptionObject, "未处理致命异常");
    Log.CloseAndFlush();
};

// 再初始化WebApplication
var builder = WebApplication.CreateBuilder(args);
builder.UseSerilog();
// 后续配置代码

5. 配置Windows原生崩溃转储

通过注册表配置Windows在服务崩溃时生成完整转储,直接分析崩溃现场,无需依赖应用日志:

  1. 打开注册表编辑器,定位到HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps
  2. 创建子项,名称为你的服务进程名(如dotnet.exe或自定义进程名)
  3. 在子项中添加以下DWORD值:
    • DumpType:设为2(完整转储)
    • DumpFolder:设为转储文件存储路径(如D:\CrashDumps)
    • DumpCount:设为保留的转储文件数量(如5)

6. 修复第三方DLL内存泄漏问题

使用工具监控非托管内存增长:

  • Process Explorer:查看进程的非托管内存(Private Bytes)变化
  • VMMap:分析进程的内存分布,定位泄漏的内存区域
    联系第三方DLL供应商修复泄漏,或优化代码中对该DLL的调用逻辑(如减少不必要的打开/关闭操作,复用实例)

内容的提问来源于stack exchange,提问作者InfernumDeus

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 05:48:29