我正在评估如何使用Microsoft健康检查来改进内部负载均衡器的路由。到目前为止,我对这个特性和它周围的社区提供的功能非常满意。然而,有一件事我还没有找到,我想问一问,这是否有可能是现成的:
一旦提出要求,健康检查似乎就会检索到自己的状态。但是,由于我们的服务在给定的时刻可能很难处理大量的请求,所以对SQL Server这样的第三方组件的查询可能需要很长的时间来响应。因此,我们希望定期对健康检查进行预评估(比如每隔几秒钟),并在调用健康检查api时返回该状态。
原因是,我们希望我们的负载平衡器尽快获得健康状态。对我们的用例来说,使用预评估的结果似乎足够好。
现在的问题是:是否可以在ASP.NET核心健康检查中添加一种“轮询”或“自动更新”机制?还是意味着我必须执行自己的健康检查,从定期对结果进行预评估的后台服务返回值?
请注意,我希望在每个请求上使用预先评估的结果,这不是HTTP缓存,在那里为下一个请求缓存活动结果。
发布于 2020-10-08 08:04:37
短版
这已经可用,并且已经可以与共同的监测系统相结合。您可以将健康检查直接绑定到您的监控基础结构中。
The details
Health中间件通过实现出版接口方法的任何注册类周期性地对目标进行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这样的监控系统,或者像Prometheus这样的时间序列数据库。AspNetCore.Diagnostics.HealthCheck软件包为App、Seq、Datadog和Prometheus提供了大量现成的支票和发行商。
普罗米修斯使用轮询本身。它定期调用所有已注册的源来检索度量。虽然这适用于服务,但它不适用于CLI应用程序。因此,应用程序可以将结果推送到Prometheus网关,该网关缓存度量,直到Prometheus自己提出请求。
services.AddHealthChecks()
.AddSqlServer(connectionString: Configuration["Data:ConnectionStrings:Sample"])
.AddCheck<RandomHealthCheck>("random")
.AddPrometheusGatewayPublisher();除了推进Prometheus之外,Prometheus 还提供了一个端点还通过AspNetcore.HealthChecks.Publisher.Prometheus包直接检索实时度量。其他应用程序可以使用相同的端点检索这些指标:
// default endpoint: /healthmetrics
app.UseHealthChecksPrometheusExporter();发布于 2020-10-09 09:13:58
Panagiotis的答案很精彩,给我带来了一个优雅的解决方案,我很想留给下一个开发人员在这个问题上步履蹒跚。
为了在不实现后台服务或任何计时器的情况下实现定期更新,我注册了一个IHealthCheckPublisher。这样,ASP.NET核心将自动定期运行注册的健康检查,并将其结果发布到相应的实现中。
在我的测试中,健康报告在默认情况下每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,并使用UIResponseWriter.WriteHealthCheckUIResponse返回一个漂亮的json响应。
但我绘制了另一条路线/health/latest。在那里,谓词_ => 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基本上是一份包含内部健康报告的健康报告。这不是你想要的。发布于 2022-02-10 22:36:29
另一种选择是使用洗涤器,并装饰HealthCheckService。如果您想要多管齐下地重新发布多个线程,那么您必须在从内部HealthCheckReport中获取HealthCheckService时添加一个锁定机制。这里就是一个很好的例子。
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;
}
}https://stackoverflow.com/questions/64245987
复制相似问题