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.