ASP.NET Core 6框架揭祕範例演示[42]:檢查應用的健康狀況

2023-07-06 12:01:12

現代化的應用及服務的部署場景主要體現在叢集化、微服務和容器化,這一切都建立在針對部署應用或者服務的健康檢查上。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 健康報告的定期釋出