ASP.NET Core 6框架揭秘实例演示[42]:检查应用的健康状况
现代化的应用及服务的部署场景主要体现在集群化、微服务和容器化,这一切都建立在针对部署应用或者服务的健康检查上。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();
图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 健康报告的定期发布