如何在.NET Core Worker Service中实现健康检查?
服务将在Docker内运行,需要能够检查服务的健康状况。
IHealthCheckPublisher
接口。这种方法的好处是可以重复使用现有的 IHealthCheck
,或者与依赖于 IHealthCheck
接口的第三方库(如 此库)集成。Microsoft.NET.Sdk.Web
作为 SDK,但不需要添加任何 asp.net 特定的内容。public static IHostBuilder CreateHostBuilder(string[] args)
{
return Host
.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services
.AddHealthChecks()
.AddCheck<RedisHealthCheck>("redis_health_check")
.AddCheck<RfaHealthCheck>("rfa_health_check");
services.AddSingleton<IHealthCheckPublisher, HealthCheckPublisher>();
services.Configure<HealthCheckPublisherOptions>(options =>
{
options.Delay = TimeSpan.FromSeconds(5);
options.Period = TimeSpan.FromSeconds(5);
});
});
}
public class HealthCheckPublisher : IHealthCheckPublisher
{
private readonly string _fileName;
private HealthStatus _prevStatus = HealthStatus.Unhealthy;
public HealthCheckPublisher()
{
_fileName = Environment.GetEnvironmentVariable(EnvVariableNames.DOCKER_HEALTHCHECK_FILEPATH) ??
Path.GetTempFileName();
}
public Task PublishAsync(HealthReport report, CancellationToken cancellationToken)
{
// AWS will check if the file exists inside of the container with the command
// test -f $DOCKER_HEALTH_CHECK_FILEPATH
var fileExists = _prevStatus == HealthStatus.Healthy;
if (report.Status == HealthStatus.Healthy)
{
if (!fileExists)
{
using var _ = File.Create(_fileName);
}
}
else if (fileExists)
{
File.Delete(_fileName);
}
_prevStatus = report.Status;
return Task.CompletedTask;
}
}
Microsoft.Extensions.Diagnostics.HealthChecks
包。从这里获取:https://www.nuget.org/packages/Microsoft.Extensions.Diagnostics.HealthChecks/ - Frantz Paul添加HTTPListener并公开健康检查端点。
使用HTTPListener不需要添加Microsoft.NET.Sdk.Web SDK。
Program.cs
using Consumer;
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddHostedService<Worker>();
services.AddHostedService<HttpHealthcheck>();
})
.Build();
await host.RunAsync();
HttpHealthcheck.cs
using System.Net;
using System.Text;
namespace Consumer;
public class HttpHealthcheck : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly HttpListener _httpListener;
private readonly IConfiguration _configuration;
public HealthcheckHttpListener(ILogger<Worker> logger, IConfiguration configuration)
{
_logger = logger;
_configuration = configuration;
_httpListener = new HttpListener();
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_httpListener.Prefixes.Add($"http://*:5001/healthz/live/");
_httpListener.Prefixes.Add($"http://*:5001/healthz/ready/");
_httpListener.Start();
_logger.LogInformation($"Healthcheck listening...");
while (!stoppingToken.IsCancellationRequested)
{
HttpListenerContext ctx = null;
try
{
ctx = await _httpListener.GetContextAsync();
}
catch (HttpListenerException ex)
{
if (ex.ErrorCode == 995) return;
}
if (ctx == null) continue;
var response = ctx.Response;
response.ContentType = "text/plain";
response.Headers.Add(HttpResponseHeader.CacheControl, "no-store, no-cache");
response.StatusCode = (int)HttpStatusCode.OK;
var messageBytes = Encoding.UTF8.GetBytes("Healthy");
response.ContentLength64 = messageBytes.Length;
await response.OutputStream.WriteAsync(messageBytes, 0, messageBytes.Length);
response.OutputStream.Close();
response.Close();
}
}
}
我认为你应该考虑保留Microsoft.NET.Sdk.Worker。
不要因为健康检查而更改整个sdk。
然后,你可以创建一个BackgroundService(就像主Worker一样),以便更新一个文件,例如写入当前时间戳。Background健康检查worker的示例代码如下:
public class HealthCheckWorker : BackgroundService
{
private readonly int _intervalSec;
private readonly string _healthCheckFileName;
public HealthCheckWorker(string healthCheckFileName, int intervalSec)
{
this._intervalSec = intervalSec;
this._healthCheckFileName = healthCheckFileName;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (true)
{
File.WriteAllText(this._healthCheckFileName, DateTime.UtcNow.ToString());
await Task.Delay(this._intervalSec * 1000, stoppingToken);
}
}
}
public static class HealthCheckWorkerExtensions
{
public static void AddHealthCheck(this IServiceCollection services,
string healthCheckFileName, int intervalSec)
{
services.AddHostedService<HealthCheckWorker>(x => new HealthCheckWorker(healthCheckFileName, intervalSec));
}
}
使用此功能,您可以在服务中添加健康检查支持。
.ConfigureServices(services =>
{
services.AddHealthCheck("hc.txt", 5);
})
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(builder =>
{
builder.UseStartup<Startup>();
})
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<Worker>();
services.AddLogging(builder =>
builder
.AddDebug()
.AddConsole()
);
});
public class FileBasedHealthCheckGenerator : BackgroundService {
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
Touch("/tmp/myapp-healthcheck");
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
}
private void Touch(string path)
{
using var fileStream = File.Open(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite);
File.SetLastWriteTimeUtc(path, DateTime.UtcNow);
}
}
Program.cs
中注册托管服务。services.AddHostedService<FileBasedHealthCheckGenerator>();
Dockerfile
:HEALTHCHECK --start-period=10s --interval=30s --timeout=10s --retries=3 \
CMD [ $(find '/tmp/myapp-healthcheck' -mmin 0.5 | wc -l) -eq 1 ] || exit 1
docker-compose.yml
文件:healthcheck:
start_period: 10s
interval: 30s
timeout: 10s
retries: 3
test: '[ $(find '/tmp/myapp-healthcheck' -mmin 0.5 | wc -l) -eq 1 ] || exit 1'
internal sealed class FileHealthCheckPublisher : IHealthCheckPublisher
{
private readonly IClock _clock;
private readonly string _healthCheckFilePath = "/healthz";
public FileHealthCheckPublisher(IClock clock)
{
_clock = clock;
}
public Task PublishAsync(HealthReport report, CancellationToken cancellationToken)
{
if (report.Status == HealthStatus.Healthy)
{
Touch(_healthCheckFilePath);
}
else
{
Delete(_healthCheckFilePath);
}
return Task.CompletedTask;
}
private void Touch(string path)
{
using var fileStream = File.Open(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite);
var now = _clock.GetCurrentInstant();
File.SetLastWriteTimeUtc(path, now.ToDateTimeUtc());
}
private void Delete(string path)
{
try
{
File.Delete(path);
}
catch
{
// best effort delete; might not exist in the first place
}
}
}
我过滤了最近0.6秒内被触及的文件,而不是0.5秒,就像@lonix's的回答一样,以避免可能的边缘情况,即检查成功但恰好是30秒前。
HEALTHCHECK --start-period=10s --interval=30s --timeout=10s --retries=3 \
CMD [ $(find '/healthz' -mmin 0.6 | wc -l) -eq 1 ] || exit 1
Program.cs
/Startup.cs
/DI配置中注册服务。添加适用于您的用例的检查。我在这里使用Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore。社区集合AspNetCore.Diagnostics.HealthChecks也可能很有趣。builder.Services.AddHealthChecks()
.AddDbContextCheck<AppDbContext>();
builder.Services.AddSingleton<IHealthCheckPublisher, FileHealthCheckPublisher>();
if (!fileExists)
,以便每次都会触发文件更新,这样 k8s 就可以检查最后修改时间(以防应用程序冻结)。 - silent