簡體   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