在ASP.NET Core中,健康检查端点并不是简单的返回200状态码,它更像是应用向外部基础设施开放的一个自检通道。负载均衡器、容器编排系统、监控探针会定期请求这个路径,通过响应结果判断当前实例是否应该继续接收流量。ASP.NET Core将健康检查设计为中间件的一部分,而不是依赖MVC控制器,因此执行路径更短,也不容易受到路由、过滤器或模型绑定的干扰。理解它的工作方式,有助于在微服务或多实例部署中快速发现故障节点,配合自动恢复机制缩短不可用时间。

框架内部维护了一个HealthCheckService,它负责聚合所有已注册的IHealthCheck实现,并根据配置并行或串行执行这些检查。每个检查会返回Healthy、Degraded或Unhealthy三种状态之一,最终形成一个HealthReport。默认情况下,MapHealthChecks只把报告中的状态映射为HTTP状态码,不输出响应体,这样既减少了带宽开销,也避免泄露敏感信息。不过在生产环境中,为了便于排障,通常会通过ResponseWriter自定义输出内容。
一、健康检查端点的核心机制
健康检查端点的价值在于它为系统提供了一个统一的探针入口。假设一个服务依赖数据库、缓存和消息队列,如果其中某个依赖不可用,接口请求可能开始失败。通过健康检查,流量入口可以在问题扩大之前把该实例标记为不可用,或者触发告警。ASP.NET Core本身不关心谁在调用这个端点,它只负责执行你注册的检查逻辑并返回结果。这个设计使得健康检查可以被Kubernetes的livenessProbe、readinessProbe引用,也可以被Nginx、HAProxy、云负载均衡器集成。
从状态语义上看,Healthy表示所有检查都通过,实例可以正常对外服务;Degraded表示实例虽然还能工作,但某些指标已经偏离正常范围,例如磁盘剩余空间偏低或某个非关键依赖响应变慢;Unhealthy表示至少一个关键检查失败,实例应当停止接收新流量。默认情况下,只要有一个检查返回Unhealthy,整体状态码就会变成503 Service Unavailable。这种分级机制为运维决策提供了比单纯的“活没活”更细粒度的信息。
还需要注意执行模型。HealthCheckService支持设置超时时间,避免某个检查长时间阻塞整个健康探测。多个检查可以并行执行,也可以根据需要进行分组。分组的意义在于区分存活探针和就绪探针:存活探针通常只检查进程是否正常,就绪探针则检查依赖是否就绪。通过给检查打上标签,再在映射端点时使用Predicate过滤,就能让同一个应用暴露多个不同用途的健康检查路径。
二、从零创建基础健康检查端点
最简实现只需要两步:注册健康检查服务,并映射一个URL。在.NET 6及之后版本中,所有配置都集中在Program.cs中完成。打开项目中的Program.cs,在var app = builder.Build()之前加入服务注册,在构建应用之后调用映射方法。下面的代码展示了最小可运行配置:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHealthChecks();
var app = builder.Build();
app.MapHealthChecks("/health");
app.Run();
这段代码会在/health路径上暴露一个健康检查端点。由于没有注册任何具体检查,HealthCheckService会返回一个空的健康报告,状态默认为Healthy,因此访问该路径会得到200 OK。如果希望看到响应体,可以在浏览器中直接访问,但默认响应体为空,只能通过开发者工具查看状态码。使用curl命令验证时,执行curl -i http://localhost:5000/health即可看到HTTP/1.1 200 OK的返回。
只返回状态码虽然足够让负载均衡器工作,但对开发人员排障并不友好。为此可以在MapHealthChecks中传入HealthCheckOptions,并设置ResponseWriter属性。下面的示例使用System.Text.Json输出一个包含状态和检查项的JSON对象,方便人类阅读和机器解析:
using System.Text.Json;
app.MapHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = async (context, report) =>
{
context.Response.ContentType = "application/json";
var result = JsonSerializer.Serialize(new
{
status = report.Status.ToString(),
checks = report.Entries.Select(e => new
{
name = e.Key,
status = e.Value.Status.ToString(),
description = e.Value.Description
})
});
await context.Response.WriteAsync(result);
}
});
这里用到了HealthCheckOptions,它位于Microsoft.AspNetCore.Diagnostics.HealthChecks命名空间。ResponseWriter是一个委托,接收HttpContext和HealthReport两个参数,开发人员可以完全控制输出格式。需要注意的是,如果响应包含内部主机名、连接字符串或异常堆栈,要做好信息脱敏,避免健康端点成为信息泄露的入口。
三、自定义健康检查与响应格式化
真实项目中几乎不会只使用空健康检查。更常见的做法是实现IHealthCheck接口,把数据库连通性、磁盘空间、第三方API可用性等逻辑封装成独立检查。实现该接口需要编写CheckHealthAsync方法,返回HealthCheckResult对象。下面是一个检查SQL Server连接的自定义健康检查:
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Data.SqlClient;
public class DatabaseHealthCheck : IHealthCheck
{
private readonly string _connectionString;
public DatabaseHealthCheck(string connectionString)
{
_connectionString = connectionString;
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
try
{
await using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync(cancellationToken);
return HealthCheckResult.Healthy("数据库连接正常");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("数据库连接失败", ex);
}
}
}
在Program.cs中注册该检查时,可以使用AddCheck方法,并指定一个逻辑名称。例如builder.Services.AddHealthChecks().AddCheck<DatabaseHealthCheck>("database")。如果构造函数需要参数,可以使用AddTypeActivatedCheck或手动传入实例。AddCheck还允许设置失败状态、超时时间和标签,例如.AddCheck<DatabaseHealthCheck>("database", tags: new[] { "ready" })。这样在映射就绪探针时就可以通过标签过滤,只执行包含ready标签的检查。
除了手写检查,社区提供了丰富的现成扩展包,例如AspNetCore.HealthChecks.SqlServer、AspNetCore.HealthChecks.Redis、AspNetCore.HealthChecks.Uris等。这些包封装了常见依赖的健康检查逻辑,安装后只需一行AddSqlServer或AddRedis即可完成注册。不过在引入第三方包时,建议先阅读其实现细节,确认默认超时时间和异常处理策略是否符合自己的故障判定标准。
响应格式化还可以做得更结构化。在Kubernetes场景中,一些平台会解析响应体中的字段来判断是否需要重启容器。下面是一个输出JSON数组格式的ResponseWriter示例,它只暴露检查名称和状态,不输出异常明细:
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready"),
ResponseWriter = async (context, report) =>
{
context.Response.ContentType = "application/json";
var checks = report.Entries.Select(e => new
{
name = e.Key,
status = e.Value.Status.ToString()
});
await context.Response.WriteAsync(
JsonSerializer.Serialize(new { checks = checks }));
}
});
这个示例同时展示了Predicate的用法。通过Predicate可以控制当前端点只执行哪些检查,而不需要为不同探针重复维护多套检查注册代码。存活探针通常映射到/health/live,只检查进程状态;就绪探针映射到/health/ready,检查数据库、Redis、消息队列等外部依赖。两者分离后,当数据库短暂不可用时,就绪探针失败但存活探针仍然通过,容器不会被重启,而是暂时从Service中摘除流量,待数据库恢复后自动重新接入。
四、接入容器与运维场景的注意事项
将健康检查端点接入Kubernetes时,典型配置是在Deployment中为容器定义livenessProbe和readinessProbe。livenessProbe指向/health/live,initialDelaySeconds可以设置为10秒,periodSeconds设置为10秒。readinessProbe指向/health/ready,失败阈值可以适当放宽,避免网络抖动导致Pod频繁上下线。Kubernetes根据探针结果决定是否重启容器或是否将Pod加入到Service的Endpoints列表。这个机制对无状态服务非常有效,但对于有状态服务,需要结合具体存储方案的故障转移策略。
健康检查本身也会给系统带来一定开销。如果每个检查都去连接数据库执行一条查询,当实例数量增多时,会对数据库形成周期性的探测压力。为了降低影响,可以适当增大periodSeconds,或在检查逻辑中加入缓存结果,例如每30秒才真正执行一次数据库查询,其他请求直接返回上次结果。对于CPU密集或网络延迟较高的检查,应该设置合理的超时时间,避免健康探测线程堆积,反而拖垮应用。
另一个容易忽略的问题是端点安全。健康检查路径通常不需要身份认证,如果暴露在公网,任何人都可以请求它,虽然信息量较少,但可能被用来探测服务是否存在或触发依赖检查。建议在网络层通过防火墙或反向代理限制访问来源,只允许负载均衡器、监控系统或内网IP访问。如果必须对公网开放,也应在ResponseWriter中只输出最小必要信息,并且不要在日志中记录完整的健康检查请求内容。
最后,健康检查不能替代完善的日志、指标和告警体系。它只是一个快速判断实例是否可用的手段,无法反映应用的吞吐量、延迟分布或错误率变化。把健康检查与Prometheus指标、结构化日志结合起来,才能在故障发生时既知道节点是否健康,也知道为什么健康状态发生了变化。对于ASP.NET Core应用来说,这种组合通常意味着在AddHealthChecks之外,再接入UseHealthChecksPrometheusExporter或自定义指标端点,形成完整的可观测性方案。
ASP.NET Core健康检查HealthChecks修改时间:2026-09-19 17:30:48