Protegiendo tu aplicación de fallos externos con el patrón Circuit Breaker (Parte 1)

Recientemente he releído Release It!: Design and Deploy Production-Ready Software de Michael T. Nygard, un clásico de la informática a la altura de Clean Code (Robert C. Martin) y que todo profesional de la informática que se tome en serio este arte, debe leer.

Este libro está plagado de sabios consejos, anécdotas y frases divertidas que te equipan con herramientas para construir aplicaciones resilentes, administrables, aptas para ser usadas en producción. De estas herramientas he querido tratar en este post el patrón de estabilidad Circuit Breaker, ya muy conocido en el mundo de los microservicios pero útil en cualquier tipo de aplicación que tenga dependencias con sistemas externos (y cual no?). Veremos también su aplicación práctica con Java, utilizando Netflix Hystrix  con Spring Boot.

El problema

Con muy pocas excepciones, nuestras aplicaciones siempre van a depender de un sistema externo, generalmente una base de datos u otros servicios y esto es especialmente cierto en una arquitectura SOA o una de microservicios en donde ensamblamos nuevos productos y servicios partiendo de la composición de otros. Estos sistemas externos pueden ser hechos por nuestra empresa o simplemente servicios web de otras compañías.

 

Cuando uno de estos servicios externos se pone lento o falla completamente, nuestra aplicación puede pasar muchos segundos esperando su respuesta hasta que un timeout en la conexión nos lleva a un estado de fallo (ojalá controlado, por lo menos con un try/catch). Esto es una característica del protocolo TCP.

El problema radica en que cada vez que esto sucede, un hilo de nuestra aplicación estará bloqueado esperando la respuesta, ocupando el CPU esperando por nada, sólo para fallar al final de la espera. Durante este tiempo que nuestro hilo está bloqueado, el CPU se desperdicia pues no puede atender otras peticiones y si nuestro sistema está bajo una carga alta el problema se maximiza: cada petición nueva que consulta al sistema externo debe esperar a que las peticiones que llegaron primero se rompan por un timeout. La configuración por defecto usual de los timeout TCP es de 30 segundos.

Imagina que llega una petición por la que debes consultar al sistema externo. Esta petición queda esperando la respuesta del sistema externo por 30 segundos durante los cuales llegan nuevas peticiones a una tasa, por ejemplo, de 5 peticiones por segundo. Para cuando la primera petición se rompa por timeout, habrá 150 peticiones en cola esperando también a romper por timeout!. Este efecto es exponencial y terminará agotando los hilos de tu aplicación hasta que ésta termine fallando también y causando una caída en cascada hacia tus clientes, bien sea en forma de respuesta muy lenta, un error 500 o ninguna respuesta.

La solución

En estos casos lo mejor que puedes hacer es hacer que tu servicio se «degrade elegantemente» o gracefully degradation, como se conoce en inglés a la forma en que tu aplicación sigue respondiendo ante un fallo, quizá con menos prestaciones o sin ninguna prestación, pero aún respondiendo y no con incomprensibles trazas de error sino con mensajes significativos para el usuario. En otros casos puede que sea posible atender la petición con información guardada localmente en lugar de consultar al servicio externo o simplemente enviar la información de la petición a una cola para su procesamiento posterior. Lo importante es no volcar una traza de error (o una pantalla azul de la muerte…) frente al usuario.

Uno de los patrones de estabilidad bien conocidos y utilizados en estos casos es Circuit Breaker, que ha revitalizado su popularidad gracias al auge de los microservicios.

Un poco de historia…

Citando el ejemplo ilustrado en Release it!, en las primeras instalaciones eléctricas en los hogares, la gente acostumbraba a conectar muchos aparatos eléctricos al circuito de su casa. Cada aparato extraía cierta cantidad de corriente. La corriente al presentar resistencia (la del aparato) produce calor. Algunas veces el calor era suficiente para producir fuego entre las paredes e incendiar las casas.

Para prevenirlo, la gente empezó a utilizar fusibles: dispositivos similares a bombillas, conectados al circuito que cuando experimentaban corrientes altas, se quemaban dejando abierto el circuito e interrumpiendo la corriente. Su problema es que eran costosos y desechables por lo que muchas personas empezaron a crear sus propias versiones de fusibles y… los incendios regresaron.

Finalmente y desde entonces utilizamos los circuit breakers que detectan una corriente alta y cambian automáticamente a una posición en la que abren el circuito, interrumpiendo el paso de la corriente (open). Luego cuando la situación vuelve a la normalidad, manualmente los cambiamos de posición para volver a cerrar el circuito y permitir nuevamente el paso de la corriente (close).

Eaton circuit breaker panel open
Panel de Circuit Breakers

Y como funciona un Circuit Breaker en software?

La idea es la misma: aislar nuestro dispositivo (aplicación) cuando se detecte un fallo en el circuito (conexión con una dependencia) y restablecer la conexión cuando el fallo sea solucionado.

La implementación del patrón es así:

  1. En el estado normal o cerrado (close), el circuit breaker ejecuta operaciones como de costumbre. Estas operaciones pueden ser llamados a sistemas externos o pueden ser operaciones internas sujetas a un timeout u otra condición de error. Si la ejecución es exitosa, nada ocurre.
  2. Si el llamado al sistema externo falla, el Circuit Breaker incrementa su conteo interno de fallos
  3. Cuando el número de fallos supera un límite, el circuit breaker pasa al estado open y abre el circuito, interrumpiendo el flujo de la aplicación. esto significa que cualquier invocación al sistema externo fallara inmediatamente sin siquiera intentar el llamado. Aquí es recomendable fallar con una excepción diferente que indique que el breaker está abierto y así mismo reaccionar de otro modo indicando al usuario y operador lo que sucede para que puedan tomar las acciones adecuadas.
  4. Después de cierto periodo de tiempo, el breaker decide que la operación podría tener éxito y pasa al estado semi-abierto o half-open. En este estado el breaker no fallará inmediatamente sino que intentará el llamado al sistema externo la próxima vez que lo invoquen. Si el llamado es exitoso, pasará al estado close permitiendo los llamados posteriores. Si el llamado falla nuevamente, se quedará en el estado open hasta que vuelva a transcurrir otra vez el periodo de tiempo y pase a half-open nuevamente.

La características principal de un Circuit Breaker es que sirve para impedir la operación externa en lugar de reintentarla. Esto puede traer un impacto sobre la operación de negocio que se está protegiendo, por lo cual es muy importante involucrar a los responsables de negocio para tomar las decisiones sobre como actuará nuestra aplicación en estos casos.

En la segunda parte de este post veremos como implementar un Circuit Breaker en un microservicio con Netflix Hystrix y Spring Boot.

« »