ASP.NET Core如何在服务关闭前或发生致命错误时执行自定义处理
IHostApplicationLifetime 确实无法满足致命错误崩溃的捕获需求:这个接口的所有回调仅在Host执行正常优雅关停流程时触发,未处理异常打崩进程、非托管错误导致进程直接退出、进程被外部强制终止这类场景下,Host根本走不完关停流程,对应的回调完全不会执行。要实现崩溃时的错误日志记录和落库,需要做三层兜底覆盖所有可能的崩溃场景:
1. 托管层未处理异常捕获
这层覆盖90%以上托管代码导致的崩溃场景,所有逻辑必须写在Program.cs的最顶部(早于Host构建逻辑,不要等WebApplication实例化后再注册):
- 注册
AppDomain.CurrentDomain.UnhandledException事件,捕获当前AppDomain内所有线程(包括后台线程、线程池线程)抛出的未被处理的托管异常,事件参数里的IsTerminating字段为true时就代表进程即将崩溃退出。 - 注册
TaskScheduler.UnobservedTaskException事件,捕获所有未被await、也未注册异常处理的Task抛出的异常,这类异常在特定配置下也会直接导致进程崩溃。 - 两个事件的回调逻辑不要做复杂异步操作,尽量用短超时的同步逻辑执行落库,避免进程提前退出导致操作没完成;必须加本地文件日志兜底,防止崩溃时数据库连接失效导致日志丢失。
示例代码:
// 放在Program.cs第一行,所有其他逻辑之前 AppDomain.CurrentDomain.UnhandledException += (sender, e) => { var exception = e.ExceptionObject as Exception; // 本地文件兜底写入 var logContent = $"[{DateTimeOffset.Now:O}] [Terminating:{e.IsTerminating}] {exception}{Environment.NewLine}"; File.AppendAllText("./critical_fail.log", logContent); // 短超时同步落库,不要走DI获取DbContext,直接用最小化实例 try { using var db = new YourServiceDbContext(); db.CriticalErrorLogs.Add(new CriticalErrorLog { OccurredAt = DateTimeOffset.Now, IsProcessTerminating = e.IsTerminating, Message = exception?.Message ?? "Unknown fatal error", FullException = exception?.ToString() }); db.Database.SetCommandTimeout(2); // 设2秒超时,不要卡太久 db.SaveChanges(); } catch { /* 落库失败已经有本地日志兜底,不用再抛异常 */ } }; TaskScheduler.UnobservedTaskException += (sender, e) => { var exception = e.Exception; // 复用上面的日志写入逻辑即可 File.AppendAllText("./critical_fail.log", $"[{DateTimeOffset.Now:O}] [UnobservedTask] {exception}{Environment.NewLine}"); e.SetObserved(); // 标记异常已处理,避免不必要的进程崩溃 }; // WebApplication构建完成后,第一时间加全局异常中间件,拦截请求管道内的异常,避免异常冒泡到进程崩溃级别 var app = builder.Build(); app.UseExceptionHandler(errApp => { errApp.Run(async context => { var exHandler = context.Features.Get<IExceptionHandlerFeature>(); if (exHandler?.Error != null) { // 这里可以复用日志逻辑记录请求级异常,这类异常一般不会直接导致进程崩溃 } context.Response.StatusCode = 500; await context.Response.WriteAsync("Service internal error"); }); });
2. 非托管/CLR级崩溃捕获
如果服务依赖非托管组件、出现栈溢出、内存访问违规这类CLR本身已经无法稳定执行托管代码的致命错误,上面的托管层事件是不会触发的,需要做系统级的捕获:
- Windows环境可通过Win32 API
SetUnhandledExceptionFilter注册非托管异常回调,Linux环境可通过sigaction注册SIGSEGV、SIGABRT、SIGILL这类致命信号的处理回调。 - 注意这类场景下不要尝试在回调里执行任何托管代码(包括C#写的数据库写入逻辑),回调里仅做最小化操作:把崩溃码、崩溃地址、线程上下文这类基础信息写入固定路径的本地文件或者预分配的内存映射文件,等服务下次重启时,由新启动的进程读取这些崩溃信息补录到数据库。
- 生产环境建议开启系统级dump生成:Windows开WER、Linux开core dump,把崩溃时的完整进程镜像存到固定路径,后续排查问题时信息比单纯的文本日志全很多。
3. 进程外监控兜底
没有任何应用内捕获机制能覆盖100%的崩溃场景,比如进程被操作系统强制OOM Kill、服务器掉电、CLR完全死锁无响应这类情况,应用内的所有逻辑都不会执行,必须加进程外的兜底:
- 用systemd(Linux)、Windows Service(Windows)或者容器编排平台(K8s、Docker)配置服务保活和自动重启策略,同时记录服务的退出码、退出时间。
- 部署独立的轻量监控探针,定期检测服务的存活端口和健康检查接口,一旦检测到服务异常退出,探针负责把崩溃事件写入数据库,同时触发告警通知。
关键注意点
- 崩溃回调里的逻辑越简单越好,不要依赖DI容器、不要调用已经被释放的服务,尽量用静态单例的客户端或者直接new最小化的依赖实例,避免回调本身抛出二次异常。
- 本地文件日志是必须的兜底,崩溃时网络、数据库连接大概率处于异常状态,先把日志落本地磁盘,后续再补录到数据库,不要强依赖实时落库。
- 不要在回调里主动调用
Environment.Exit(),给日志写入留1-2秒的缓冲时间即可。
内容的提问来源于stack exchange,提问作者Anna Melashkina
相关产品推荐
相关产品推荐

