kestrel 默认支持优雅停机但仅等待5秒,业务代码必须主动响应applicationstopping令牌并传递cancellationtoken,否则请求被强制中断、资源泄漏。

直接说结论:ASP.NET Core 的 Kestrel 服务器默认已支持优雅停机,但你的业务代码必须主动响应 ApplicationStopping 令牌,否则请求会超时被强制切断,资源照样泄漏。
为什么 Kestrel 默认不“真正等完”所有请求?
Kestrel 在收到系统终止信号(如 Ctrl+C、kill -15)后,会立即停止接受新连接,并开始等待正在处理的请求完成。但它不会无限等待——默认超时是 5 秒(由 WebHostOptions.ShutdownTimeout 控制)。超时一到,未完成的请求会被中止,进程强制退出。
- 这个 5 秒不是“留给你的清理时间”,而是“Kestrel 自身关闭连接池 + 等待 HTTP 响应写出”的窗口
- 你自己的数据库保存、文件写入、第三方 API 调用等耗时操作,必须在这个窗口内完成,否则会被砍断
- 如果你在
ApplicationStopping.Register()里用了.Wait()或阻塞调用,会卡住整个 shutdown 流程,导致超时提前触发
BackgroundService 中必须重写 StopAsync 并传入 CancellationToken
BackgroundService 类本身已封装了生命周期协调逻辑,但常见错误是忽略 StopAsync 的参数或未正确 await 内部任务。
- 不要在
StopAsync里写while (_running) { ... }这类轮询逻辑,而应监听cancellationToken.IsCancellationRequested - 所有异步操作(如
await _queueReader.ReadAsync(token)、await _dbContext.SaveChangesAsync(token))必须把传入的cancellationToken向下传递 - 如果内部有长循环,每轮迭代开头加
token.ThrowIfCancellationRequested(),避免卡死 - 不要在
StopAsync中调用Task.Wait()或Result,这会阻塞线程,违反 async/await 契约
中间件和控制器也要配合取消信号
即使 Kestrel 在 shutdown 阶段不再接受新请求,已进入 pipeline 的请求仍会继续执行——除非你主动检查并退出。
- 在关键中间件(如日志、事务、限流)中,用
HttpContext.RequestAborted替代全局ApplicationStopping.Token,它专为当前请求生命周期设计 - 控制器 Action 方法签名可加
CancellationToken cancellationToken参数,ASP.NET Core 会自动注入当前请求的取消令牌 - 若你在 Action 里调用
HttpClient.SendAsync()或 EF Core 查询,务必把该cancellationToken传进去,否则请求可能挂住直到超时 - 避免在 Action 中启动后台
Task.Run却不跟踪其生命周期——这些任务不会被 shutdown 机制感知
调试时最容易被忽略的三个点
优雅停机失效,往往不是框架没起作用,而是这几个地方漏掉了。
-
IHostedService.StartAsync中启动的后台线程,如果没把stoppingToken传进去,或没在循环里检查IsCancellationRequested,它会一直跑下去 - 手动创建的
HttpClient实例未设置Timeout,导致 shutdown 时卡在 DNS 解析或连接建立阶段 - 使用了
Task.Run(() => { while(true) {...} })却没传任何 token,这种“裸线程”完全游离于 .NET 生命周期之外,shutdown 时只能靠超时硬杀











