繁体   English   中英

ASP.NET 核心健康检查:返回预评估结果

[英]ASP.NET Core Health Checks: Returning pre-evaluated results

我正在评估使用Microsoft Health Checks来改进我们内部负载均衡器的路由。 到目前为止,我对该特性及其 周围社区提供的功能非常满意。 但是有一件事我还没有找到,想问一下是否可以开箱即用:

健康检查似乎会在被请求时立即检索自己的状态。 但是因为我们的服务可能很难在给定时刻处理大量请求,所以对第三方组件(如 SQL 服务器)的查询可能需要时间来响应。 因此,我们希望定期(比如每隔几秒)预评估健康检查,并在健康检查 api 被调用时返回 state。

原因是,我们希望我们的负载均衡器尽快获得健康 state。 使用预先评估的结果似乎足以满足我们的用例。

现在的问题是:是否可以在 ASP.NET Core 健康检查中添加一种“轮询”或“自动更新”机制? 或者这是否意味着我必须从定期预评估结果的后台服务实施我自己的健康检查返回值?

请注意,我想在每个请求上使用预评估结果,这不是 HTTP 缓存,其中为下一个请求缓存实时结果。

Panagiotis 的回答非常棒,并为我提供了一个优雅的解决方案,我很乐意留给下一个遇到这个问题的开发人员......

为了在不实施后台服务或任何计时器的情况下实现定期更新,我注册了一个IHealthCheckPublisher 这样,ASP.NET Core 将自动定期运行已注册的健康检查,并将其结果发布到相应的实现。

在我的测试中,健康报告默认每 30 秒发布一次。

// add a publisher to cache the latest health report
services.AddSingleton<IHealthCheckPublisher, HealthReportCachePublisher>();

我注册了我的实现HealthReportCachePublisher ,它只是获取已发布的健康报告并将其保存在 static 属性中。

我不太喜欢 static 属性,但对我来说它似乎足以满足这个用例。

/// <summary>
/// This publisher takes a health report and keeps it as "Latest".
/// Other health checks or endpoints can reuse the latest health report to provide
/// health check APIs without having the checks executed on each request.
/// </summary>
public class HealthReportCachePublisher : IHealthCheckPublisher
{
    /// <summary>
    /// The latest health report which got published
    /// </summary>
    public static HealthReport Latest { get; set; }

    /// <summary>
    /// Publishes a provided report
    /// </summary>
    /// <param name="report">The result of executing a set of health checks</param>
    /// <param name="cancellationToken">A task which will complete when publishing is complete</param>
    /// <returns></returns>
    public Task PublishAsync(HealthReport report, CancellationToken cancellationToken)
    {
        Latest = report;
        return Task.CompletedTask;
    }
}

现在真正的魔法发生在这里

正如在每个健康检查示例中所见,我将健康检查映射到路由/health并使用UIResponseWriter.WriteHealthCheckUIResponse返回漂亮的 json 响应。

但是我映射了另一条路线/health/latest 在那里,谓词_ => false完全阻止执行任何健康检查。 但是,我没有返回零健康检查的空结果,而是通过访问 static HealthReportCachePublisher.Latest返回了之前发布的健康报告。

app.UseEndpoints(endpoints =>
{
    // live health data: executes health checks for each request
    endpoints.MapHealthChecks("/health", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions()
    {
        ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
    });

    // latest health report: won't execute health checks but return the cached data from the HealthReportCachePublisher
    endpoints.MapHealthChecks("/health/latest", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions()
    {
        Predicate = _ => false, // do not execute any health checks, we just want to return the latest health report
        ResponseWriter = (context, _) => UIResponseWriter.WriteHealthCheckUIResponse(context, HealthReportCachePublisher.Latest)
    });
});

这样,通过对每个请求执行所有健康检查,调用/health返回实时健康报告。 如果有很多事情要检查或要发出网络请求,这可能需要一段时间。

调用/health/latest将始终返回最新的预评估健康报告。 这是非常快的,如果您有一个负载均衡器等待健康报告相应地路由传入请求,这可能会有很大帮助。


补充一点:上面的方案是使用路由映射取消健康检查的执行,返回最新的健康报告。 正如建议的那样,我尝试首先构建一个进一步的健康检查,它应该返回最新的缓存健康报告,但这有两个缺点:

  • 返回缓存报告本身的新健康检查也出现在结果中(或者必须按名称或标签过滤)。
  • 没有简单的方法可以将缓存的健康报告 map 发送到HealthCheckResult 如果您复制属性和状态代码,这可能会起作用。 但是得到的 json 基本上是一个包含内部健康报告的健康报告。 那不是你想要的。

精简版

这已经可用并且已经可以与通用监控系统集成。 您可以将健康检查直接绑定到您的监控基础设施中。

细节

Health Check 中间件通过任何实现IHealthCheckPublisher.PublishAsync接口方法的已注册类定期向目标发布指标来解决这个问题。

services.AddSingleton<IHealthCheckPublisher, ReadinessPublisher>();

可以通过 HealthCheckPublisherOptions 配置发布。 默认周期为 30 秒。 这些选项可用于添加延迟、过滤要运行的检查等:

services.Configure<HealthCheckPublisherOptions>(options =>
{
    options.Delay = TimeSpan.FromSeconds(2);
    options.Predicate = (check) => check.Tags.Contains("ready");
});

一种选择是使用发布者缓存结果(HealthReport 实例)并从另一个 HealthCheck 端点提供它们。

也许更好的选择是将它们推送到像 Application Insights 这样的监控系统或像 Prometheus 这样的时间序列数据库。 AspNetCore.Diagnostics.HealthCheck package 为 App Insights、Seq、Datadog 和 Prometheus 提供了大量现成的检查和发布者。

Prometheus 使用轮询本身。 它会定期调用所有已注册的源来检索指标。 虽然这适用于服务,但不适用于 CLI 应用程序。 出于这个原因,应用程序可以将结果推送到缓存指标的 Prometheus 网关,直到 Prometheus 本身请求它们。

services.AddHealthChecks()
        .AddSqlServer(connectionString: Configuration["Data:ConnectionStrings:Sample"])
        .AddCheck<RandomHealthCheck>("random")
        .AddPrometheusGatewayPublisher();

除了推送到 Prometheus 网关之外,Prometheus 发布者还提供了一个端点来直接检索实时指标,通过AspNetcore.HealthChecks.Publisher.Prometheus package。其他应用程序可以使用相同的端点来检索这些指标:

// default endpoint: /healthmetrics
app.UseHealthChecksPrometheusExporter();

另一种选择是使用Scrutor并装饰 HealthCheckService。 如果您不想让多个线程重新发布,则必须在从内部 HealthCheckService 获取 HealthCheckReport 时添加锁定机制。 一个不错的例子是here

using System.Reflection;
using HealthCheckCache;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Diagnostics.HealthChecks;

var builder = WebApplication.CreateBuilder(args);

// used by the Decorator CachingHealthCheckService
builder.Services.AddMemoryCache();
builder.Services.AddHttpContextAccessor();

// register all IHealthCheck types - basically builder.Services.AddTransient<AlwaysHealthy>(), but across all types in this assembly.
var healthServices = builder.Services.Scan(scan =>
    scan.FromCallingAssembly()
        .AddClasses(filter => filter.AssignableTo<IHealthCheck>())
        .AsSelf()
        .WithTransientLifetime()
);

// Register HealthCheckService, so it can be decorated.
var healthCheckBuilder = builder.Services.AddHealthChecks();
// Decorate the implementation with a cache
builder.Services.Decorate<HealthCheckService>((inner, provider) =>
    new CachingHealthCheckService(inner,
        provider.GetRequiredService<IHttpContextAccessor>(),
        provider.GetRequiredService<IMemoryCache>()
    )
);

// Register all the IHealthCheck instances in the container
// this has to be a for loop, b/c healthCheckBuilder.Add will modify the builder.Services - ServiceCollection
for (int i = 0; i < healthServices.Count; i++)
{
    ServiceDescriptor serviceDescriptor = healthServices[i];
    var isHealthCheck = serviceDescriptor.ServiceType.IsAssignableTo(typeof(IHealthCheck)) && serviceDescriptor.ServiceType == serviceDescriptor.ImplementationType;
    if (isHealthCheck)
    {
        healthCheckBuilder.Add(new HealthCheckRegistration(
            serviceDescriptor.ImplementationType.Name,
            s => (IHealthCheck)ActivatorUtilities.GetServiceOrCreateInstance(s, serviceDescriptor.ImplementationType),
            failureStatus: null,
            tags: null)
        );
    }

}

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.MapHealthChecks("/health", new HealthCheckOptions()
{
    AllowCachingResponses = true, // allow caching at Http level
});

app.Run();

public class CachingHealthCheckService : HealthCheckService
{
    private readonly HealthCheckService _innerHealthCheckService;
    private readonly IHttpContextAccessor _contextAccessor;
    private readonly IMemoryCache _cache;
    private const string CacheKey = "CachingHealthCheckService:HealthCheckReport";

    public CachingHealthCheckService(HealthCheckService innerHealthCheckService, IHttpContextAccessor contextAccessor, IMemoryCache cache)
    {
        _innerHealthCheckService = innerHealthCheckService;
        _contextAccessor = contextAccessor;
        _cache = cache;
    }

    public override async Task<HealthReport> CheckHealthAsync(Func<HealthCheckRegistration, bool>? predicate, CancellationToken cancellationToken = new CancellationToken())
    {
        HttpContext context = _contextAccessor.HttpContext;


        var forced = !string.IsNullOrEmpty(context.Request.Query["force"]);
        context.Response.Headers.Add("X-Health-Forced", forced.ToString());
        var cached = _cache.Get<HealthReport>(CacheKey);
        if (!forced && cached != null)
        {
            context.Response.Headers.Add("X-Health-Cached", "True");
            return cached;
        }
        var healthReport = await _innerHealthCheckService.CheckHealthAsync(predicate, cancellationToken);
        if (!forced)
        {
            _cache.Set(CacheKey, healthReport, TimeSpan.FromSeconds(30));
        }
        context.Response.Headers.Add("X-Health-Cached", "False");
        return healthReport;
    }
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM