必须同时注册检查项、暴露端点、确保检查逻辑真实执行;addhealthchecks()仅注册服务,需maphealthchecks()挂载中间件且顺序正确;数据库检查需显式启用探测并设failurestatus;自定义检查须避免scoped依赖、同步阻塞、无超时http调用;responsewriter改写须保留标准字段名。

加了 AddHealthChecks() 却访问 /health 返回 404,或者始终显示 Healthy —— 这不是配置漏了,而是服务注册、端点暴露、检查逻辑三者没对齐。必须同时满足:注册检查项、暴露端点、每个检查真做事。
为什么 /health 总是 404
根本原因不是路径写错,而是中间件根本没挂上。ASP.NET Core 不会自动把健康检查变成 HTTP 端点。
-
AddHealthChecks()只注册服务,不暴露任何路由 —— 漏掉这步,IHealthCheckService都解析不出来 -
MapHealthChecks("/health")才真正挂载中间件;写成MapGet或手写控制器,会丢失超时控制、状态码映射、并发调度等能力 - 顺序不能错:
app.MapHealthChecks()必须在app.UseRouting()之后、app.UseEndpoints()(或app.MapControllers())之前,否则路由匹配失败 - 若用了反向代理(如 Nginx),确认它转发了
Host和原始路径,否则/health可能被重写成/
为什么加了数据库检查还一直 Healthy
因为默认的 AddSqlServer(connectionString) 并不立即建连 —— 它只验证连接字符串可解析、类型存在。真连库要显式启用探测逻辑。
- 不加参数的
AddSqlServer()实际执行的是轻量级sp_executesql N'SELECT 1',但若连接池卡住或网络不通,可能仍返回Degraded而非Unhealthy - 想让连不上立刻变
Unhealthy,得传failureStatus: HealthStatus.Unhealthy参数 - 别用
AddDbContext<mycontext>()</mycontext>替代健康检查 —— 那只是校验 DI 是否注册成功,完全不碰数据库 - 连接字符串里用
host.docker.internal代替localhost,本地 Docker 测试时才不会连错
自定义 IHealthCheck 容易踩的坑
实现 IHealthCheck 不是写个异步方法就行,生命周期、超时、异常处理全要手动兜底。
- 不能注入
Scoped服务(比如带 DbContext 的仓储),运行时报Cannot resolve scoped service from root provider;要么改注册为Transient,要么手动 newDbContextOptions -
CheckHealthAsync里禁止用.Result、.Wait()或Thread.Sleep(),会阻塞线程池,拖垮整个健康端点 - 外部 HTTP 调用必须设
HttpClient.Timeout(建议 ≤2 秒),且必须传入cancellationToken,否则超时信号无法中断请求 - 别在检查里做重试 —— 健康检查不是故障恢复机制,它只回答“此刻是否可用”
- 失败时用
HealthCheckResult.Unhealthy("msg")显式返回,不要 throw 异常;否则堆栈会进全局异常处理器,客户端看不到具体原因
ResponseWriter 改写时的关键约束
想看每个检查的耗时、异常、状态,必须替换默认 ResponseWriter,但格式不能乱——UI 和监控工具靠字段名解析。
- UI(如 HealthChecksUI)只认
HealthReport.Status、Entries、TotalDuration这几个 key,删了或改名会导致静默忽略该端点 - 序列化
Exception?.Message可以,但别序列化整个Exception对象,JSON 序列化会抛异常 - Kubernetes
livenessProbe只看 HTTP 状态码(200/503),不解析 JSON 内容;但readinessProbe若配了failureThreshold,响应格式错误会导致反复重启 - 生产环境禁用详细输出,用
Environment.IsDevelopment()控制开关,避免泄露连接串、堆栈等敏感信息
最常被忽略的一点:所有检查默认并发执行,但共享同一个全局超时(默认 30 秒)。一个慢检查没传 CancellationToken,整个 /health 就卡住 —— 这比单个检查失败更危险。











