【问题标题】:ASP.NET Core Health Checks: Returning pre-evaluated resultsASP.NET Core 运行状况检查:返回预评估结果
【发布时间】:2020-10-07 14:15:50
【问题描述】:

我正在评估使用Microsoft Health Checks 来改进我们内部负载平衡器的路由。到目前为止,我对该功能和the community around it 提供的功能感到非常满意。但是有一件事我还没有找到,想问一下是否可以开箱即用:

运行状况检查似乎会在收到请求后立即检索自己的状态。但是因为我们的服务在那个特定时刻可能很难处理大量请求,所以对 SQL Server 等第三方组件的查询可能需要时间来响应。因此,我们希望定期(例如每隔几秒)预先评估健康检查,并在调用健康检查 api 时返回该状态。

原因是,我们希望负载均衡器尽快获得健康状态。对于我们的用例来说,使用预先评估的结果似乎已经足够了。

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

请注意,我想对每个请求使用预先评估的结果,而不是 HTTP 缓存,其中实时结果被缓存用于下一个请求。

【问题讨论】:

  • 您可以扭转这种局面,并定期将指标推送到服务器。 HealthCheck 支持pushing 到例如Prometheus。 This package 包含大量针对 Prometheus(实际上是 Prometheus 网关)、App Insights、Seq 和 Datadog 的检查和发布者
  • 还有一个direct endpoint for Prometheus to poll。 Prometheus 执行您所描述的操作,它通过调用端点来轮询其事件源。这不适用于例如 CLI 应用程序,因此 Prometheus 网关用作“缓存”来保存应用程序发布的数据,直到 Prometheus 请求它。

标签: c# asp.net-core health-monitoring health-check


【解决方案1】:

Panagiotis 的回答非常出色,并为我带来了一个优雅的解决方案,我很想留给下一个遇到此问题的开发人员...

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

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

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

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

我不太喜欢静态属性,但对我来说,这对于这个用例来说似乎已经足够了。

/// <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 Checks 示例中看到的那样,我将运行状况检查映射到路由 /health 并使用 UIResponseWriter.WriteHealthCheckUIResponse 返回漂亮的 json 响应。

但我绘制了另一条路线/health/latest。在那里,谓词_ =&gt; false 完全阻止执行任何健康检查。但是,我没有返回零健康检查的空结果,而是通过访问静态 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 将始终返回最新的预先评估的健康报告。这非常快,如果您有一个负载均衡器等待运行状况报告相应地路由传入请求,这可能会很有帮助。


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

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

【讨论】:

  • 经过所有这些搜索,完整的答案是如此简单,我想知道为什么它没有作为示例包含在文档中!大多数示例都过于复杂,但这个看似复杂的场景比看起来要容易得多
  • 这很漂亮。很高兴看到基于推送的方式与基于拉取的方式相比。
【解决方案2】:

短版

这已经可用,并且已经可以与常见的监控系统集成。您或许可以将运行状况检查直接绑定到您的监控基础架构中。

详情

运行状况检查中间件通过实现IHealthCheckPublisher.PublishAsync 接口方法的任何已注册类定期将publishing 指标传递给目标来解决此问题。

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 软件包为 App Insights、Seq、Datadog 和 Prometheus 提供了大量现成的检查和发布者。

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

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

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

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

【讨论】:

  • 非常感谢您,Panagiotis。发布者的提示是纯金的,并引导我找到了一个我作为第二个答案发布的简单解决方案,但是我希望您获得解决方案标记。顺便说一句,Prometheus 对我来说看起来很棒,但对我们的环境来说是没有选择的。
  • 在阅读文档之前我不知道这一点。我知道有一种方法可以将健康数据发布到 Prometheus,但直到我开始搜索这个问题时才知道发布功能已经内置
  • 我实际上停止了映射步骤,因为我不知道 Predicate 会阻止指标收集
【解决方案3】:

另一种选择是使用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;
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-10-13
    • 1970-01-01
    • 2015-05-02
    • 1970-01-01
    • 2020-03-30
    • 1970-01-01
    • 2021-12-09
    相关资源
    最近更新 更多