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