Compartir

Sistemas reactivos

 

En Julio de 2013 un grupo de ingenieros de software liderados por Jonas Bonér publicó el Reactive Manifesto, recientemente actualizado en su versión 2.0. El diseño de sistemas reactivos es una tendencia actualmente en auge y es un tema que aparece de manera recurrente en numerosas conferencias en el ámbito de la Ingeniería del Software. Pero ¿qué es un sistema reactivo? ¿qué proclama este manifiesto? Vayamos por partes.

Sistemas cada vez más exigentes

Los requerimientos de las aplicaciones han cambiado drásticamente en los últimos años. Hace no mucho tiempo, una aplicación considerada grande podía llegar a tener decenas de servidores, gigabytes de datos y tiempos de respuesta del orden de varios segundos. Además el mantenimiento de estos sistemas requería tiempos de bajada del servicio de varias horas.

Hoy en día las aplicaciones se despliegan en una variedad ingente de dispositivos, desde móviles hasta clusters de cloud computing con miles de procesadores multi-núcleo. Los datos se miden en petabytes y los usuarios esperan tiempos de respuesta de milisegundos y un 100% de tiempo de disponibilidad del servicio.

Con esta situación, podemos decir que las demandas actuales de los sistemas no quedan cubiertas adecuadamente por las arquitecturas de software existentes hasta el momento.

Reactive Manifesto

El manifiesto proclama que es necesario un nuevo enfoque más coherente a la hora de diseñar la arquitectura de un sistema, de manera que éste sea:

  • Responsive: el sistema detecta rápidamente los problemas y los gestiona de manera efectiva. Un sistema de este tipo está focalizado en proporcionar tiempos de respuesta rápidos y calidad de servicio consistentes, siendo esto un pilar básico de la usabilidad.
  • Resilient: el sistema se mantiene responsive aunque haya fallos, mediante el uso de técnicas de replicación, contención, aislamiento y delegación. Los fallos se contienen en cada componente, aislando unos componentes de otros y asegurando por tanto que unas partes del sistema pueden fallar y recuperarse sin comprometer el sistema completo. La recuperación de cada componente se delega en otros componentes externos y la alta disponibilidad se asegura mediante la replicación allí donde sea necesaria. No se carga al cliente de un componente con la responsabilidad de gestionar los fallos que dicho componente genere.
  • Elastic: el sistema se mantiene responsive bajo diferentes condiciones de carga, incrementando o decrementando dinámicamente los recursos reservados (CPU, memoria, almacenamiento,…) para el servicio. Para ello es necesario contar con un diseño del sistema sin cuellos de botella, que permita replicar componentes y distribuir la carga entre ellos. Además, el sistema debe proporcionar medidas relevantes sobre su rendimiento en tiempo real para poder aplicar algoritmos de escalado de manera predictiva y/o reactiva.
  • Message driven: el sistema se basa en el paso asíncrono de mensajes entre componentes, asegurando un bajo acoplamiento entre los mismos y una independencia de su ubicación. Igualmente, el sistema proporciona los medios para delegar los errores en forma de mensajes asíncronos.

Los sistemas reactivos son mucho más tolerantes al fallo, y cuando un fallo ocurre lo gestionan de una manera más elegante. Además son altamente adaptables a las condiciones que les rodean, siendo capaces de proporcionar un feedback efectivo a los usuarios.

Un ejemplo: la máquina de café (o cómo gestionar errores de manera elegante)

La gestión de errores suele ser un aspecto secundario y que se deja para el final en la mayoría de las aplicaciones. Dos problemas que suele generar este enfoque son: un pobre aislamiento y contención de los errores y que éstos son enviados directamente al cliente de manera síncrona para que éste los gestione.

Supongamos que una persona quiere comprar un café en una máquina de vending. El café cuesta 40 céntimos. Si la persona echa una sola moneda de 20 céntimos, no pasará nada, porque no ha cumplido su parte del contrato del servicio. Por tanto la máquina, en lugar de devolver un café, mostrará un mensaje de error: “por favor, introduce otra moneda de 20”. Esto es lo que cabría esperar. El usuario de la máquina de café es responsable de cumplir su parte del contrato del servicio. La mayoría de las aplicaciones hacen un buen trabajo presentando mensajes de error y gestionando fallos a este nivel (errores de validación).

Pero ¿qué pasa si la persona echa otra moneda de 20 pero la máquina no funciona porque se ha atascado el café? No sería esperable que la máquina devolviera un mensaje diciéndole al usuario que abra y desmonte la máquina para solucionar el problema. Esta no es la responsabilidad del usuario (es un error de aplicación). En su lugar, idealmente la máquina enviaría una notificación al servicio de mantenimiento de la máquina para que viniera a arreglarla.

El enfoque reactivo: Let it crash

En lugar de simplemente enviar mensajes de error al usuario (que no podrá hacer nada para resolver el problema), con un enfoque reactivo el sistema será capaz de aislar y contener el error (evitando un fallo completo de la aplicación) e intentar resolverlo de manera automática, enviando un mensaje al receptor más adecuado (componente supervisor del componente que ha fallado) para gestionar el error.

En este modelo de diseño para fallos - denominado a veces como Embrace failure o Let it crash- los errores no se consideran ya como algo excepcional, si no como parte del flujo normal de mensajes que intercambian entre sí los distintos componentes de una aplicación.

 Conclusión

La separación entre errores de validación y errores de aplicación, siendo muy importante, es algo que aún en la actualidad la mayor parte de las aplicaciones a menudo no considera o lo hace de una manera confusa.

Lo cierto es que los errores de aplicación no deberían enviarse al usuario, pero la mayoría de los lenguajes (p.e. Java) es lo que esperan que hagas con su mecanismo síncrono de lanzamiento de excepciones, donde los bloques try-catch son la única herramienta para tratar los errores. Esto fuerza a programar muy defensivamente y estar preparado para que cualquier llamada a un método o servicio pueda fallar en cualquier momento, retornando un error de aplicación.

Como resultado de este enfoque a menudo vemos aplicaciones donde el código de  gestión de errores está desperdigado por toda la aplicación y entremezclado con el código de la lógica de negocio en un revoltijo incomprensible.