.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在服务崩溃时生成完整转储,直接分析崩溃现场,无需依赖应用日志:
- 打开注册表编辑器,定位到
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps - 创建子项,名称为你的服务进程名(如
dotnet.exe或自定义进程名) - 在子项中添加以下DWORD值:
DumpType:设为2(完整转储)DumpFolder:设为转储文件存储路径(如D:\CrashDumps)DumpCount:设为保留的转储文件数量(如5)
6. 修复第三方DLL内存泄漏问题
使用工具监控非托管内存增长:
- Process Explorer:查看进程的非托管内存(Private Bytes)变化
- VMMap:分析进程的内存分布,定位泄漏的内存区域
联系第三方DLL供应商修复泄漏,或优化代码中对该DLL的调用逻辑(如减少不必要的打开/关闭操作,复用实例)
内容的提问来源于stack exchange,提问作者InfernumDeus

