ASP.NET 核心健康检查:返回预先评估的结果
ASP.NET Core Health Checks: Returning pre-evaluated results
我正在评估 Microsoft Health Checks to improve routing of our internal load balancer. So far I'm very happy with the functionality provided by this feature and the community around it 的使用情况。但是有一件事我还没有找到,想问一下是否可以开箱即用:
健康检查似乎会在被请求时立即检索自己的状态。但是因为我们的服务可能很难在给定的时刻处理大量请求,所以对像 SQL 服务器这样的第三方组件的查询可能需要时间来响应。因此,我们希望定期(例如每隔几秒)预评估该健康检查,并 return 调用健康检查 api 时的状态。
原因是,我们希望我们的负载均衡器尽快获得健康状态。使用预先评估的结果似乎足以满足我们的用例。
现在的问题是:是否可以在 ASP.NET 核心健康检查中添加一种“轮询”或“自动更新”机制? 或者这是否意味着我必须实施我自己的健康检查 return 从定期预评估结果的后台服务获取值?
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.
精简版
这已经可用并且已经可以与常见的监控系统集成。您可以将健康检查直接绑定到您的监控基础设施中。
详情
Health Check 中间件通过定期 publishing metrics to a target, through any registered classes that implement the 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 这样的 time-series 数据库。 AspNetCore.Diagnostics.HealthCheck 软件包为 App Insights、Seq、Datadog 和 Prometheus 提供了大量 ready-made 检查和发布者。
Prometheus 使用轮询本身。它会定期调用所有已注册的源来检索指标。虽然这适用于服务,但不适用于 CLI 应用程序。出于这个原因,应用程序可以将结果推送到缓存指标的 Prometheus 网关,直到 Prometheus 本身请求它们。
services.AddHealthChecks()
.AddSqlServer(connectionString: Configuration["Data:ConnectionStrings:Sample"])
.AddCheck<RandomHealthCheck>("random")
.AddPrometheusGatewayPublisher();
除了推送到 Prometheus Gateway 之外,Prometheus 发布者 also offers an endpoint 还可以通过 AspNetcore.HealthChecks.Publisher.Prometheus
包直接检索实时指标。其他应用程序可以使用相同的端点来检索这些指标:
// default endpoint: /healthmetrics
app.UseHealthChecksPrometheusExporter();
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
到 return 一个漂亮的 json响应。
但我绘制了另一条路线 /health/latest
。在那里,谓词 _ => false
根本阻止执行任何健康检查。但是,我没有 return 零健康检查的空结果,而是 return 通过访问静态 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
就是 return 实时健康报告。如果有很多事情要检查或要发出网络请求,这可能需要一段时间。
调用 /health/latest
将始终 return 最新的 pre-evaluated 健康报告。这是非常快的,如果您有一个负载均衡器等待健康报告相应地路由传入请求,这可能会有很大帮助。
一点补充: 上面的解决方案使用路由映射来取消执行健康检查和return获取最新的健康报告。正如建议的那样,我尝试首先构建一个进一步的健康检查,它应该 return 最新的缓存健康报告,但这有两个缺点:
- 对 return 缓存报告本身的新健康检查也出现在结果中(或者必须按名称或标签过滤)。
- 没有简单的方法可以将缓存的健康报告映射到
HealthCheckResult
。如果您复制属性和状态代码,这可能会起作用。但是生成的 json 基本上是包含内部健康报告的健康报告。那不是你想要的。
另一种选择是使用 Scrutor 并装饰 HealthCheckService。
如果您想对拥有多个线程 re-publishing 持偏执态度,则必须在从内部 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;
}
}
我正在评估 Microsoft Health Checks to improve routing of our internal load balancer. So far I'm very happy with the functionality provided by this feature and the community around it 的使用情况。但是有一件事我还没有找到,想问一下是否可以开箱即用:
健康检查似乎会在被请求时立即检索自己的状态。但是因为我们的服务可能很难在给定的时刻处理大量请求,所以对像 SQL 服务器这样的第三方组件的查询可能需要时间来响应。因此,我们希望定期(例如每隔几秒)预评估该健康检查,并 return 调用健康检查 api 时的状态。
原因是,我们希望我们的负载均衡器尽快获得健康状态。使用预先评估的结果似乎足以满足我们的用例。
现在的问题是:是否可以在 ASP.NET 核心健康检查中添加一种“轮询”或“自动更新”机制? 或者这是否意味着我必须实施我自己的健康检查 return 从定期预评估结果的后台服务获取值?
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.
精简版
这已经可用并且已经可以与常见的监控系统集成。您可以将健康检查直接绑定到您的监控基础设施中。
详情
Health Check 中间件通过定期 publishing metrics to a target, through any registered classes that implement the 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 这样的 time-series 数据库。 AspNetCore.Diagnostics.HealthCheck 软件包为 App Insights、Seq、Datadog 和 Prometheus 提供了大量 ready-made 检查和发布者。
Prometheus 使用轮询本身。它会定期调用所有已注册的源来检索指标。虽然这适用于服务,但不适用于 CLI 应用程序。出于这个原因,应用程序可以将结果推送到缓存指标的 Prometheus 网关,直到 Prometheus 本身请求它们。
services.AddHealthChecks()
.AddSqlServer(connectionString: Configuration["Data:ConnectionStrings:Sample"])
.AddCheck<RandomHealthCheck>("random")
.AddPrometheusGatewayPublisher();
除了推送到 Prometheus Gateway 之外,Prometheus 发布者 also offers an endpoint 还可以通过 AspNetcore.HealthChecks.Publisher.Prometheus
包直接检索实时指标。其他应用程序可以使用相同的端点来检索这些指标:
// default endpoint: /healthmetrics
app.UseHealthChecksPrometheusExporter();
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
到 return 一个漂亮的 json响应。
但我绘制了另一条路线 /health/latest
。在那里,谓词 _ => false
根本阻止执行任何健康检查。但是,我没有 return 零健康检查的空结果,而是 return 通过访问静态 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
就是 return 实时健康报告。如果有很多事情要检查或要发出网络请求,这可能需要一段时间。
调用 /health/latest
将始终 return 最新的 pre-evaluated 健康报告。这是非常快的,如果您有一个负载均衡器等待健康报告相应地路由传入请求,这可能会有很大帮助。
一点补充: 上面的解决方案使用路由映射来取消执行健康检查和return获取最新的健康报告。正如建议的那样,我尝试首先构建一个进一步的健康检查,它应该 return 最新的缓存健康报告,但这有两个缺点:
- 对 return 缓存报告本身的新健康检查也出现在结果中(或者必须按名称或标签过滤)。
- 没有简单的方法可以将缓存的健康报告映射到
HealthCheckResult
。如果您复制属性和状态代码,这可能会起作用。但是生成的 json 基本上是包含内部健康报告的健康报告。那不是你想要的。
另一种选择是使用 Scrutor 并装饰 HealthCheckService。 如果您想对拥有多个线程 re-publishing 持偏执态度,则必须在从内部 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;
}
}