.NET Kubernetes DevOps Observabilidade ASP.NET Core Banco de Dados

Health checks no .NET: liveness, readiness e startup probes para Kubernetes

Guia completo de health checks no ASP.NET Core: checks customizados para banco, Redis e filas, endpoints distintos para Kubernetes.

N
Neryx Digital Architects
23 de novembro de 2025
11 min de leitura
170 profissionais leram
Categoria: .NET Público: Times de plataforma e operação Etapa: Decisão

Um container que responde 200 na raiz não significa que a aplicação está saudável. Ela pode estar conectada mas com o pool de conexões do banco esgotado, o Redis inacessível ou a fila de jobs represada. Health checks bem configurados fazem o Kubernetes e o seu load balancer tomarem decisões certas — reiniciar o que está quebrado, tirar do tráfego o que não está pronto, e dar tempo para o container inicializar.

Os três tipos de probe do Kubernetes

  • Liveness probe — "O container ainda está vivo?" Se falhar, Kubernetes reinicia o pod. Use para detectar deadlocks e estados irrecuperáveis.
  • Readiness probe — "O container está pronto para receber tráfego?" Se falhar, o pod é retirado do Service (sem reinício). Use para dependências externas e warm-up.
  • Startup probe — "O container terminou de inicializar?" Substitui liveness durante o startup. Use para aplicações com inicialização lenta (migrations, caches pré-carregados).

Setup básico

dotnet add package Microsoft.Extensions.Diagnostics.HealthChecks
dotnet add package AspNetCore.HealthChecks.SqlServer    # SQL Server
dotnet add package AspNetCore.HealthChecks.NpgSql       # PostgreSQL
dotnet add package AspNetCore.HealthChecks.Redis         # Redis
dotnet add package AspNetCore.HealthChecks.RabbitMQ     # RabbitMQ
dotnet add package AspNetCore.HealthChecks.Aws.S3       # AWS S3
dotnet add package AspNetCore.HealthChecks.UI           # Dashboard visual
dotnet add package AspNetCore.HealthChecks.UI.Client    # Relatório JSON para UI
// Program.cs — configuração completa
builder.Services
    .AddHealthChecks()
    // Checks de infraestrutura crítica (afetam liveness E readiness)
    .AddNpgSql(
        connectionString: builder.Configuration.GetConnectionString("Default")!,
        name: "postgres",
        tags: ["live", "ready", "db"])

    .AddRedis(
        redisConnectionString: builder.Configuration.GetConnectionString("Redis")!,
        name: "redis",
        tags: ["live", "ready", "cache"])

    .AddRabbitMQ(
        rabbitConnectionString: builder.Configuration.GetConnectionString("RabbitMQ")!,
        name: "rabbitmq",
        tags: ["ready", "messaging"]) // Só readiness — ficar sem fila não é fatal

    // Checks customizados
    .AddCheck<DatabaseConnectionPoolHealthCheck>("db-pool", tags: ["live", "ready"])
    .AddCheck<PendingMigrationsHealthCheck>("migrations", tags: ["startup"])
    .AddCheck<ExternalApiHealthCheck>("payment-gateway", tags: ["ready"])
    .AddCheck<DiskSpaceHealthCheck>("disk-space", tags: ["live"]);

Endpoints separados por tipo de probe

// app configuração no Program.cs
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    // Liveness: apenas checks que indicam estado irrecuperável
    Predicate = check => check.Tags.Contains("live"),
    ResultStatusCodes =
    {
        [HealthStatus.Healthy] = StatusCodes.Status200OK,
        [HealthStatus.Degraded] = StatusCodes.Status200OK,   // Degraded NÃO reinicia
        [HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable
    }
});

app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    // Readiness: tudo que impede servir tráfego corretamente
    Predicate = check => check.Tags.Contains("ready"),
    ResultStatusCodes =
    {
        [HealthStatus.Healthy] = StatusCodes.Status200OK,
        [HealthStatus.Degraded] = StatusCodes.Status200OK,
        [HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable
    }
});

app.MapHealthChecks("/health/startup", new HealthCheckOptions
{
    // Startup: verifica que a inicialização foi concluída
    Predicate = check => check.Tags.Contains("startup"),
    ResultStatusCodes =
    {
        [HealthStatus.Healthy] = StatusCodes.Status200OK,
        [HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable
    }
});

// Endpoint completo para monitoramento interno (com detalhes JSON)
app.MapHealthChecks("/health", new HealthCheckOptions
{
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse // JSON detalhado
}).RequireAuthorization("InternalOnly"); // Proteger com auth para não expor dados

Health check customizado: pool de conexões do banco

// Verifica não só se o banco responde, mas se o pool não está esgotado
public class DatabaseConnectionPoolHealthCheck : IHealthCheck
{
    private readonly IDbConnectionFactory _connectionFactory;
    private readonly ILogger<DatabaseConnectionPoolHealthCheck> _logger;

    public DatabaseConnectionPoolHealthCheck(
        IDbConnectionFactory connectionFactory,
        ILogger<DatabaseConnectionPoolHealthCheck> logger)
    {
        _connectionFactory = connectionFactory;
        _logger = logger;
    }

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        var sw = Stopwatch.StartNew();

        try
        {
            await using var connection = _connectionFactory.CreateConnection();

            // Timeout curto — se demorar mais que 2s, pool provavelmente esgotado
            using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
            cts.CancelAfter(TimeSpan.FromSeconds(2));

            await connection.OpenAsync(cts.Token);

            await using var command = connection.CreateCommand();
            command.CommandText = "SELECT 1";
            command.CommandTimeout = 2;
            await command.ExecuteScalarAsync(cts.Token);

            sw.Stop();

            var data = new Dictionary<string, object>
            {
                ["responseTimeMs"] = sw.ElapsedMilliseconds
            };

            // Degraded se resposta lenta (pool com pressão mas ainda funcional)
            if (sw.ElapsedMilliseconds > 500)
            {
                return HealthCheckResult.Degraded(
                    $"Banco respondeu em {sw.ElapsedMilliseconds}ms (lento)", data: data);
            }

            return HealthCheckResult.Healthy(
                $"Banco respondeu em {sw.ElapsedMilliseconds}ms", data: data);
        }
        catch (OperationCanceledException)
        {
            return HealthCheckResult.Unhealthy("Timeout ao conectar ao banco — pool possivelmente esgotado");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Health check do banco falhou");
            return HealthCheckResult.Unhealthy($"Banco inacessível: {ex.Message}");
        }
    }
}

Health check customizado: API externa com degraded

// Verifica API de pagamento — se estiver fora, somos Degraded (não Unhealthy)
// Unhealthy pararia o tráfego; Degraded apenas sinaliza problema sem parar
public class ExternalApiHealthCheck : IHealthCheck
{
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly ILogger<ExternalApiHealthCheck> _logger;

    public ExternalApiHealthCheck(
        IHttpClientFactory httpClientFactory,
        ILogger<ExternalApiHealthCheck> logger)
    {
        _httpClientFactory = httpClientFactory;
        _logger = logger;
    }

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        try
        {
            var client = _httpClientFactory.CreateClient("PaymentGateway");
            var sw = Stopwatch.StartNew();

            var response = await client.GetAsync("/health", cancellationToken);
            sw.Stop();

            if (response.IsSuccessStatusCode)
            {
                return HealthCheckResult.Healthy(
                    $"Gateway de pagamento respondeu {(int)response.StatusCode} em {sw.ElapsedMilliseconds}ms");
            }

            // API externa retornou erro — Degraded (não para o tráfego, apenas alerta)
            return HealthCheckResult.Degraded(
                $"Gateway de pagamento retornou {(int)response.StatusCode}",
                data: new Dictionary<string, object>
                {
                    ["statusCode"] = (int)response.StatusCode,
                    ["responseTimeMs"] = sw.ElapsedMilliseconds
                });
        }
        catch (HttpRequestException ex)
        {
            _logger.LogWarning("Gateway de pagamento inacessível: {Message}", ex.Message);

            // HttpRequestException = gateway inacessível — Degraded, não Unhealthy
            // Nossa API ainda pode funcionar (com fallback / circuit breaker)
            return HealthCheckResult.Degraded(
                $"Gateway de pagamento inacessível: {ex.Message}");
        }
        catch (TaskCanceledException)
        {
            return HealthCheckResult.Degraded("Gateway de pagamento: timeout");
        }
    }
}

Health check de espaço em disco

public class DiskSpaceHealthCheck : IHealthCheck
{
    private const long WarningThresholdBytes = 1L * 1024 * 1024 * 1024;   // 1 GB
    private const long CriticalThresholdBytes = 500L * 1024 * 1024;        // 500 MB

    public Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        var drive = DriveInfo.GetDrives()
            .FirstOrDefault(d => d.IsReady && d.Name == "/");

        if (drive is null)
            return Task.FromResult(HealthCheckResult.Healthy("Drive não monitorado"));

        var freeBytes = drive.AvailableFreeSpace;
        var totalBytes = drive.TotalSize;
        var usedPercent = (double)(totalBytes - freeBytes) / totalBytes * 100;

        var data = new Dictionary<string, object>
        {
            ["freeGb"] = Math.Round(freeBytes / (1024.0 * 1024 * 1024), 2),
            ["totalGb"] = Math.Round(totalBytes / (1024.0 * 1024 * 1024), 2),
            ["usedPercent"] = Math.Round(usedPercent, 1)
        };

        if (freeBytes < CriticalThresholdBytes)
            return Task.FromResult(HealthCheckResult.Unhealthy(
                $"Disco crítico: apenas {freeBytes / 1024 / 1024}MB livres ({usedPercent:F1}% usado)",
                data: data));

        if (freeBytes < WarningThresholdBytes)
            return Task.FromResult(HealthCheckResult.Degraded(
                $"Disco com pouco espaço: {freeBytes / 1024 / 1024 / 1024}GB livres ({usedPercent:F1}% usado)",
                data: data));

        return Task.FromResult(HealthCheckResult.Healthy(
            $"Disco OK: {freeBytes / 1024 / 1024 / 1024}GB livres ({usedPercent:F1}% usado)",
            data: data));
    }
}

Configuração no Kubernetes

# deployment.yaml
spec:
  containers:
    - name: orderservice
      image: myregistry/orderservice:latest
      ports:
        - containerPort: 8080

      # Startup probe — dá até 5min para a app inicializar (migrations, etc.)
      startupProbe:
        httpGet:
          path: /health/startup
          port: 8080
        failureThreshold: 30     # 30 tentativas
        periodSeconds: 10        # A cada 10s = 5min máximo de startup
        successThreshold: 1

      # Liveness probe — verifica se o container está vivo
      livenessProbe:
        httpGet:
          path: /health/live
          port: 8080
        initialDelaySeconds: 0   # Começa após startup probe passar
        periodSeconds: 10        # Verifica a cada 10s
        timeoutSeconds: 5        # Timeout de 5s
        failureThreshold: 3      # 3 falhas consecutivas = reinicia
        successThreshold: 1

      # Readiness probe — controla se recebe tráfego
      readinessProbe:
        httpGet:
          path: /health/ready
          port: 8080
        initialDelaySeconds: 0
        periodSeconds: 10
        timeoutSeconds: 5
        failureThreshold: 3      # 3 falhas = sai do Service (sem reinício)
        successThreshold: 2      # Precisa de 2 sucessos consecutivos para entrar

      resources:
        requests:
          memory: "256Mi"
          cpu: "100m"
        limits:
          memory: "512Mi"
          cpu: "500m"

Dashboard de Health Checks

// Program.cs — dashboard visual (apenas em ambiente não-produção ou com auth)
builder.Services
    .AddHealthChecksUI(settings =>
    {
        settings.SetEvaluationTimeInSeconds(15); // Verifica a cada 15s
        settings.MaximumHistoryEntriesPerEndpoint(50);

        settings.AddHealthCheckEndpoint("OrderService", "/health");
        settings.AddHealthCheckEndpoint("CatalogService", "http://catalog-service/health");
    })
    .AddInMemoryStorage();

// Expor apenas em ambientes não-produção (ou com auth)
if (!app.Environment.IsProduction())
{
    app.MapHealthChecksUI(options =>
    {
        options.UIPath = "/health-ui";
        options.ApiPath = "/health-ui-api";
    });
}

// Acesse: http://localhost:8080/health-ui

Resposta JSON estruturada

// GET /health — resposta do UIResponseWriter
{
  "status": "Healthy",
  "totalDuration": "00:00:00.0423891",
  "entries": {
    "postgres": {
      "data": { "responseTimeMs": 12 },
      "description": "Banco respondeu em 12ms",
      "duration": "00:00:00.0182345",
      "status": "Healthy",
      "tags": ["live", "ready", "db"]
    },
    "redis": {
      "data": {},
      "description": null,
      "duration": "00:00:00.0021234",
      "status": "Healthy",
      "tags": ["live", "ready", "cache"]
    },
    "payment-gateway": {
      "data": { "statusCode": 200, "responseTimeMs": 234 },
      "description": "Gateway de pagamento respondeu 200 em 234ms",
      "duration": "00:00:00.0234567",
      "status": "Healthy",
      "tags": ["ready"]
    }
  }
}

Regra de ouro: o que usar em cada probe

Dependência Liveness Readiness Startup
Banco de dados principal
Redis / cache
Fila de mensagens
API externa (pagamento) ✅ (Degraded)
Migrations pendentes
Espaço em disco
Pool de conexões DB

Health checks bem configurados são a diferença entre descobrir problemas pelo cliente ou pelo seu sistema de monitoramento. Se você quer implementar observabilidade de produção na sua stack .NET, a Neryx pode ajudar.

Quer sair do modo reativo e priorizar o que mais importa?

O diagnóstico de maturidade ajuda a transformar sintomas operacionais em um plano mais claro de evolução.

Avaliar maturidade

Newsletter

Receba artigos como este no seu e-mail

Conteúdo técnico sobre arquitetura de software, .NET, IA e gestão de produto. Sem spam.