在.NET Core Worker Service中执行健康检查

43

如何在.NET Core Worker Service中实现健康检查?

服务将在Docker内运行,需要能够检查服务的健康状况。

7个回答

14
另一种方法是实现 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;
  }
}

1
谢谢,这提供了我解决方案的一些很好的灵感!https://dev59.com/z8Hqa4cB1Zd3GeqPtiVd#68069909 我删除了 if (!fileExists),以便每次都会触发文件更新,这样 k8s 就可以检查最后修改时间(以防应用程序冻结)。 - silent

11
我认为更改SDK为Microsoft.NET.Sdk.Web并不值得。仅因一个健康检查而包含额外的中间件?不,谢谢...
你可以使用不同的协议,如TCP。
总体思路是:
  1. 创建一个单独的后台服务,创建一个TCP服务器(请参阅TcpListener.cs
  2. 当您收到请求时,您有两个选项:如果应用程序健康,则接受TCP连接,否则拒绝它。
  3. 如果您使用容器,则编排器应该有一个选项来通过TCP调用它(在k8s中有一个属性tcpSocket
如果需要更详细的信息,可以查看:Monitoring Health of ASP.NET Core Background Services With TCP Probes on Kubernetes 干杯!

我认为这是在使用Kubernetes时不使用Microsoft.NET.Sdk.Web的最优雅的方法。您甚至可以打开两个不同的TCP端口,一个用于_live_端点,另一个用于_ready_端点。 - MÇT
人们应该知道的一件事是,在 .net 7 中,他们应该使用 Microsoft.Extensions.Diagnostics.HealthChecks 包。从这里获取:https://www.nuget.org/packages/Microsoft.Extensions.Diagnostics.HealthChecks/ - Frantz Paul

7

添加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();
            }
        }
    }

services.AddHostedService<Worker>(); 是做什么的? - pedrommuller
1
在我的情况下,这是Kafka消费者的实现。您可以在此处阅读有关工作程序的更多信息:https://learn.microsoft.com/en-us/dotnet/core/extensions/workers - kaminzo

5

我认为你应该考虑保留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);
})

2
我为了达成这个目标,将 Microsoft.NET.Sdk.Web 添加到我的 Worker 中,并配置了一个 Web 主机来与 Worker 并行运行:
Host.CreateDefaultBuilder(args)
    .ConfigureWebHostDefaults(builder =>
    {
        builder.UseStartup<Startup>();
    })
    .ConfigureServices((hostContext, services) =>
    {
        services.AddHostedService<Worker>();
        services.AddLogging(builder =>
            builder
                .AddDebug()
                .AddConsole()
        );
    });

完成这些操作后,唯一剩下的就是像通常在ASP.NET Core中一样映射健康检查端点。请记得保留HTML标签。

这会报告工作服务的错误线程吗?我的意思是,假设工作服务由于未处理的异常而无法执行任务并卡住了,而/health端点不考虑它,而总是响应200ok,因为它在完全不同的线程中运行。 - Ranvir
不,你必须从健康检查中检测出服务停止响应的情况。也许可以从工作进程中获取心跳信号,但即使如此,我也见过它并不完全准确的情况。 - marcroussy

1
避免使用HTTP方法;只需在容器内部触摸一个文件,并运行基于文件的健康检查 - 这在纯Docker或编排器中都能很好地工作。
创建一个托管服务,定期触摸文件/tmp/myapp-healthcheck。
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'

或者更新编排器配置。
这将每30秒触发/tmp/myapp-healthcheck(TimeSpan.FromSeconds(30))。当Docker健康检查运行时,它会尝试找到在最近30秒内创建/更新的匹配文件(-mmin 0.5)。
这是最基本的实现。可以添加错误处理,并将路径和周期提取到配置设置中,使其更加优雅。

0
我在我的工作进程中运行了以下代码。在我看来,它相对于这里描述的其他解决方案具有以下优点:
- 它包含完全可用于生产的代码。 - 如果工作进程不健康,它可靠地失败健康检查。 - 例如,Veikedo的解决方案似乎依赖于文件的存在。如果工作进程完全崩溃,文件可能仍然存在。因此,即使进程已经不存在,健康检查仍然通过。 - 它完全依赖于Microsoft.Extensions.Diagnostics.HealthChecks基础设施,不使用任何自定义托管服务。 - 因此,它包含了许多附加健康检查可能提供的健康信息。 - 它不涉及任何打开的端口/TCP监听器/HTTP服务器或其他网络交互。 - 此答案包括如何在您的Dockerfile中设置健康检查的说明(由lonix提供)。
首先,创建一个`FileHealthCheckPublisher`类。
请注意,我使用NodaTime的IClock接口来使其可测试。您可以轻松地删除IClock依赖项,并改为编写`File.SetLastWriteTimeUtc(path, DateTime.UtcNow);`。请参阅@lonix's的答案。
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
        }
    }
}

然后,在您的Dockerfile中添加这行代码

我过滤了最近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>();

参见:Microsoft Docs on ASP.NET Core中的健康检查 我从@Veikedo@lonix的回答中获得了很多启发,谢谢!

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接