健康检查:在 .NET 微服务模板中优雅配置 Health Checks

🚀 健康检查:在 .NET 微服务模板中优雅配置 Health Checks


📚 目录

  • [🚀 健康检查:在 .NET 微服务模板中优雅配置 Health Checks](#🚀 健康检查:在 .NET 微服务模板中优雅配置 Health Checks)
    • [一、背景与意义 🔍](#一、背景与意义 🔍)
    • [二、核心配置 🔧](#二、核心配置 🔧)
      • [2.1 引入必要的 NuGet 依赖 📦](#2.1 引入必要的 NuGet 依赖 📦)
      • [2.2 注册 Health Checks 服务 ⚙️](#2.2 注册 Health Checks 服务 ⚙️)
      • [2.3 集成 HealthChecks UI 🖥️](#2.3 集成 HealthChecks UI 🖥️)
      • [2.4 自定义健康端点响应 📋](#2.4 自定义健康端点响应 📋)
    • [三、实战演示 🚀](#三、实战演示 🚀)
      • [3.1 Gateway 端整合示例 🔗](#3.1 Gateway 端整合示例 🔗)
        • [3.2 Health Check 整体流程示意图 📈](#3.2 Health Check 整体流程示意图 📈)
    • [四、Kubernetes 集成 🐳](#四、Kubernetes 集成 🐳)
        • [4.1 Kubernetes 整体流程示意图 🌐](#4.1 Kubernetes 整体流程示意图 🌐)
    • [五、安全性与健壮性 🔐](#五、安全性与健壮性 🔐)
    • [六、性能与可用性考量 🚀](#六、性能与可用性考量 🚀)
      • [完整示例项目结构概览(摘录) 📂](#完整示例项目结构概览(摘录) 📂)

一、背景与意义 🔍

在微服务架构下,各组件、服务分散部署,互相依赖性增加,单纯依靠人工巡检难以及时发现故障。健康检查(Health Check)作为监控与自愈机制的基石,能够让我们:

  • 实时检测关键依赖(如数据库、缓存、中间件、外部 API)是否"在线" ✅
  • 向 Kubernetes 或服务网关暴露健康端点,自动触发重启或流量调整 🔄
  • 为运维团队提供统一可视化界面,便于快速定位问题 🎛️

本节要点:为什么微服务需要健康检查?Health Checks 在分布式系统里有哪些主要用途?


二、核心配置 🔧

在 .NET 中,Health Checks 基于 Microsoft.Extensions.Diagnostics.HealthChecks 扩展,由 ASP.NET Core 提供一整套健康检查接口与中间件。下面我们依次展示如何引入依赖、注册各类检查、集成 HealthChecks UI、并自定义响应格式。

2.1 引入必要的 NuGet 依赖 📦

要使用 SQL Server、Redis、外部 HTTP 等检查,需要先安装对应的 NuGet 包。例如,在项目根目录执行:

bash 复制代码
# 核心健康检查包
dotnet add package Microsoft.Extensions.Diagnostics.HealthChecks

# SQL Server 健康检查扩展
dotnet add package AspNetCore.HealthChecks.SqlServer

# Redis 健康检查扩展
dotnet add package AspNetCore.HealthChecks.Redis

# HTTP/URL 健康检查扩展
dotnet add package AspNetCore.HealthChecks.System

# HealthChecks UI(可视化界面)
dotnet add package AspNetCore.HealthChecks.UI
dotnet add package AspNetCore.HealthChecks.UI.InMemory.Storage  # 演示或开发环境
# 若需持久化存储,可替换为 SqlServer/PostgreSQL 存储包

提示:在生产环境,建议将 HealthChecks UI 的存储改为持久化存储(如 SQL Server、PostgreSQL),否则重启后历史记录会丢失。💾


2.2 注册 Health Checks 服务 ⚙️

Program.cs(或 Startup.cs)中,先将 IConfiguration 拿到本地,然后通过 AddHealthChecks() 注册各类检查项。示例代码如下:

csharp 复制代码
var builder = WebApplication.CreateBuilder(args);
var configuration = builder.Configuration;
var services = builder.Services;

// 1. 注册基本健康检查
services.AddHealthChecks()
    // 自检项:确保应用启动后至少返回 Healthy
    .AddCheck("Self", () => HealthCheckResult.Healthy("I'm alive"))
    // SQL Server 检查:超时 3s,失败返回 Unhealthy,带上 tags 便于筛选
    .AddSqlServer(
        configuration["ConnectionStrings:Default"], 
        name: "SqlServer",
        failureStatus: HealthStatus.Unhealthy,
        tags: new[] { "db", "sql" },
        timeout: TimeSpan.FromSeconds(3)
    )
    // Redis 检查:超时 2s,失败返回 Degraded
    .AddRedis(
        configuration["ConnectionStrings:Redis"], 
        name: "Redis",
        failureStatus: HealthStatus.Degraded,
        tags: new[] { "cache", "redis" },
        timeout: TimeSpan.FromSeconds(2)
    )
    // 外部 HTTP/URL 检查:超时 1s,失败返回 Unhealthy
    .AddUrlGroup(
        new Uri(configuration["ExternalServices:PingUrl"]), 
        name: "ExternalAPI",
        failureStatus: HealthStatus.Unhealthy,
        tags: new[] { "http", "external" },
        timeout: TimeSpan.FromSeconds(1)
    );

要点

  • 🔖 AddCheck("Self", ...):自检项,保持应用启动后能返回健康;
  • 🏷️ tags:为每个检查项打标签,后续在 UI 或 Gateway 可以根据标签筛选;
  • ⏱️ timeout:超过该时长视为检查失败。

2.3 集成 HealthChecks UI 🖥️

HealthChecks UI 提供可视化界面,帮助运维团队查看各个端点历史状态。示例注册如下:

csharp 复制代码
// 2.3.1 注册 UI 服务
services.AddHealthChecksUI(setup =>
{
    setup.SetEvaluationTimeInSeconds(60);           // 每 60s 重新评估一次
    setup.MaximumHistoryEntriesPerEndpoint(50);     // 每个端点保留 50 条历史记录
    // 仅监控 /health-status 这个端点
    setup.AddHealthCheckEndpoint("MicroservicesHealth", "/health-status");
})
// 开发环境或演示环境使用内存存储
.AddInMemoryStorage();

// 若要生产环境使用 SQL Server 存储,请替换为:
// .AddSqlServerStorage(configuration["ConnectionStrings:HealthChecksUI"]);

安全性提示

  • 建议对 UI 界面添加授权策略(如"AdminOnly"),否则任何人都能查看或篡改数据。🛡️

2.4 自定义健康端点响应 📋

默认情况下,MapHealthChecks 只会返回 HTTP 200 和简单的"Healthy/Unhealthy"文本。通常我们希望输出更丰富的 JSON,并根据总体健康状态设置 HTTP 状态码,还要对异常进行日志告警。示例如下:

csharp 复制代码
var app = builder.Build();
var logger = app.Services.GetRequiredService<ILogger<Program>>();

// 将 Health Checks 映射到 /health-status
app.MapHealthChecks("/health-status", new HealthCheckOptions
{
    // 允许在端点调用失败时返回 503
    ResultStatusCodes =
    {
        [HealthStatus.Healthy] = StatusCodes.Status200OK,
        [HealthStatus.Degraded] = StatusCodes.Status503ServiceUnavailable,
        [HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable
    },
    ResponseWriter = async (context, report) =>
    {
        // 若请求取消,提前返回
        context.RequestAborted.ThrowIfCancellationRequested();

        // 遍历所有检查结果,如果有非 Healthy,写警告日志
        foreach (var entry in report.Entries)
        {
            if (entry.Value.Status != HealthStatus.Healthy)
            {
                logger.LogWarning(
                    "Health Check '{Name}' status: {Status}. Error: {Error}",
                    entry.Key,
                    entry.Value.Status,
                    entry.Value.Exception?.Message
                );
            }
        }

        // 自定义 JSON 格式
        var response = new
        {
            status = report.Status.ToString(),
            totalDuration = report.TotalDuration.TotalMilliseconds + " ms",
            results = report.Entries.Select(e => new
            {
                name = e.Key,
                status = e.Value.Status.ToString(),
                duration = e.Value.Duration.TotalMilliseconds + " ms",
                error = e.Value.Exception != null 
                    ? "Error occurred, see logs for details" 
                    : null
            })
        };

        context.Response.ContentType = "application/json; charset=utf-8";
        var options = new JsonSerializerOptions
        {
            WriteIndented = true,
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        };
        await context.Response.WriteAsync(JsonSerializer.Serialize(response, options));
    }
})
.RequireAuthorization("HealthCheckPolicy"); // 仅允许特定角色访问

要点

  1. 将 Degraded/Unhealthy 状态映射为 HTTP 503 ⚠️;
  2. ResponseWriter 中遍历每个检查项,若非 Healthy,就写警告日志 📝;
  3. 不直接将 Exception.Message 返回客户端,避免泄露内部实现 🔒;
  4. 使用 JsonSerializerOptions 美化 JSON 🎨。

三、实战演示 🚀

下面以一个 API Gateway 为例,演示如何聚合多个微服务的 /health-status,并在本地提供一个"聚合健康状态"端点。

3.1 Gateway 端整合示例 🔗

csharp 复制代码
// 在 Program.cs 中继续配置
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("GatewayHealthPolicy", policy =>
        policy.RequireRole("GatewayAdmin"));
});

var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();

// 注入 HttpClient,用于调用下游服务
builder.Services.AddHttpClient("HealthClient")
    .ConfigureHttpClient(client =>
    {
        client.Timeout = TimeSpan.FromSeconds(2); // 每个请求超时 2s
    });

app.MapGet("/aggregate-health", async (IHttpClientFactory httpFactory) =>
{
    var urls = new[]
    {
        "http://serviceA/health-status",
        "http://serviceB/health-status",
        "http://serviceC/health-status"
    };
    var client = httpFactory.CreateClient("HealthClient");

    var tasks = urls.Select(async url =>
    {
        try
        {
            var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
            var resp = await client.GetStringAsync(url, cts.Token);
            // 简单判断下游状态是否包含 "Unhealthy"
            var isUnhealthy = resp.Contains("\"status\":\"Unhealthy\"");
            return new { Url = url, IsUnhealthy = isUnhealthy };
        }
        catch
        {
            // 调用失败,视为 Unhealthy
            return new { Url = url, IsUnhealthy = true };
        }
    });

    var results = await Task.WhenAll(tasks);

    // 若任一服务不可用,则聚合状态为 Unhealthy
    var aggregateStatus = results.Any(r => r.IsUnhealthy) ? "Unhealthy" : "Healthy";

    var response = new
    {
        aggregateStatus,
        details = results.Select(r => new
        {
            service = r.Url,
            status = r.IsUnhealthy ? "Unhealthy" : "Healthy"
        })
    };

    return Results.Json(response);
})
.RequireAuthorization("GatewayHealthPolicy");

说明

  • 使用 IHttpClientFactory 创建带超时设置的 HttpClient ⏲️;
  • 并发调用各下游 /health-status,若任一返回中包含 "Unhealthy",则认为该服务不可用 🚫;
  • 最后再聚合结果,返回一个整体状态及每个服务的健康情况。
3.2 Health Check 整体流程示意图 📈

Gateway Microservices HTTP GET HTTP GET HTTP GET JSON 聚合 Aggregate Health Endpoint
`/aggregate-health` Service A
`/health-status` Service B
`/health-status` Service C
`/health-status` 客户端/监控系统


四、Kubernetes 集成 🐳

在 Kubernetes 环境中,我们通常要配置两个探针:

  1. livenessProbe:检测应用进程是否"活着",若失败则重启 Pod 🔄
  2. readinessProbe:检测服务是否已"准备就绪"并能正常对外提供流量 ✅

因为 /health-status 包含了对数据库/Redis/外部 API 的检查,不建议直接当作 livenessProbe,否则只要依赖短暂不可用就会不断重启。最佳实践如下:

yaml 复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-microservice
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-microservice
  template:
    metadata:
      labels:
        app: my-microservice
    spec:
      containers:
        - name: web
          image: myregistry/my-microservice:latest
          ports:
            - containerPort: 80
          # livenessProbe:只检查应用自检 ping 接口
          livenessProbe:
            httpGet:
              path: /health/ping
              port: 80
            initialDelaySeconds: 30
            periodSeconds: 10
            timeoutSeconds: 2
            failureThreshold: 3
          # readinessProbe:检查完整的 /health-status
          readinessProbe:
            httpGet:
              path: /health-status
              port: 80
            initialDelaySeconds: 15
            periodSeconds: 20
            timeoutSeconds: 5
            failureThreshold: 2
          # 环境变量或 ConfigMap 挂载可灵活配置连接字符串
          env:
            - name: ConnectionStrings__Default
              valueFrom:
                secretKeyRef:
                  name: my-secrets
                  key: SqlConnectionString
            - name: ConnectionStrings__Redis
              valueFrom:
                secretKeyRef:
                  name: my-secrets
                  key: RedisConnectionString
            - name: ExternalServices__PingUrl
              value: "https://api.external.com/ping"

要点

  • 📌 /health/ping 端点仅检查自检项(见 2.2 中 .AddCheck("Self", ...)),保证应用运行即可;
  • 📌 /health-status 端点同时检查数据库、Redis、外部 API,仅当所有依赖都可用时才返回 Healthy;
  • 📌 合理设置 initialDelaySecondsperiodSecondstimeoutSecondsfailureThreshold,避免频繁误判;
  • 🔒 如果探针端点公开在公网上,一定要在 Ingress 或 Service 层面添加 IP 白名单或身份验证。
4.1 Kubernetes 整体流程示意图 🌐

KubeControlPlane Pod 检查自检 检查数据库/Redis/外部 livenessProbe readinessProbe Pod 状态 Kubelet 调度器/ReplicaSet 应用进程 "/health/ping"
(livenessProbe) "/health-status"
(readinessProbe)


五、安全性与健壮性 🔐

  1. 不要泄露异常细节

    • 在自定义响应时,将 Exception.Message 只写入日志,不直接返回给客户端,防止敏感信息泄露。
  2. 鉴权与授权

    • /health-status/hc-ui 端点均要加上授权策略。例如在 Program.cs 中:
    csharp 复制代码
    builder.Services.AddAuthorization(options =>
    {
        options.AddPolicy("HealthCheckPolicy", policy =>
            policy.RequireRole("HealthAdmin"));
        options.AddPolicy("UIAccessPolicy", policy =>
            policy.RequireRole("OpsUser"));
    });
    
    app.UseAuthentication();
    app.UseAuthorization();
    
    app.MapHealthChecks("/health-status", new HealthCheckOptions { ... })
        .RequireAuthorization("HealthCheckPolicy");
    
    app.MapHealthChecksUI(options =>
    {
        options.UIPath = "/hc-ui";
    }).RequireAuthorization("UIAccessPolicy");

    说明:🔒 只有拥有对应角色的用户才可访问健康检查端点和 UI 界面。

  3. 日志告警与追踪 📣

    • 将非 Healthy 检查项的异常写入告警日志,并结合 Prometheus、Grafana 等工具配置告警规则。例如:

      csharp 复制代码
      foreach (var entry in report.Entries)
      {
          if (entry.Value.Status != HealthStatus.Healthy)
          {
              logger.LogError(
                  "【HealthCheck告警】{Name} 状态: {Status},详细: {Exception}",
                  entry.Key,
                  entry.Value.Status,
                  entry.Value.Exception?.ToString()
              );
          }
      }
    • 配合 ILogger 将日志发送到 Elasticsearch / Seq / Kibana 等日志分析平台。

  4. Graceful Shutdown(优雅下线) 🌅

    • 在程序关闭时,将 Pod 状态先置为 NotReady,再执行清理逻辑。可以在 Program.cs 中订阅 ApplicationStopping 事件:

      csharp 复制代码
      var lifetime = app.Services.GetRequiredService<IHostApplicationLifetime>();
      lifetime.ApplicationStopping.Register(() =>
      {
          // 将自检状态置为 Unhealthy,通知 Kubernetes 不再发流量
          // 具体实现可将"Self"检查改为动态返回 Unhealthy
      });

六、性能与可用性考量 🚀

  1. 监控健康检查耗时 ⏱️

    • 使用 Prometheus Client 库,将每个健康检查项的耗时上报到 Prometheus:

      csharp 复制代码
      // 在自定义 ResponseWriter 中添加
      var gauge = Metrics.CreateGauge("health_check_duration_seconds", "健康检查耗时(秒)", "dependency");
      foreach (var entry in report.Entries)
      {
          gauge.WithLabels(entry.Key).Set(entry.Value.Duration.TotalSeconds);
      }
    • Grafana 可据此画出每个依赖的响应曲线,帮助发现性能瓶颈。📊

  2. 抖动过滤与熔断 ⚡

    • 若数据库偶发抖动,会在 /health-status 上触发短暂 Unhealthy,造成 Pod 频繁重启。可使用"熔断+滑动窗口"策略,将检查结果在内存中缓存一定时长:

      • 使用 Polly 的 CircuitBreaker 在检测到连续 3 次失败后,将后续 30s 直接判定为 Unhealthy,避免频繁触发探针。

      • 示例:

        csharp 复制代码
        using Polly;
        using Polly.CircuitBreaker;
        
        // 定义熔断策略
        var breakerPolicy = Policy.Handle<Exception>()
            .CircuitBreakerAsync(
                exceptionsAllowedBeforeBreaking: 3,
                durationOfBreak: TimeSpan.FromSeconds(30)
            );
        
        services.AddHealthChecks()
            .AddCheck("SqlServerWithBreaker", async () =>
            {
                return await breakerPolicy.ExecuteAsync(async () =>
                {
                    // 这里实际调用 SQL Server 做检查
                    // ...
                    return HealthCheckResult.Healthy();
                });
            });
    • 这样在连续失败后,短时间内不再实际调用数据库,只把健康状态返回为 Unhealthy,等熔断解除后再恢复正常调用。🔄

  3. 水平扩展与缓存 ☁️

    • 当微服务水平扩展后,如果每个实例都直接去访问数据库检查,容易造成短时压力;可考虑:
      1. 统一在一个"侧车"(Sidecar)或中间层进行数据库探测,主实例直接从侧车获取结果。
      2. 将某些常驻的健康检查(如 Redis)结果缓存在内存 30 秒,每次请求 /health-status 时先读取缓存。

完整示例项目结构概览(摘录) 📂

复制代码
MicroserviceDemo
├─ Program.cs
├─ appsettings.json
├─ Controllers
│  └─ WeatherController.cs
├─ HealthChecks
│  └─ CustomHealthChecks.cs  # 可放置自定义熔断/滑动窗口检查逻辑
├─ Properties
│  └─ launchSettings.json
└─ Dockerfile
  • Program.cs:包含上述健康检查与 UI 的注册、映射、授权逻辑。

  • appsettings.json :存储连接字符串与配置项,例如:

    jsonc 复制代码
    {
      "ConnectionStrings": {
        "Default": "Server=.;Database=MyDb;User Id=sa;Password=Your_password;",
        "Redis": "localhost:6379"
      },
      "ExternalServices": {
        "PingUrl": "https://api.external.com/ping"
      },
      "HealthChecksUI": {
        "HealthChecks-UI": [
          {
            "Name": "MicroservicesHealth",
            "Uri": "/health-status"
          }
        ],
        "EvaluationTimeOnSeconds": 60,
        "MinimumSecondsBetweenFailureNotifications": 50
      },
      "Logging": {
        "LogLevel": {
          "Default": "Information",
          "Microsoft.AspNetCore": "Warning"
        }
      }
    }

注意:示例仅供参考,请根据实际项目需求调整配置项及命名规范。


相关推荐
前端付豪1 小时前
连夜睡服低管,后端回滚没你想的简单:网易如何用一套体系保障“无痛撤退”?
前端·后端·架构
码观天工1 小时前
揭秘C#异步编程核心机制:从状态机到线程池的全面拆解
性能优化·c#·.net·线程·多线程·并发·异步·并行·高性能
义达2 小时前
Anycast从原理到实践
后端·架构·云计算
往日情怀酿做酒 V17639296383 小时前
web架构2------(nginx多站点配置,include配置文件,日志,basic认证,ssl认证)
前端·nginx·架构
AWS官方合作商3 小时前
AWS App Mesh实战:构建可观测、安全的微服务通信解决方案
微服务·云原生·架构·aws
白总Server11 小时前
C++语法架构解说
java·网络·c++·网络协议·架构·golang·scala
掘金-我是哪吒12 小时前
分布式微服务系统架构第142集:全栈开发
分布式·微服务·云原生·架构
weixin_4723394613 小时前
MySQL优化全链路实践:从慢查询治理到架构升级
数据库·mysql·架构
厚衣服_313 小时前
第1篇:数据库中间件概述:架构演进、典型方案与应用场景
数据库·中间件·架构
Jouzzy15 小时前
【iOS安全】使用LLDB调试iOS App | LLDB基本架构 | LLDB安装和配置
安全·ios·架构