Usando o Liveness e Readiness com Asp.net Core
Opa, depois de algum tempo parado resolvi voltar a escrever e vou começar com um assunto bem na moda… Kubernetes. Tenho visto muitas pessoas usando o Kubernetes, mas não sabem bem como ele funciona. Então vou começar apresentando o Liveness e o Readiness e como usá-los em uma aplicação Asp.Net Core.
Antes de começarmos vamos entender o que são Liveness e Readiness e como o Kubernetes trata cada um deles.
Liveness — é usado para indicar que a sua aplicação está viva para o Kubernetes e caso não esteja faz com que o Kubernetes remova o pod e coloque outro no lugar.
Readiness — é usado para indicar ao Kubernetes que a sua aplicação está pronta para receber as requisições e neste caso o Kubernetes através do seu balanceamento de carga começa a enviar as requisições para ele.
Para definirmos o liveness e readiness em nosso deployment, por exemplo, precisamos incluir as seções readinessProbe e livenessProbe em nosso YAML na seção spec/container.
readinessProbe:
httpGet:
path: /health/readiness
port: 80
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health/liveness
port: 80
initialDelaySeconds: 15
periodSeconds: 20
timeoutSeconds: 5
Agora que entendemos os conceitos e como o Kubernetes trata cada um deles, como conseguimos integrar com o Asp.Net Core?
Criaremos uma aplicação Web Api e adicionaremos o pacote para health check Microsoft.AspNetCore.Diagnostics.HealthChecks.
dotnet new webapi --name HelthCkeckExemplo
dotnet add package Microsoft.AspNetCore.Diagnostics.HealthChecks
Agora vamos registrar o serviço de Health Check para a nossa aplicação e criar um endpoint que será executado pelo Kubernetes para verificar a saúde de nossa aplicação.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddHealthChecks();
}public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHealthChecks("/health/liveness", new
HealthCheckOptions()
{
ResponseWriter = WriteResponseLiveness
});
});
}private Task WriteResponseLiveness(HttpContext context, HealthReport result)
{
context.Response.ContentType = "application/json; charset=utf-8";
System.Console.WriteLine($"{ DateTime.Now } - Health");
return context.Response.WriteAsync("Health");
}
O código acima habilita a nossa aplicação a responder as chamadas de Health Check escrevendo no console. A ideia é conseguir acompanhar as chamadas e verificar se o Kubernetes está realmente chamando o nosso endpoint.
Vamos ver na prática as implicações do liveness.
Alterando o código para colocar um Delay de 10 segundos, pois definimos em nosso deploy um timeout de 5 segundos (timeoutSeconds). Caso você não defina o timeoutSeconds, lembre-se que o default é de 1 segundo, então muito cuidado, pois dependendo do tempo que o seu endpoint demore para responder a sua aplicação poderá sofrer bastante com isso.
private static Task WriteResponseLiveness(HttpContext context, HealthReport result)
{
context.Response.ContentType = "application/json; charset=utf-8";
System.Console.WriteLine($"{ DateTime.Now } - Health");
Task.Delay(10000).Wait(); //feio.. eu sei!
System.Console.WriteLine($"{ DateTime.Now } - Health");
return context.Response.WriteAsync("Health");
}
O POD foi reiniciado 2 vezes em 3 minutos, mas como é feita essa conta? Vamos as explicações… Colocamos o nosso timeoutSeconds para 5 segundos e o nosso endpoint leva 10 segundos para executar a verificação de liveness que é feita a cada 20 segundos e precisamos falhar 3 vezes (valor default quando não informamos o valor para failureThreshold), sendo assim a cada 20 segundos teremos uma falha que demora 10 segundos então em 90 segundos teremos o nosso primeiro RESTART, ou seja, o nosso POD será reiniciado a cada 90 segundos deixando a nossa aplicação indisponível.
O objetivo do liveness é informar que a nossa aplicação está viva, então temos que ser objetivos para conseguir responder em um tempo aceitável, caso contrário teremos um efeito indesejável em nossa aplicação.
Vamos alterar o nosso código para fazer uma verificação de conectividade no SQL Server e neste caso vamos abrir uma conexão com o SQL Server e executar uma consulta SELECT 1, apenas para verificar se estamos com acesso ao banco de dados.
private Task WriteResponseLiveness(HttpContext context, HealthReport result)
{
context.Response.ContentType = "application/json; charset=utf-8";
System.Console.WriteLine("Iniciando o Health Liveness");
System.Console.WriteLine($"{ DateTime.UtcNow } - Health"); using var conexao = new SqlConnection(
Configuration["ConnectionStrings:MinhaStringConexao"]);
try
{
conexao.Open();
using var comando = new SqlCommand("SELECT 1", conexao);
comando.ExecuteNonQuery();
conexao.Close();
}
catch
{
throw;
}
System.Console.WriteLine($"{ DateTime.UtcNow } - Health");
return context.Response.WriteAsync("Liveness");
}
Sempre que pensar em uma consulta ao banco de dados, pense em alguma que não sobrecarregue o banco ou sofra com alguma concorrência, por isso no exemplo optamos por executar um SELECT 1 ao invés de acessar as tabelas do nosso sistema.
Agora que entendemos o liveness e vimos como devemos utilizá-lo, vamos falar do readiness. Será através dele que informaremos ao Kubernetes que a nossa aplicação está pronta para receber o tráfego.
Quando incluímos o readinessProbe na spec do container o Kubernetes inicializa o POD e não direciona tráfego para ele. O mesmo só receberá o tráfego quando for sinalizado para o Kubernetes que a inicialização foi concluída. Devemos utilizar o readiness sempre que a nossa aplicação necessitar de uma inicialização e essa for demorada (arquivos de configuração, carga de dados para cache e até migrations), garantindo que a mesma não receberá tráfego durante essa inicialização evitando um comportamento indesejado em nossa aplicação.
private Task WriteResponseReadiness(HttpContext context, HealthReport result)
{
context.Response.ContentType = "application/json; charset=utf-8";
System.Console.WriteLine("Iniciando o Health Readiness");
System.Console.WriteLine($"{ DateTime.UtcNow } - Health");
System.Console.WriteLine($"{ DateTime.UtcNow } - Health");
return context.Response.WriteAsync("Readiness");
}
Opa!!! Mas o readiness é executado várias vezes? Sim! Então nem pense em colocar o seu código de inicialização neste endpoint.
O readiness segue a mesma configuração e comportamento do liveness, sendo um valor de delay inicial (initialDeplaySeconds), um valor para informar de quanto em quanto tempo o Kubernetes deve invocar o nosso endpoint (periodSeconds) e um tempo para ele esperar até considerar como falha (timeoutSeconds). No caso de falha ele considerará 3 falhas (failureThreshold) para declarar que o container não foi inicializado com sucesso e neste caso ele irá remover o container. Então cuidado com os tempos definidos e com a implementação deste endpoint, você poderá não ter a sua aplicação no ar.
Código Final! (Não fiquei preocupado com as boas práticas de programação, o objetivo é demonstrar o funcionamento do Liveness e do Readiness)
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddHealthChecks();
}public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
} app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHealthChecks("/health/liveness", new
HealthCheckOptions()
{
ResponseWriter = WriteResponseLiveness
});
endpoints.MapHealthChecks("/health/readiness", new
HealthCheckOptions()
{
ResponseWriter = WriteResponseReadiness
});
});
}private Task WriteResponseLiveness(HttpContext context, HealthReport result)
{
context.Response.ContentType = "application/json; charset=utf-8";
System.Console.WriteLine("Iniciando o Health Liveness");
System.Console.WriteLine($"{ DateTime.UtcNow } - Health");
//Task.Delay(10000).Wait(); using var conexao = new SqlConnection(
Configuration["ConnectionStrings:MinhaStringConexao"]);
try
{
conexao.Open();
using var comando = new SqlCommand("SELECT 1", conexao);
comando.ExecuteNonQuery();
conexao.Close();
}
catch
{
throw;
} System.Console.WriteLine($"{ DateTime.UtcNow } - Health");
return context.Response.WriteAsync("Liveness");
}private Task WriteResponseReadiness(HttpContext context, HealthReport result)
{
context.Response.ContentType = "application/json; charset=utf-8";
System.Console.WriteLine("Iniciando o Health Readiness");
System.Console.WriteLine($"{ DateTime.UtcNow } - Health");
System.Console.WriteLine($"{ DateTime.UtcNow } - Health");
return context.Response.WriteAsync("Readiness");
}
Conclusão
Quando estiver pensando em criar uma aplicação para ser executada em um Cluster Kubernetes pense em como você vai sinalizar para ele se a sua aplicação está pronta ou se está viva. Para isso implemente um HealthCheck nela e lembre de separar os endpoints, pois eles tem funções diferentes.