健康检查:在 .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"
        }
      }
    }

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


相关推荐
海宇AI12 分钟前
微服务架构实战:基于海宇对外投资历史查询服务构建自动化合规审计网关
人工智能·微服务·架构·自动化
这个DBA有点耶2 小时前
银行核心系统数据库迁移怎么选?6 步法+5 个避坑指南
数据库·安全·架构
caoerzhong3 小时前
JeeWMS 开源仓库管理系统多租户架构解析:一套 Java WMS 如何同时服务多个货主与多个仓库
java·架构·开源·vue
聚搜云——JuSouClouD3 小时前
阿里云代理商能帮忙设计架构方案吗?有哪些增值服务
阿里云·架构·云计算
白远山4 小时前
智慧场馆解决方案软件开发实战:从架构设计到落地部署指南
java·开发语言·架构·需求分析
m0_587383004 小时前
折扣卡CPS软件开发实战:从系统架构设计到上线指南
java·小程序·架构·需求分析
Dawson Zhu4 小时前
基于 OKF 知识图谱的 Text2SQL 领域知识注入实践——半导体晶圆厂数据资产知识库 MVP 剖析
人工智能·语言模型·架构·aigc·agi
晚安日记wanna5 小时前
批量请求失败只弹一个 Toast面试官想听五层
前端·面试·架构
大模型码小白5 小时前
告别造假数据,直接连数据库查真实时序数据喂给 TimechoAI 大模型
java·数据库·人工智能·microsoft·架构
晚安日记wanna5 小时前
大表 DDL 面试翻车现场Online DDL 为什么还会锁死业务
数据库·面试·架构