c# 在高并发服务中,如何做优雅停机(Graceful Shutdown)

来源:这里教程网 时间:2026-02-21 17:38:44 作者:

什么是
GracefulShutdown
在 C# 服务中的真实含义

它不是简单调用

Environment.Exit()
或杀进程,而是:等正在处理的请求完成、拒绝新请求、释放资源(如数据库连接、HttpClient 实例、后台任务)、再退出。在 ASP.NET Core 中,这由
IHostApplicationLifetime
IHostedService
协同控制;在裸
BackgroundService
或自托管场景中,需手动监听取消信号。

ASP.NET Core Web API 如何注册优雅停机逻辑

核心是利用

IHostApplicationLifetime
ApplicationStopping
取消令牌,在其触发时做清理,同时确保中间件/控制器不接收新请求。关键点在于:HTTP 服务器(Kestrel)默认已支持 graceful shutdown,但你的业务逻辑必须响应取消信号。

Program.cs
中注入
IHostApplicationLifetime
,并在
ApplicationStopping
回调里执行耗时清理(如等待队列清空、关闭长连接)
所有异步操作(如
await dbContext.SaveChangesAsync()
await httpClient.PostAsync()
)必须传入
ApplicationStopping.Token
避免在
ApplicationStopping
中阻塞主线程(如用
.Wait()
),应
await
异步清理
Kestrel 默认有 5 秒超时(可通过
WebHostOptions.ShutdownTimeout
调整),超时后强制终止 —— 所以清理逻辑务必可控
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHostedService<MyBackgroundWorker>();
<p>var app = builder.Build();
app.MapGet("/health", () => "ok");</p><p>// 注册停机钩子
app.Lifetime.ApplicationStopping.Register(() =>
{
Console.WriteLine("Shutting down gracefully...");
// 这里可 await,但注意:ApplicationStopping.Token 已触发,不可再用于新操作
// 推荐把清理逻辑封装进 async 方法,并用 Task.Run + await 配合 Token
_ = Task.Run(async () =>
{
await MyCleanupAsync(app.Lifetime.ApplicationStopping.Token);
});
});</p><p>await app.RunAsync();

BackgroundService 中如何正确响应停机信号

BackgroundService
本身已内置对
CancellationToken
的支持,但常见错误是忽略
StopAsync
的超时约束或未取消内部循环。

ExecuteAsync
必须接收并传递
cancellationToken
给所有
await
操作
StopAsync
中不能只调用
cts.Cancel()
就结束,要
await
等待工作真正退出(比如等待队列消费完毕)
若内部使用
Task.Delay(1000, token)
做轮询,必须确保 token 来自
stoppingToken
,而非新创建的
CancellationTokenSource
不要在
StopAsync
中执行同步 I/O(如写文件、发 HTTP),除非加超时保护,否则可能拖垮整个停机流程
public class MyBackgroundWorker : BackgroundService
{
    private readonly ILogger<MyBackgroundWorker> _logger;
<pre class='brush:php;toolbar:false;'>public MyBackgroundWorker(ILogger<MyBackgroundWorker> logger)
{
    _logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    while (!stoppingToken.IsCancellationRequested)
    {
        try
        {
            await DoWorkAsync(stoppingToken);
            await Task.Delay(5000, stoppingToken); // 注意:token 来自 ExecuteAsync 参数
        }
        catch (OperationCanceledException)
        {
            break;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error in background worker");
        }
    }
}
private async Task DoWorkAsync(CancellationToken ct)
{
    // 所有 await 必须传入 ct
    await _httpClient.GetAsync("https://api.example.com/health", ct);
}
public override async Task StopAsync(CancellationToken stoppingToken)
{
    _logger.LogInformation("Stopping background worker...");
    // 等待当前轮次完成,但不超过 stoppingToken 超时
    await base.StopAsync(stoppingToken);
}

}

容易被忽略的三个破坏点

哪怕代码写了

await ... CancellationToken
,仍可能因以下原因导致“假优雅”:

HttpClient
实例未复用且未设置
Timeout
:单次请求卡死会阻塞整个停机流程;建议统一用
IHttpClientFactory
并配置
DefaultRequestTimeout
数据库连接池未释放:EF Core 的
SaveChangesAsync
若没传 token,可能永远挂住;
DbContext
应注册为
Scoped
,并在
StopAsync
后显式调用
DisposeAsync()
第三方 SDK(如 Redis client、RabbitMQ consumer)未提供 cancellation 支持:必须查阅文档确认其
StopAsync
是否真正等待消息 ACK 完成,否则可能丢消息

最危险的是:你以为停机成功了,其实某个

Task.Run(() => { Thread.Sleep(30000); })
还在跑 —— 它完全脱离取消体系,只能靠超时强杀。

相关推荐