Si hacemos microservicios habremos llegado a un punto en el que tenemos que saber de forma continuada si nuestra aplicación está corriendo y no tiene ningún fallo que la haga estar caída.
Esta comprobación la podríamos hacer de forma manual, pero nos tiraríamos todo el día refrescando la página.
Lo que vamos a hacer es utilizar los health checks y así, automatizar dicha comprobación.
Índice
1 - Qué es un health check?
Un health check es lo que utilizamos para comprobar si una aplicación o parte de nuestra infraestructura está siendo ejecutada como debería.
Esta comprobación no se limita a su forma más básica, de que funciona o no, sino que además podemos incluir comprobaciones como el tiempo de respuesta, el uso de memoria o incluso la comprobación de las dependencias.
1.1 - Tipos de health checks
Puedes incluir tantos como quieras, y adaptarlos a tus necesidades tanto como quieras, los más comunes son:
- Comprobación básica, denominada “basic probe” el cual es un endpoint que devolverá si la aplicación responde o no.
- Comprobación básica con dependencias, Lo mismo que el anterior, pero comprobará que cada dependencia está siendo ejecutada correctamente. (Este tiene que ser construido manualmente); Dentro de las dependencias incluimos tanto otros microservicios como elementos de la infraestructura como pueden ser la base de datos, el service bus, etc.
- Comprobaciones de sistema: Desde mi experiencia personal, este tipo de comprobaciones se hacen sobre todo cuando desplegamos aplicaciones en la nube, ya sea serverless o en contenedores, para asegurarnos que no estamos asignando más recursos de los puramente necesarios. Solemos comprobar elementos como el uso de la CPU, la memoria, uso de disco, etc.
1.2 - Nivel de respuesta de un health check
Una vez tenemos los tipos pasamos a la respuesta
Por norma general utilizaremos 3 niveles distintos a la hora de responder:
- Healthy: todo funciona con normalidad
- Degraded: La aplicación funciona pero va muy lento, por ejemplo una llamada API que de normal tarda 120ms está tardando 5000.
- Unhealthy: la aplicación está caída o su funcionamiento no es el esperado.
2 - Cómo y cuándo utilizar health checks
Bajo mi punto de vista debemos tener health checks de todos los elementos de nuestro sistema. Tanto de las aplicaciones como de la infraestructura.
Esto quiere decir que en cada aplicación que vayamos a crear a partir de ahora, debemos crear un endpoint que compruebe exclusivamente que todo funciona.
Lo mismo aplica para los elementos de la infraestructura.Todos los servicios que vayamos a utilizar deberán tener que poder ser comprobados, y ten en cuenta que diferentes servicios utilizan diferentes formas de comprobación, aunque la gran mayoría serán comandos o peticiones HTTP.
Y debemos incluirlos siempre, la falla de un sistema no debe pasar desapercibida nunca ya que puede traer muy malas consecuencias por parte del cliente.
3 - Implementar Health checks en ASP.NET Core
Para implementar el código de esta aplicación, vamos a continuar con el código del curso de los sistemas distribuidos Distribt, pero el código es igual en cualquier otro sistema.
Lo primero que vamos a tener en cuenta es que vamos a añadir un health check básico a TODOS los servicios que tenemos.
Por lo tanto navegamos a donde tenemos la abstracción de nuestro setup e incluimos en los services .AddhealthChecks()
.
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddHealthChecks();
Posteriormente, cuando hemos construido nuestra WebApplication
debemos llamar a MapHealthChecks
pasando como parámetro la ruta en la que el health check va a estar ubicado.
WebApplication app = builder.Build();
app.MapHealthChecks("/health");
...
Una vez lo tenemos configurado, si ejecutamos la aplicación podemos ver que si llamamos al endpoint /health
tenemos el siguiente resultado:
Además su status code tendrá un código 200
, lo que indica que ha funcionado correctamente.
- NOTA: Si estás utilizando la librería de Distribt, esta información está ubicada dentro de
Distribt.Shared.Setup.API
en la claseDefaultDistribtWebApplication
lo que quiere decir que todas las aplicaciones van a tener el health check por defecto.
3.1 - Comprobar dependencias en un health check
Pero qué pasa si lo que queremos es comprobar más cosas, no únicamente que la app está corriendo sino que queremos comprobar las dependencias.
La gran mayoría de elementos que queramos comprobar lo podremos hacer a través de su propia librería de nuget, si buscas en nuget por AspNetCore.HealthChecks.
verás que tienes 70 resultados, esos resultados son librerías ya creadas para realizar dicho health check.
En nuestro caso particular utilizamos dos tipos de bases de datos como vimos en el post sobre CQRS, por lo tanto vamos a importar AspNetCore.HealthChecks.MongoDb
y AspNetCore.HealthChecks.MySql
.
Esta acción la podemos realizar tanto en la aplicación que va a verse afectada, o como es mi caso particular en la abstracción.
Si estamos siguiendo una aplicación que no utiliza Distribt, simplemente después de .AddHealthChecks();
incluimos el extension method para el sistema que vamos a configurar:
builder.Services.AddHealthChecks.AddMongoDb(configuration.GetSection("Database:MongoDb"));
Y tendrás que repetir esta acción con cada una de las dependencias.
Si por el contrario, utilizamos la librería de Distribt, este código va a ir abstraído en cada uno de los servicios que utilicemos, y se va a añadir automáticamente cuando configuremos dicho servicio, en el caso de MongoDb creamos un método que calculará la información:
public static IServiceCollection AddMongoHealthCheck(this IServiceCollection serviceCollection)
{
ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider();
string mongoConnectionString = serviceProvider.GetRequiredService<IMongoDbConnectionProvider>().GetMongoConnectionString();
serviceCollection.AddHealthChecks().AddMongoDb(mongoConnectionString);
return serviceCollection;
}
Y posteriormente al método que llaman nuestras aplicaciones
public static IServiceCollection AddDistribtMongoDbConnectionProvider(this IServiceCollection serviceCollection,
IConfiguration configuration)
{
return serviceCollection
.AddMongoDbConnectionProvider()
.AddMongoDbDatabaseConfiguration(configuration)
.AddMongoHealthCheck();
}
Con esta acción conseguimos que nuestras aplicaciones no necesiten configurar el health check por sí solo, con añadir la funcionalidad padre es suficiente.
Debemos repetir esta acción para cada software que estamos utilizando.
Ahora si volvemos al endpoint /health
veremos el mismo resultado “healthy” pero si paramos la base de datos en docker podemos ver que devuelve “Unhealthy
”
El cual además, tendrá un código de estado 503
, lo que significa erróneo.
3.2 - Crear un health check personalizado en .NET
Además de los propios health check que nos vienen de otras librerías podemos crear los nuestros propios.
Como he comentado antes, es común tener health checks que comprueben el tiempo de respuesta o el uso de memoria, pero esos son más comunes de crear a nivel infraestructura, ya que hay un montón de servicios que lo hacen por nosotros.
Por ejemplo, si tenemos todo el sistema en la nube, todos los proveedores nos proveen de formas de conseguir esta información, además de enviar alertas, etc.
Lo que vamos a hacer es crear nuestro health check para comprobar que otro microservicio del que dependemos está funcionando correctamente.
En el Proyecto de Distribt tenemos una relación directa (a través de RabbitMq) entre el microservicio de pedidos Orders y el microservicio Products; por lo tanto para tener la información siempre correcta en un pedido (por ejemplo el nombre) el microservicio de productos debe estar ejecutándose correctamente.
En caso de que no lo esté, el microservicio de pedidos seguirá funcionando, ya que tenemos consistencia eventual y duplicamos la información que es indispensable para nosotros.
Para crear un health check personalizado lo que debemos hacer es crear una clase que herede de IHealthCheck
y únicamente necesitamos implementar el método:
public class ProductsHealthCheck : IHealthCheck
{
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context,
CancellationToken cancellationToken = new CancellationToken())
{
throw new NotImplementedException();
}
}
En nuestro caso vamos a hacer una llamada HTTP al microservicio de productos, con lo que nos quedará el siguiente código:
public class ProductsHealthCheck : IHealthCheck
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly IServiceDiscovery _discovery;
public ProductsHealthCheck(IHttpClientFactory httpClientFactory, IServiceDiscovery discovery)
{
_httpClientFactory = httpClientFactory;
_discovery = discovery;
}
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context,
CancellationToken cancellationToken = new CancellationToken())
{
//TODO: abstract out all the HTTP calls to other distribt microservices #26
HttpClient client = _httpClientFactory.CreateClient();
string productsReadApi =
await _discovery.GetFullAddress(DiscoveryServices.Microservices.ProductsApi.ApiRead, cancellationToken);
client.BaseAddress = new Uri(productsReadApi);
HttpResponseMessage responseMessage = await client.GetAsync($"health", cancellationToken);
if (responseMessage.IsSuccessStatusCode)
{
return HealthCheckResult.Healthy("Product service is healthy");
}
return HealthCheckResult.Degraded("Product service is down");
}
}
Lo indicamos como Degraded
porque podemos seguir creando pedidos, pero los nombres de los productos no se verán actualizados si el servicio está caído.
Nota: si salta una excepción durante la ejecución del health check este sera transformado a Unhealthy
.
Ahora únicamente debemos añadir ese healthcheck a nuestra lista de healthchecks que estamos ejecutando con .AddCheck<T>
webappBuilder.Services.AddHealthChecks().AddCheck<ProductsHealthCheck>(nameof(ProductsHealthCheck));
Ahora el resultado del health check de nuestra aplicación es la combinación de todos los health checks que disponemos.
pero claro, esto nos lo agrupa en un solo resultado lo cual pues no es lo mejor, lo que tenemos que hacer es importar el paquete nuget AspNetCore.HealthChecks.UI.Client
y poner la siguiente configuración:
webApp.UseHealthChecks("/health", new HealthCheckOptions()
{
Predicate = _ => true,
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
Así podremos ver el resultado separado de forma individual con lo que está caído y con lo que no.
como podemos ver ya tenemos una mayor explicación de lo que está fallando.
3.3 - Incluir Interfaz para los health checks
Hasta ahora lo único que tenemos es un endpoint que nos indica la respuesta;
¿Qué pasa si queremos una interfaz visual?
Para ello tenemos otro paquete nuget llamado AspNetCore.HealthChecks.UI
el cual nos dará una pequeña interfaz.
El cambio es muy sencillo, en los servicios debemos indicar
services.AddHealthChecksUI();
Y cómo esta información tenemos que almacenar de alguna forma para poder mostrarla por pantalla, utilizaremos el paquete AspNetCore.HealthChecks.UI.InMemory.Storage
el cual nos permite amacenar en memoria la infomración:
builder.Services.AddHealthChecksUI().AddInMemoryStorage();
y en el apartado de la app donde vamos a configurar la url en la cual ubicamos la interfaz y que endpoint comprobar para recibir los resultados.
webApp.UseHealthChecksUI(config =>
{
config.UIPath = "/health-ui";
});
Ahora en cada uno de nuestros microservicios, debemos indicar cual es el endpoint al que vamos a llamar para recibir dicha configuración en el fichero appsettings.json
.
{
...
"HealthChecksUI": {
"HealthChecks": [
{
"Name": "Orders health check",
"Uri": "/health"
}
]
}
...
}
Y si ejecutamos la aplicación, podemos ver el resultado.
- Nota: en el proyecto de Distribt esta información está configurada por defecto para funcionar con el proyecto
Distribt.Shared.Setup
sin la necesidad de modificarappsettings
.
Esto es solo para el microservicio configurado, podríamos configurar un microservicio para añadir observabilidad sobre todos los microservicios, ya que el fichero de configuración contiene una array, pero esta visión global de todo lo que está caído/funcionando lo veremos en otro vídeo sobre observabilidad.
Conclusión
En este post hemos visto que es un health check
Cuáles son los estados de un health check
Cuándo configurar health checks
Cómo construir y configurar health checks con .net