Monitorizando nuestros servicios en Kubernetes con Beatpulse

Disclaimer: En ese post hablo de una librería (Beatpulse) de la que soy contribuidor (lo aclaro, para que no haya ningún malentendido).

En todo sistema distribuído es importante disponer de un mecanismo que permita saber en todo momento si un servicio está funcionando o no. Es cierto que el concepto de «funcionando» es algo difuso de definir, pero yendo a mínimos deberíamos saber si un sistema se ha caído o no.

Toda imagen de Docker puede definir un healthcheck, que no es nada más que un comando, que ejecutado periódicamente, indica si el servicio está funcionando correctamente o no. Docker ejecuta este comando como un proceso separado dentro del mismo contenedor. Si el comando devuelve un error, Docker considera que el contenedor «no está funcionando».

Docker en sí mismo no hace nada más que marcar el estado del contenedor, pero otros sistemas más avanzados como Swarm pueden usar esa información para reiniciar aquellos contenedores una vez fallan.

En el Dockerfile el healthcheck como un comando y, lo típico (aunque no tiene porque ser así) es llamar a un endpoint provisto por el propio contenedor: si ese responde correctamente se asume que el contenedor está funcionando:

HEALTHCHECK CMD curl --fail http://localhost/hc || exit 1

Ese es el ejemplo canónico de healthcheck en Docker: se usa curl para navegar al endpoint (/hc) provisto por le contenedor. Usar curl es un mecanismo sencillo aunque viene con el precio de qué curl debe existir en la imagen (las imágenes base del runtime de asp.net core vienen con curl instalado) lo que en según qué escenarios no te puedes permitir. Aconsejo leer este post de Elton Stoneman para más información al respecto.

Por supuesto si tu contenedor expone un endpoint (/hc) para poder ser llamado por el healthcheck ese endpoint debe estar levantado por tu API. En asp.net core eso es muy sencillo. Basta con agregar lo siguiente en el método Configure de la clase Startup para que las peticiones a /hc sean gestionadas por este pipeline (que se limita a devolver un 200):

app.Map("/hc", hcapp =>
{
    hcapp.Run(async ctx => ctx.Response.StatusCode = 200);
});

La principal limitación de los healthchecks de Docker es que solo hay uno y el resultado es binario: o el «contenedor funciona» o no. Pero la vida real no es tan simple: imagina que tienes una API que depende de una base de datos. Si la base de datos está caída, es obvio que tu API no va a funcionar… Pero, ¿eso significa que el contenedor de tu API no funciona? Recuerda que antes he comentado que orquestradores como Swarm usan esa información para reiniciar tu contenedor. Y, si el problema está en la base de datos… ¿de qué sirve reiniciar el contenedor de la API?

Por otro lado tener un healthcheck en la API que verifique que ésta pueda conectarse con la base de datos es útil: nos permite saber qué la API está en un estado, pongamos «inusable», por culpa de alguna de sus dependencias.

Probes de Kubernetes

Kubernetes, a diferencia de Swarm, no usa los healthchecks de Docker. A priori puede parecer un paso atrás: ¿quién mejor que la propia imagen (en el Dockerfile) puede saber lo qué singifica qué un contenedor «no funciona»? La razón de que Kubernetes no use los healthchecks de Docker (usa el modificador –no-healthcheck de «docker run» al lanzar los contenedores) es debido a qué Kubernetes ofrece su propio mecanismo, llamado probes. En esencia un probe es lo mismo que un healthcheck: un comando que ejecuta sobre un contenedor y que devuelvo un estado (ok o error). Pero Kubernetes soporta dos tipos de probes:

  1. Liveness probes: Son aquellos que cuando fallan la solución pasa por reiniciar el contenedor. En el ejemplo anterior, sería el healthcheck que hemos implementado. Si llamar a un endpoint que devuelve un 200 siempre, te da un error, és que hay algo mal en el propio contenedor (quizá la API no se ha levantado). En estos casos, reiniciar es la solución.
  2. Readiness probes: Son aquellos que cuando fallan la solución NO pasa por reiniciar, si no por «esperar» a que se solucione el problema. Durante esta espera Kubernetes se asegura de que el contenedor (realmente el pod) no reciba tráfico. En el ejemplo anterior es lo que ocurre si lo que falla es la conexión a la BBDD: en este caso reiniciar la API sirve de poco. La solución es reiniciar la BBDD (si está caída) y esperar a qué la conexión se restablezca (puede ocurrir sin reiniciar la BBDD por supuesto, si era p. ej. un error de red).

Por lo tanto en Kubernetes, en un sistema con dos pods uno de los cuales ejecuta una API conectada a una BBDD y el segundo ejecuta la BBDD, sería interesante definir tres probes:

  • Un liveness probe en la API (endpoint que devuelve siempre 200)
  • Un readiness probe en la API (endpoint que intenta conectarse al SQL Server y si puede devuelve un 200)
  • Un liveness probe en el pod de la BBDD (que lance un comando para intentar conectarse a la BBDD).

De este modo el primer probe garantiza que la API se reiniciará si, por cualquier razón, no se levanta o se queda «bloqueada». El segundo se asegura de que no se enrute tráfico a la API mientras la BBDD no está accesible y el tercero se encarga de reiniciar la BBDD en caso de que esta se cuelgue.

En Docker/Swarm los healthchecks los ejecutaba el propio contenedor, mientras que en Kubernetes los ejecuta el pod (realmente kubelet) que ejecuta el contenedor. Eso tiene la ventaja de que el kubelet ya sabe hacer determinadas tareas sin necesidad de que la imagen tenga nada especial. P. ej. no necesitas tener curl en la imagen del contenedor para crear un probe que sea una llamada HTTP GET: kubelet ya sabe hacer llamadas HTTP GET. Eso permite que las imágenes puedan ser más reducidas.

De los tres probes mencionados antes me interesa el segundo, el readiness: un readiness probe generalmente debe validar todas las dependencias. Vale, quizá solo sea una BBDD, pero pueden ser más sistemas. Puedes depender también de un Redis, o de un storage de Azure… todas aquellas dependencias que impacten en el correcto funcionamiento de tu API deberían ser validadas en el readiness probe. Por supuesto que puedes hacerlo «a mano» (de echo estás usando esas dependencias en tu API, así que los SDKs para acceder a ellos ya los estás usando), pero es código bastante repetitivo. Por eso existen librerías para automatizarlo, y una de ellas, que quiero presentarte es Beatpulse.

Hace tiempo escribí sobre Healthchecks, una librería diseñada por (parte de) el equipo de ASP.NET para eso mismo, pero la realidad es que esta librería ni ha salido y sus características distan mucho de ser las necesarias. El principal problema de esa librería es que solo soporta un healthcheck para tu API: suficiente para healthchecks de Docker/Swarm, pero no para probes de Kubernetes.

Beatpulse, por otro lado se alinea con la mentalidad de Kubernetes: se expone un endpoint en tu API que, básicamente, devuelve siempre un 200 (para ser usado como liveness probe) y luego otro endpoint donde puedes comprobar las dependencias. Para que no tengas que teclear mucho códgo, se incluyen muchos «verificadores» de dependencias.

Para empezar a usar BeatPulse debes instalar el paquete de NuGet (Install-Package Beatpulse) y usar el método UseBeatPulse de IWebHostBuilder:

public static IWebHost BuildWebHost(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .UseBeatPulse()
        .UseStartup<Startup>()
        .Build();

Para que te funcione debes también llamar al método AddBeatPulse() en ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services.AddBeatPulse();
    services.AddMvc();
}

¡Listos! Con esto ya tienes tu endpoint (/hc/self) que devuelve un 200, pensado para que lo uses como liveness probe en Kubernetes.

El siguiente paso es crear el endpoint de readiness probe: imagina que dependes de un Sql Server. El primer paso es instalar el paquete Beatpulse.SqlServer que contiene el «verificador» de Sql Server. Y agregarlo a la llamada a AddBeatPulse:

services.AddBeatPulse(opt =>
{
    opt.AddSqlServer(Configuration["connectionstring"]);
});

Con esto ahora tienes lo siguiente:

  1. El endpoint /hc/self devuelve siempre 200 y es para ser usado como liveness probe
  2. El endpoint /hc/sqlserver verificará solamente el Sql Server
  3. El endpoint /hc verificará todas tus dependencias (en este caso solo tenemos el Sql Serve) y es el que debes usar como readiness probe

A partir de ahí si tenemos más dependencias, agregaríamos su «verificador» correspondiente. Hay «verificadores» para varias de las dependencias más clásicas (y si no, el modelo es extensible para que te puedas crear las tuyas propias).

La filosofía de Beatpulse es muy simple: cada «verificador» que añadas, te añade un endpoint (por debajo del endpoint raíz que por defecto es /hc) que te indica si esa dependencia (solo esa) está funcionando. Luego el endpoint raíz (/hc) te verifica todas las dependencias (es el que debes usar para readiness) y /hc/self es un endpoint «especial» que devuelve siempre 200 pensado para liveness.

En este caso, los probes que definiríamos en la spec del pod de Kubernetes serian (en este ejemplo uso el probe httpGet que es el más habitual y que significa, como debes imaginar, realizar una llamada HTTP GET):

readinessProbe:
  httpGet:
    path: /hc
    port: 80
    scheme: HTTP 
  periodSeconds: 60 
livenessProbe:
  httpGet:
    path: /hc/self
    port: 80
    scheme: HTTP 
  periodSeconds: 60

De este modo, si se cae la conexión entre el Sql Server y nuestra API, fallará el readiness probe (ya que /hc ejecuta todos los «verificadores») y Kubernetes no nos mandará tráfico al pod. Pero el pod no se reiniciará (a no ser que también falle el /hc/self que es un endpoint que, recuerda, devuelve siempre 200).

Luego, otra de las ventajas de usar una librería como Beatpulse para eso, son los «servicios añadidos» que esta te ofrece respecto a «hacértelo tu». En concreto Beatpulse ofrece integraciones a través de Webhooks (para usar con Microsoft Teams o Slack), con Application Insights y tiene también servicios de valor añadido como cache, información para saber cual de los «verificadores» ha fallado e incluso… ¡una web para mostrar de forma gráfica el estado de tus verificaciones!

No sé, si usas ASP.NET Core en escenarios distribuídos échale un vistazo a Beatpulse y por supuesto: ¡usa las issues de Github para cualquier duda o sugerencia!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *