现代化的应用及服务的部署场景主要体现在集群化、微服务和容器化,这一切都建立在针对部署应用或者服务的健康检查上。ASP.NET提供的健康检查不仅可能确定目标应用或者服务的可用性,还具有健康报告发布功能。ASP.NET框架的健康检查功能是通过HealthCheckMiddleware中间件完成的。我们不仅可以利用该中间件确定当前应用的可用性,还可以注册相应的IHealthCheck对象来完成针对不同方面的健康检查。

现代化的应用及服务的部署场景主要体现在集群化、微服务和容器化,这一切都建立在针对部署应用或者服务的健康检查上。ASP.NET提供的健康检查不仅可能确定目标应用或者服务的可用性,还具有健康报告发布功能。ASP.NET框架的健康检查功能是通过HealthCheckMiddleware中间件完成的。我们不仅可以利用该中间件确定当前应用的可用性,还可以注册相应的IHealthCheck对象来完成针对不同方面的健康检查。(本文提供的示例演示已经同步到《 ASP.NET Core 6框架揭秘-实例演示版 》)

[S3001]确定应用可用状态( 源代码
[S3002]定制健康检查逻辑( 源代码
[S3003]改变健康状态对应的响应状态码( 源代码
[S3004]提供细粒度的健康检查( 源代码
[S3005]定制健康报告响应内容( 源代码
[S3006]IHealthCheck对象的过滤( 源代码
[S3007]定期发布健康报告( 源代码

[S3001]确定应用可用状态

对于部署于集群或者容器的应用或者服务来说,它需要对外暴露一个终结点,负载均衡器或者容器编排框架以一定的频率向该终结点发送“心跳”请求,以确定应用和服务的可用性。演示程序应用采用如下的方式提供了这个健康检查终结点。

var builder = WebApplication.CreateBuilder();
builder.Services.AddHealthChecks();
var app = builder.Build();
app.UseHealthChecks(path: "/healthcheck");
app.Run();
演示程序调用了UseHealthChecks扩展方法注册了HealthCheckMiddleware中间件,并利用指定的参数将健康检查终结点的路径设置为“/healthcheck”。该中间件依赖的服务通过调用AddHealthChecks扩展方法进行注册。在程序正常运行的情况下,如果利用浏览器向注册的健康检查路径“/healthcheck”发送一个简单的GET请求,就可以得到图1所示的“健康状态”。

图1 健康检查结果

如下所示的代码片段是健康检查响应报文的内容。这是一个状态码为“200 OK”且媒体类型为“text/plain”的响应,其主体内容就是健康状态的字符串描述。在大部分情况下,发送健康检查请求希望得到的是目标应用或者服务当前实时的健康状况,所以响应报文是不应该被缓存的,如下所示的响应报文的“Cache-Control”和“Pragma”报头也体现了这一点。

HTTP/1.1 200 OK
Content-Type: text/plain
Date: Sat, 13 Nov 2021 05:08:00 GMT
Server: Kestrel
Cache-Control: no-store, no-cache
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Pragma: no-cache
Content-Length: 7
Healthy

[S3002]定制健康检查逻辑

对于前面演示的实例来说,只要应用正常启动,它就被视为“健康”(完全可用),这种情况有时候可能并不是我们希望的。有时候应用在启动之后需要做一些初始化的工作,并希望在这些工作完成之前当前应用处于不可用的状态,这样请求就不会被导流进来。这样的需求就需要我们自行实现具体的健康检查逻辑。下面的演示程序将健康检查实现在内嵌的Check方法中,该方法会随机返回三种健康状态(Healthy、Unhealthy和Degraded)。在调用AddHealthChecks扩展方法注册所需依赖服务并返回IHealthChecksBuilder对象后,它接着调用了该对象的AddCheck方法注册了一个IHealthCheck对象,后者会调用Check方法决定当前的健康状态。

using Microsoft.Extensions.Diagnostics.HealthChecks;
var random = new Random();
var builder = WebApplication.CreateBuilder();
builder.Services
    .AddHealthChecks()
    .AddCheck(name:"default",check: Check);
var app = builder.Build();
app.UseHealthChecks(path: "/healthcheck");
app.Run();
HealthCheckResult Check() => random!.Next(1, 4) switch
    1 => HealthCheckResult.Unhealthy(),
    2 => HealthCheckResult.Degraded(),
    _ => HealthCheckResult.Healthy(),

如下所示的代码片段是针对三种健康状态的响应报文,可以看出它们的状态码是不同的。针对健康状态Healthy和Degraded,响应码都是“200 OK”,因为此时的应用或者服务均会被视为可用(Available)状态,两者之间只是“完全可用”和“部分可用”的区别。状态为Unhealthy的服务被视为不可用(Unavailable),所以响应状态码为“503 Service Unavailable”。

HTTP/1.1 200 OK
Content-Type: text/plain
Date: Sat, 13 Nov 2021 05:08:00 GMT
Server: Kestrel
Cache-Control: no-store, no-cache
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Pragma: no-cache
Content-Length: 7
Healthy
HTTP/1.1 503 Service Unavailable
Content-Type: text/plain
Date: Sat, 13 Nov 2021 05:13:42 GMT
Server: Kestrel
Cache-Control: no-store, no-cache
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Pragma: no-cache
Content-Length: 9
Unhealthy
HTTP/1.1 200 OK
Content-Type: text/plain
Date: Sat, 13 Nov 2021 05:14:05 GMT
Server: Kestrel
Cache-Control: no-store, no-cache
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Pragma: no-cache
Content-Length: 8
Degraded

[S3003]改变健康状态对应的响应状态码

前面我们已经简单解释了三种健康状态与对应的响应状态码。虽然健康检查默认响应状态码的设置是合理的,但是不能通过状态码来区分Healthy和Unhealthy这两种可用状态,可以通过如下所示的方式来改变默认的响应状态码设置。

using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
var random = new Random();
var options = new HealthCheckOptions
    ResultStatusCodes = new Dictionary<HealthStatus, int>
        [HealthStatus.Healthy] 	    = 299,
        [HealthStatus.Degraded] 	= 298,
        [HealthStatus.Unhealthy] 	= 503
var builder = WebApplication.CreateBuilder();
builder.Services
    .AddHealthChecks()
    .AddCheck(name:"default",check: Check);
var app = builder.Build();
app.UseHealthChecks(path: "/healthcheck", options: options);
app.Run();
HealthCheckResult Check() => random!.Next(1, 4) switch
    1 => HealthCheckResult.Unhealthy(),
    2 => HealthCheckResult.Degraded(),
    _ => HealthCheckResult.Healthy(),

上面的演示程序调用UseHealthChecks扩展方法注册HealthCheckMiddleware中间件时提供了一个HealthCheckOptions配置选项。此配置选项通过 ResultStatusCodes 属性返回的字典维护了这三种健康状态与对应响应状态码之间的映射关系。演示程序将针对Healthy和Unhealthy这两种健康状态对应的响应状态码分别设置为“299”与“298”,它们体现在如下所示的三种响应报文中。

HTTP/1.1 299
Content-Type: text/plain
Date: Sat, 13 Nov 2021 05:19:34 GMT
Server: Kestrel
Cache-Control: no-store, no-cache
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Pragma: no-cache
Content-Length: 7
Healthy
HTTP/1.1 298
Content-Type: text/plain
Date: Sat, 13 Nov 2021 05:19:30 GMT
Server: Kestrel
Cache-Control: no-store, no-cache
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Pragma: no-cache
Content-Length: 8
Degraded

[S3004]提供细粒度的健康检查

如果当前应用承载或者依赖了若干组件或者服务,我们可以针对它们做细粒度的健康检查。前面的演示实例通过注册的IHealthCheck对象对“应用级别”的健康检查进行了定制,我们可以采用同样的形式为某个组件或者服务注册相应的IHealthCheck对象来确定它们的健康状况。

using Microsoft.Extensions.Diagnostics.HealthChecks;
var random = new Random();
var builder = WebApplication.CreateBuilder();
builder.Services.AddHealthChecks()
    .AddCheck(name: "foo", check: Check)
    .AddCheck(name: "bar", check: Check)
    .AddCheck(name: "baz", check: Check);
var app = builder.Build();
app.UseHealthChecks(path: "/healthcheck");
app.Run();
HealthCheckResult Check() => random!.Next(1, 4) switch
    1 => HealthCheckResult.Unhealthy(),
    2 => HealthCheckResult.Degraded(),
    _ => HealthCheckResult.Healthy(),

假设当前应用承载了三个服务,分别命名为foo、bar和baz,我们可以采用如下所示的方式为它们注册三个IHealthCheck对象来完成针对它们的健康检查。由于注册的三个IHealthCheck对象采用同一个Check方法决定最后的健康状态,所以最终具有 27种不同的组合 。针对三个服务的27种健康状态组合最终会产生如下 三种不同的响应报文

HTTP/1.1 200 OK
Date: Sat, 13 Nov 2021 05:20:30 GMT
Content-Type: text/plain
Server: Kestrel
Cache-Control: no-store, no-cache
Pragma: no-cache
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Content-Length: 7
Healthy
HTTP/1.1 200 OK
Date: Sat, 13 Nov 2021 05:21:30 GMT
Content-Type: text/plain
Server: Kestrel
Cache-Control: no-store, no-cache
Pragma: no-cache
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Content-Length: 8
Degraded
HTTP/1.1 503 Service Unavailable
Date: Sat, 13 Nov 2021 05:22:23 GMT
Content-Type: text/plain
Server: Kestrel
Cache-Control: no-store, no-cache
Pragma: no-cache
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Content-Length: 9
Unhealthy

健康检查响应并没有返回针对具体三个服务的健康状态,而是返回针对整个应用的整体健康状态,这个状态是根据三个服务当前的健康状态组合计算出来的。按照严重程度,三种健康状态的顺序应该是Unhealthy > Degraded > Healthy,组合中最严重的健康状态就是应用整体的健康状态。按照这个逻辑,如果应用的整体健康状态为Healthy,就意味着三个服务的健康状态都是Healthy;如果应用的整体健康状态为Degraded,就意味着至少有一个服务的健康状态为Degraded,并且没有Unhealthy;如果其中某个服务的健康状态为Unhealthy,应用的整体健康状态就是Unhealthy。

[S3005]定制健康报告响应内容

上面演示的实例虽然注册了相应的IHealthCheck对象来检验独立服务的健康状况,但是最终得到的依然是应用的整体健康状态,我们更希望得到一份详细的针对所有服务的“健康诊断书”。所以,我们将演示程序做了如下所示的改写。我们为Check方法返回的表示健康检查结果的HealthCheckResult对象设置了对应的描述性文字(Normal、Degraded和Unavailable)。我们在调用AddCheck方法时指定了两个标签(Tag),如针对服务foo的IHealthCheck对象的标签设置为foo1和foo2。在调用UseHealthChecks扩展方法注册HealthCheckMiddleware中间件时,我们提供了HealthCheckOptions配置选项,通过之后后者的 ResponseWriter 属性完成了健康报告的呈现。

...
var options = new HealthCheckOptions
    ResponseWriter = ReportAsync
var builder = WebApplication.CreateBuilder();
builder.Services.AddHealthChecks()
    .AddCheck(name: "foo", check: Check,tags: new string[] { "foo1", "foo2" })
    .AddCheck(name: "bar", check: Check, tags: new string[] { "bar1", "bar2" })
    .AddCheck(name: "baz", check: Check, tags: new string[] { "baz1", "baz2" });
var app = builder.Build();
app.UseHealthChecks(path: "/healthcheck", options: options);
app.Run();
static Task ReportAsync(HttpContext context, HealthReport report)
    context.Response.ContentType = "application/json";
    var options = new JsonSerializerOptions();
    options.WriteIndented = true;
    options.Converters.Add(new JsonStringEnumConverter());
    return context.Response.WriteAsync(JsonSerializer.Serialize(report, options));

HealthCheckOptions配置选项的ResponseWriter属性返回一个Func<HttpContext, HealthReport, Task>委托,显示的健康报告通过HealthReport对象标识。提供委托指向的ReportAsync会直接将指定的HealthReport对象序列化成JSON格式并作为响应的主体内容。我们并没有设置相应的状态码,所以可以直接在浏览器中看到图2所示的这份完整的健康报告。

图2 完整的健康报告

[S3006]IHealthCheck对象的过滤

HealthCheckMiddleware中间件提取注册的IHealthCheck对象在完成具体的健康检查工作之前,我们可以对它们做进一步过滤。前面演示的实例注册的IHealthCheck对象指定了相应的标签,该标签不仅会出现在健康报告中,我们可以使用它们作为过滤条件。如下的演示程序通过设置HealthCheckOptions配置选项的 Predicate 属性使之选择Tag前缀不为“baz”的IHealthCheck对象。

...
var options = new HealthCheckOptions
    ResponseWriter = ReportAsync,
    Predicate = reg => reg.Tags.Any(tag => !tag.StartsWith("baz", StringComparison.OrdinalIgnoreCase))

由于我们设置的过滤规则相当于忽略了针对服务baz的健康检查,所以如图3所示的健康报告时就看不到对应的健康状态。

图3 部分IHealthCheck过滤后的健康报告

[S3007]定期发布健康报告

健康报告的发布是通过 IHealthCheckPublisher 服务来完成的,我们演示的程序定义了如下这个实现了该接口的ConsolePublisher类型,它会将健康报告输出到控制台上。

using Microsoft.Extensions.Diagnostics.HealthChecks;
var random = new Random();
var builder = WebApplication.CreateBuilder();
builder.Logging.ClearProviders();
builder.Services
    .AddHealthChecks()
    .AddCheck("foo", Check)
    .AddCheck("bar", Check)
    .AddCheck("baz", Check)
    .AddConsolePublisher()
    .ConfigurePublisher(options =>options.Period = TimeSpan.FromSeconds(5));
var app = builder.Build();
app.UseHealthChecks(path: "/healthcheck");
app.Run();
HealthCheckResult Check() => random!.Next(1, 4) switch
    1 => HealthCheckResult.Unhealthy(),
    2 => HealthCheckResult.Degraded(),
    _ => HealthCheckResult.Healthy(),

上面的演示程序注册了三个DelegateHealthCheck对象,它们会随机返回针对三种状态的健康状态。ConsolePublisher通过自定义的AddConsolePublisher扩展方法进行注册,紧随其后调用的ConfigurePublisher方法也是自定义的扩展方法,我们利用它将健康报告发布间隔设置为5秒。程序运行之后,当前应用的健康报告会以图4所示的形式输出到控制台上。

图4 健康报告的定期发布