简体   繁体   English

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

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

I'm evaluating the use of Microsoft Health Checks to improve routing of our internal load balancer.我正在评估使用Microsoft Health Checks来改进我们内部负载均衡器的路由。 So far I'm very happy with the functionality provided by this feature and the community around it .到目前为止,我对该特性及其 周围社区提供的功能非常满意。 However there's one thing I did not find yet and wanted to ask if it is possible out of the box:但是有一件事我还没有找到,想问一下是否可以开箱即用:

The Health Checks seem to retrieve their own status as soon as they are requested.健康检查似乎会在被请求时立即检索自己的状态。 But because our service might have a hard time processing a lot of request in that given moment, the query to a thrid-party component like the SQL Server might take it's time to respond.但是因为我们的服务可能很难在给定时刻处理大量请求,所以对第三方组件(如 SQL 服务器)的查询可能需要时间来响应。 Therefore, we would like to pre-evaluate that health check periodically (like every few seconds) and return that state when the health check api gets called.因此,我们希望定期(比如每隔几秒)预评估健康检查,并在健康检查 api 被调用时返回 state。

The reason is, that we want our load balancer to get the health state as quickly as possible.原因是,我们希望我们的负载均衡器尽快获得健康 state。 Using pre-evaluated results seems to be good enough for our use case.使用预先评估的结果似乎足以满足我们的用例。

Now the question is: Is it possible to add a kind of "poll" or "auto-update" mechanism to the ASP.NET Core health checks?现在的问题是:是否可以在 ASP.NET Core 健康检查中添加一种“轮询”或“自动更新”机制? Or does this mean I have to implement my own health check returning values from a background service which pre-evaluates the results periodically?或者这是否意味着我必须从定期预评估结果的后台服务实施我自己的健康检查返回值?

Please note, I want to use pre-evaluated results on each request which is NOT HTTP Caching where the live result is cached for the next requests.请注意,我想在每个请求上使用预评估结果,这不是 HTTP 缓存,其中为下一个请求缓存实时结果。

Panagiotis answer is brilliant and brought me to an elegant solution I'd love to leave for the next developers stumbling over this... Panagiotis 的回答非常棒,并为我提供了一个优雅的解决方案,我很乐意留给下一个遇到这个问题的开发人员......

To achieve periodical updates without implementing a background service or any timers, I registered an IHealthCheckPublisher .为了在不实施后台服务或任何计时器的情况下实现定期更新,我注册了一个IHealthCheckPublisher With this, ASP.NET Core will automatically run the registered health checks periodically and publish their results to the corresponding implementation.这样,ASP.NET Core 将自动定期运行已注册的健康检查,并将其结果发布到相应的实现。

In my tests, the health report was published every 30 seconds by default.在我的测试中,健康报告默认每 30 秒发布一次。

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

I registered my implementation HealthReportCachePublisher which does nothing more than taking a published health report and keeping it in a static property.我注册了我的实现HealthReportCachePublisher ,它只是获取已发布的健康报告并将其保存在 static 属性中。

I don't really like static properties but to me it seems adequate for this use case.我不太喜欢 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;
    }
}

Now the real magic happens here现在真正的魔法发生在这里

As seen in every Health Checks sample, I mapped the health checks to the route /health and use the UIResponseWriter.WriteHealthCheckUIResponse to return a beautiful json response.正如在每个健康检查示例中所见,我将健康检查映射到路由/health并使用UIResponseWriter.WriteHealthCheckUIResponse返回漂亮的 json 响应。

But I mapped another route /health/latest .但是我映射了另一条路线/health/latest There, a predicate _ => false prevents any health checks to be executed at all.在那里,谓词_ => false完全阻止执行任何健康检查。 But instead of returning the empty results of zero health checks, I return the previously published health report by accessing the static HealthReportCachePublisher.Latest .但是,我没有返回零健康检查的空结果,而是通过访问 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)
    });
});

This way, calling /health is returning the live health reports, by executing all the health checks on each request.这样,通过对每个请求执行所有健康检查,调用/health返回实时健康报告。 This might take a while if there are many things to check or.network requests to make.如果有很多事情要检查或要发出网络请求,这可能需要一段时间。

Calling /health/latest will always return the latest pre-evaluated health report.调用/health/latest将始终返回最新的预评估健康报告。 This is extremely fast and may help a lot if you have a load balancer waiting for the health report to route incoming requests accordingly.这是非常快的,如果您有一个负载均衡器等待健康报告相应地路由传入请求,这可能会有很大帮助。


A little addition: The solution above uses the route mapping to cancel the execution of health checks and returning the latest health report.补充一点:上面的方案是使用路由映射取消健康检查的执行,返回最新的健康报告。 As suggested, I tried to build an further health check first which should return the latest, cached health report but this has two downsides:正如建议的那样,我尝试首先构建一个进一步的健康检查,它应该返回最新的缓存健康报告,但这有两个缺点:

  • The new health check to return the cached report itself appears in the results as well (or has to be fitered by name or tags).返回缓存报告本身的新健康检查也出现在结果中(或者必须按名称或标签过滤)。
  • There's no easy way to map the cached health report to a HealthCheckResult .没有简单的方法可以将缓存的健康报告 map 发送到HealthCheckResult If you copy over the properties and status codes this might work.如果您复制属性和状态代码,这可能会起作用。 But the resulting json is basically a health report containing an inner health report.但是得到的 json 基本上是一个包含内部健康报告的健康报告。 That's not what you want to have.那不是你想要的。

Short Version精简版

This is already available and can already integrate with common monitoring systems.这已经可用并且已经可以与通用监控系统集成。 You may be able to tie Health Check directly into your monitoring infrastructure.您可以将健康检查直接绑定到您的监控基础设施中。

The details细节

The Health Check middleware covers this by periodically publishing metrics to a target, through any registered classes that implement the IHealthCheckPublisher.PublishAsync interface method. Health Check 中间件通过任何实现IHealthCheckPublisher.PublishAsync接口方法的已注册类定期向目标发布指标来解决这个问题。

services.AddSingleton<IHealthCheckPublisher, ReadinessPublisher>();

Publishing can be configured through HealthCheckPublisherOptions.可以通过 HealthCheckPublisherOptions 配置发布。 The default period is 30 seconds.默认周期为 30 秒。 The options can be used to add delays, filter the checks to run etc:这些选项可用于添加延迟、过滤要运行的检查等:

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

One option would be to cache the results (the HealthReport instance) with a publisher and serve them from another HealthCheck endpoint.一种选择是使用发布者缓存结果(HealthReport 实例)并从另一个 HealthCheck 端点提供它们。

Perhaps a better option would be to push them to a monitoring system like Application Insights or a time-series database like Prometheus.也许更好的选择是将它们推送到像 Application Insights 这样的监控系统或像 Prometheus 这样的时间序列数据库。 The AspNetCore.Diagnostics.HealthCheck package provides a ton of ready-made checks and publishers for App Insights, Seq, Datadog and Prometheus. AspNetCore.Diagnostics.HealthCheck package 为 App Insights、Seq、Datadog 和 Prometheus 提供了大量现成的检查和发布者。

Prometheus uses polling itself. Prometheus 使用轮询本身。 It calls all its registered sources periodically to retrieve metrics.它会定期调用所有已注册的源来检索指标。 While that works for services, it won't work for eg CLI applications.虽然这适用于服务,但不适用于 CLI 应用程序。 For that reason, applications can push results to a Prometheus Gateway that caches the metrics until Prometheus itself requests them.出于这个原因,应用程序可以将结果推送到缓存指标的 Prometheus 网关,直到 Prometheus 本身请求它们。

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

Apart from pushing to Prometheus Gateway, the Prometheus publisher also offers an endpoint to retrieve live metrics directly, through the AspNetcore.HealthChecks.Publisher.Prometheus package. The same endpoint could be used by other applications to retrieve those metrics:除了推送到 Prometheus 网关之外,Prometheus 发布者还提供了一个端点来直接检索实时指标,通过AspNetcore.HealthChecks.Publisher.Prometheus package。其他应用程序可以使用相同的端点来检索这些指标:

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

Another alternative is using Scrutor , and decorating HealthCheckService.另一种选择是使用Scrutor并装饰 HealthCheckService。 If you want to be paranoid about having multiple threads re-publishing, you'd have to add a locking mechanism while fetching the HealthCheckReport from the inner HealthCheckService.如果您不想让多个线程重新发布,则必须在从内部 HealthCheckService 获取 HealthCheckReport 时添加锁定机制。 A decent example is here .一个不错的例子是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.

相关问题 ASP.NET Core Web API中的AppMetrics运行状况检查 - AppMetrics health checks in asp.net core web api 当应用程序通过 http 请求运行时,我可以添加和删除运行状况检查吗(asp.net 核心) - Can I add and remove health checks when application is running via a http request (asp.net core) 使用路由配置在多租户环境中检查ASP.NET Core Health - ASP.NET Core Health checks in multi tenant environment using routing configuration 如何在使用 ASP.NET 核心中间件健康检查创建的健康检查端点上有选择地强制执行 HTTPS? - How can I selectively enforce HTTPS on a health check endpoint created using ASP.NET Core Middleware Health Checks? ASP.NET 铁芯返回 - ASP.NET Core Returning 如何在 ASP.NET Core 健康检查中注入依赖项 - How to inject dependencies inside an ASP.NET Core Health Check 在 ASP.NET Core 6 中确定运行时的健康检查路由 - Determine health check routes at runtime in ASP.NET Core 6 获取跨平台系统健康ASP.Net Core - Get Cross Platform System Health ASP.Net Core ASP.NET Core LinkGenerator 和编译时间检查 - ASP.NET Core LinkGenerator and compile time checks 减少/删除 ASP.NET 核心中的重复业务逻辑检查 - Reduce/remove repetitive business logic checks in ASP.NET Core
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM