
在ASP.NET Core应用中集成健康检查与优雅终止实现高可用性
大家好,我是源码库的博主。在微服务和云原生架构大行其道的今天,应用的高可用性(High Availability)不再是可选项,而是生存的基石。你是否遇到过服务发布时,正在处理的请求被粗暴中断,导致用户看到错误?或者负载均衡器将流量导向了一个实际上已经“半死不活”的服务实例?这些问题,正是我们今天要解决的。通过为ASP.NET Core应用集成健康检查(Health Checks)和优雅终止(Graceful Shutdown),我们可以让应用更健壮、更可靠。这篇文章,我将结合自己的实战经验,带你一步步实现这两个核心功能,并分享一些我踩过的“坑”。
为什么需要健康检查与优雅终止?
在深入代码之前,我们先聊聊“为什么”。在Kubernetes或Docker Swarm这样的编排系统中,健康检查端点(通常是 `/health` 或 `/healthz`)是基础设施判断应用实例是否健康的唯一标准。如果检查失败,编排器会重启容器或将其从负载均衡池中移除。
而优雅终止,则是应对“计划内停机”的关键。当应用需要更新、缩容或重启时,操作系统会向进程发送终止信号(如SIGTERM)。如果没有优雅终止,Web服务器会立刻停止,所有正在执行的请求(特别是数据库事务、文件写入等)都会被迫中断,导致数据不一致或用户端报错。优雅终止给了应用一个“缓冲期”,在这段时间内,应用停止接收新请求,但会全力处理完已接收的请求,然后再安全退出。
下面,我们就从健康检查开始。
第一步:集成基础健康检查
ASP.NET Core非常贴心地内置了健康检查中间件,让我们从最简单的“存活检查”开始。首先,我们需要在项目中安装必要的NuGet包。对于基础检查,通常只需要 `Microsoft.AspNetCore.Diagnostics.HealthChecks`,它通常已包含在元包中。
打开 `Program.cs` 文件,添加健康检查服务并配置中间件。
var builder = WebApplication.CreateBuilder(args);
// 1. 添加健康检查服务
builder.Services.AddHealthChecks();
var app = builder.Build();
// 2. 配置中间件
app.MapHealthChecks("/health"); // 映射健康检查端点
app.MapGet("/", () => "Hello World!");
app.Run();
就这么简单!启动应用,访问 `https://localhost:your-port/health`,你会看到一个返回 `Healthy` 状态的JSON响应。这个端点告诉外界:“我还活着!”
踩坑提示:在生产环境中,请务必为 `/health` 这类管理端点设置独立的认证或网络策略(如仅限内网访问),避免将其暴露给公网,以防泄露内部信息。
第二步:实现就绪检查与自定义检查
仅有“存活”检查是不够的。一个应用进程可能正在运行(存活),但可能因为数据库连接失败而无法正常工作。这就需要“就绪检查”。我们还可以创建自定义检查。
让我们模拟一个场景:检查数据库连接和某个关键外部API的健康状况。
首先,安装一个常用的扩展包来检查SQL Server数据库:
dotnet add package AspNetCore.HealthChecks.SqlServer
然后,更新 `Program.cs`:
builder.Services.AddHealthChecks()
// 添加存活检查(一个简单的示例)
.AddCheck("self", () => HealthCheckResult.Healthy(), tags: new[] { "live" })
// 添加SQL Server就绪检查
.AddSqlServer(
connectionString: builder.Configuration.GetConnectionString("DefaultConnection"),
name: "sql",
failureStatus: HealthStatus.Unhealthy,
tags: new[] { "ready", "db" }
)
// 添加一个模拟的外部API检查(自定义逻辑)
.AddUrlGroup(
new Uri("https://api.example.com/health"),
name: "external-api",
failureStatus: HealthStatus.Degraded, // 外部API失败可能只是功能降级,不一定是完全不可用
tags: new[] { "ready", "services" }
);
现在,我们可以创建不同的端点来区分“存活”和“就绪”:
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("live") // 只检查标记为“live”的检查项
});
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready") // 只检查标记为“ready”的检查项
});
// 默认的 /health 可以检查所有项
app.MapHealthChecks("/health");
这样,Kubernetes的“存活探针”可以配置为 `/health/live`,而“就绪探针”配置为 `/health/ready`。只有当就绪检查通过时,流量才会被导入该Pod。
第三步:实现应用的优雅终止
接下来是重头戏——优雅终止。ASP.NET Core的通用主机(IHost)已经内置了支持,我们只需要正确配置和利用它。
核心是使用 `IHostApplicationLifetime` 接口来监听应用程序生命周期事件。我们通常在 `Program.cs` 的 `app.Run()` 之前配置。
// 获取生命周期对象
var lifetime = app.Services.GetRequiredService();
// 监听终止信号
lifetime.ApplicationStopping.Register(() =>
{
Console.WriteLine("应用程序正在停止... 开始执行清理工作。");
// 在这里执行清理逻辑,例如:
// 1. 关闭数据库连接池
// 2. 停止后台任务或计时器
// 3. 刷新未写入的日志
// 4. 通知服务注册中心(如Consul)进行注销
// 模拟一个耗时清理操作
Thread.Sleep(5000);
Console.WriteLine("清理工作完成。");
});
// 配置主机的优雅关闭超时时间(默认为5秒)
builder.Host.ConfigureHostOptions(options =>
{
options.ShutdownTimeout = TimeSpan.FromSeconds(10); // 设置为10秒
});
app.Run();
但是,仅仅这样还不够。上面的代码处理了“停止中”事件,但Web服务器(Kestrel)可能还在接收请求。我们需要结合 `IApplicationLifetime` 和中间件,来拒绝新请求并等待旧请求完成。
一个更完整的模式是创建一个中间件:
// 在 Program.cs 的 app.Run() 之前添加这个中间件
app.Use(async (context, next) =>
{
// 检查是否处于停止阶段
if (lifetime.ApplicationStopping.IsCancellationRequested)
{
context.Response.StatusCode = 503; // Service Unavailable
await context.Response.WriteAsync("服务正在关闭,暂时无法处理请求。");
return; // 直接返回,不再向下传递管道
}
await next.Invoke();
});
实战经验:在Kubernetes中,当Pod被删除时,会先发送SIGTERM信号,等待 `terminationGracePeriodSeconds`(默认30秒)后,如果进程仍未退出,则发送SIGKILL强制杀死。因此,我们的 `ShutdownTimeout` 应小于这个宽限期。
第四步:将健康检查与优雅终止结合
现在,让我们把两者结合起来,形成一个闭环。在优雅终止开始后,我们的健康检查端点(特别是就绪检查)应该立刻返回不健康状态,以便负载均衡器快速将流量切走。
我们可以创建一个简单的“健康状态管理器”:
public class ApplicationStatusService
{
private volatile bool _isStopping = false;
public bool IsStopping => _isStopping;
public void SetStopping() => _isStopping = true;
}
// 在Program.cs中注册为单例
builder.Services.AddSingleton();
// 修改优雅终止事件
lifetime.ApplicationStopping.Register(() =>
{
var statusService = app.Services.GetRequiredService();
statusService.SetStopping();
Console.WriteLine("应用状态已标记为‘停止中’,健康检查将开始失败。");
// ... 其他清理逻辑
});
// 创建一个自定义的健康检查,响应应用停止状态
builder.Services.AddHealthChecks()
.AddCheck("application-status", tags: new[] { "ready" });
// 自定义健康检查实现
public class ApplicationStatusHealthCheck : IHealthCheck
{
private readonly ApplicationStatusService _statusService;
public ApplicationStatusHealthCheck(ApplicationStatusService statusService)
{
_statusService = statusService;
}
public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
if (_statusService.IsStopping)
{
return Task.FromResult(HealthCheckResult.Unhealthy("应用程序正在关闭,不再就绪。"));
}
return Task.FromResult(HealthCheckResult.Healthy());
}
}
这样一来,一旦优雅终止流程启动,`/health/ready` 端点会立刻返回 `Unhealthy`。Kubernetes的 `readinessProbe` 探测到失败,就会将此Pod从Service的Endpoint列表中移除,后续流量就不会再发过来了。
总结与最佳实践
通过以上步骤,我们为ASP.NET Core应用构建了一套提升高可用性的基础机制:
- 分层健康检查:区分存活与就绪,并集成关键依赖(DB、API)的状态。
- 主动优雅终止:监听终止信号,拒绝新请求,完成进行中的任务,并执行资源清理。
- 闭环反馈:将终止状态反馈给健康检查,加速流量切换。
最后几个建议:
- 超时设置要合理:优雅关闭超时应略小于编排器的宽限期。健康检查的超时和间隔也要根据依赖的响应时间仔细配置。
- 检查逻辑要轻量:健康检查端点会被高频调用,逻辑必须简单快速,避免造成性能瓶颈。复杂的检查可以放在后台任务中,健康检查只查询缓存的结果。
- 做好日志与监控:记录健康状态的变化和优雅终止的触发与完成,这对于排查问题至关重要。
希望这篇教程能帮助你构建出更稳定、更专业的ASP.NET Core应用。在实际部署中,你可能还需要结合Consul、Prometheus等工具进行更完善的服务治理与监控。如果在集成过程中遇到问题,欢迎在源码库社区交流讨论!

评论(0)