【问题标题】:Performing a health check in .NET Core Worker Service在 .NET Core Worker 服务中执行健康检查
【发布时间】:2022-01-17 22:08:27
【问题描述】:

如何在 .NET Core Worker 服务中实施健康检查?

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

【问题讨论】:

    标签: c# docker .net-core service


    【解决方案1】:

    另一种方法是实现IHealthCheckPublisher

    这种方法的好处是能够重复使用现有的IHealthChecks 或与依赖IHealthCheck 接口的第三方库(如this one)集成。

    尽管您仍然将 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;
      }
    }
    

    【讨论】:

    • 谢谢,这为我的解决方案提供了一些很好的灵感! stackoverflow.com/a/68069909/1537195 我删除了 if (!fileExists) 以便每次都触摸文件,因此 k8s 可以实际检查上次修改时间(以防应用程序冻结)
    【解决方案2】:

    我认为将 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 端点。
    【解决方案3】:

    我认为你也应该考虑保留 Microsoft.NET.Sdk.Worker。

    不要仅仅因为健康检查而改变整个 sdk。

    然后您可以创建一个后台服务(就像主要工作人员一样),以便更新文件以写入例如当前时间戳。后台运行状况检查工作人员的示例如下:

    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);
    })
    

    【讨论】:

      【解决方案4】:

      我所做的就是将 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()
              );
          });
      

      完成后,剩下要做的就是map the health check endpoint,就像您通常使用 ASP.NET Core 一样。

      【讨论】:

      • 这个会报worker服务的故障线程吗?我的意思是假设工作服务由于未处理的异常和卡住而无法执行任务,而 /health 端点不认为它总是响应 200ok,因为它一起在不同的线程中运行。
      • 不,您必须以某种方式从运行状况检查中检测到服务已停止响应。也许是工人的心跳,但即便如此,我也看到过一些不完全准确的案例。
      【解决方案5】:

      添加 HTTPListener 并公开健康检查端点。

      使用 HTTPListener 不需要添加 Microsoft.NET.Sdk.Web SDK。

      程序.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();
                  }
              }
          }
      

      【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-06-05
      • 1970-01-01
      • 2014-10-02
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多