Liberándonos del monolito

Y cómo evolucionamos con los microservicios

(19 de julio de 2018)

Un código gobernante Monolito monos

En Conio todo comenzó con lo que comúnmente se llama un «Monolito». Es decir, una única base de código que contiene toda la aplicación completa: sus componentes frontend, componentes backend, servicios API, tareas en segundo plano; demonios, incluso los scripts devops. Y funcionó muy bien al principio. Solo un par de ingenieros de software, trabajando en áreas separadas del código base (por lo que hay muy pocas posibilidades de conflictos de cambio de código), fáciles de implementar. Enfoque total en escribir funcionalidades de aplicaciones, sin preocuparse por mucho más. ¿Cómo abordamos el despliegue? Con solo unos pocos clientes beta al tanto de los avances continuos, no fue un problema real cerrar los servicios por un tiempo, implementar la base de código completa (sin importar cuán pequeños o grandes fueran los cambios generales, y si incluían migraciones de bases de datos), y luego traer los servicios nuevamente.

Fue definitivamente satisfactorio ver un producto tomando forma desde cero y recibir el reconocimiento de los clientes finales. Sin embargo, sabíamos muy bien que este enfoque no se ajusta a una empresa Fintech moderna.

¿Y luego qué?

Con la mayoría de las aplicaciones de software, los clientes son bastante tolerantes. Sí, Whatsapp podría dejar de funcionar y tener una interrupción durante algunas horas: definitivamente una molestia, pero no un problema percibido. Lo mismo ocurre con Pokemon Go o tu aplicación de juego favorita. Sin embargo, ese no es el caso cuando se trata de dinero : cambia el estado de ánimo si no puede iniciar sesión en su cuenta bancaria o no puede Realizar operaciones comerciales. Esto es aún peor en el caso de las aplicaciones de criptomonedas: la mayoría de las personas recuerdan los infames errores del pasado, y siempre que no pueden acceder a sus fondos de criptomonedas, incluso por un corto período de tiempo, surgen especulaciones. Eso es justo. Es su dinero, y debería tener poco o ningún problema cuando quiera usarlo.

El Monolito anterior no es adecuado para tal escenario: cualquier cambio en la base de código en producción requeriría una implementación completa, con el tiempo de inactividad asociado. Cada día, trabajamos para mejorar nuestros servicios corrigiendo errores, haciendo nuestra interfaz aún más amigable, eliminando funcionalidades antiguas y agregando otras nuevas que tienen un mejor uso. A menudo publicamos estas actualizaciones a diario para que nuestros clientes puedan beneficiarse de inmediato y nos esforzamos por no tener ningún impacto en la experiencia del cliente. Es decir, cualquier fórmula que inventemos detrás de escena, debe ser invisible para el mundo exterior (al menos, la mayoría de las veces). Entonces, nos alejamos del Monolito y elegimos lo que comúnmente se llama “arquitectura de microservicios”.

Evolución a través de microservicios

La enorme base de código única fuertemente pegada ahora se descompone en partes más pequeñas, cada una de las cuales representa un servicio particular. Siempre que se ejecutan, los servicios se comunican entre sí de forma sincrónica a través del protocolo HTTP estándar y de forma asincrónica a través de colas (gestionadas, por ejemplo, por RabbitMQ y Apache Kafka).

Interacciones en una arquitectura de microservicios

Es bastante difícil comenzar a dividir el monolito en componentes más pequeños, pero vale la pena el esfuerzo. En términos militares, es muy similar a lo que hizo Julio César para gobernar de manera constante el gran territorio de Galia: « divide y vencerás «.

1) El producto puede desplegarse continuamente. Una actualización de código ahora se aplica solo a un microservicio: en la mayoría de los casos, se puede implementar inmediatamente en producción y lanzarse sin impacto para el cliente

2) El código es más fácil de administrar. Desde la perspectiva de la organización de una empresa, las cosas cambian cuando un equipo de 2 ingenieros de software se convierte en un equipo de 10 ingenieros de software. Es más efectivo y con menos conflictos de código cuando cada miembro del equipo es responsable de su propio microservicio.

3) El código es más fácil de mantener. Una arquitectura de microservicios requiere, por naturaleza, la definición de una interfaz para comunicarse con el mundo externo (ya sea la aplicación frontend u otro servicio backend) y está completamente aislada de cualquier otro punto de vista. Esto permite revisar, rediseñar o incluso reescribir completamente desde cero (incluso en diferentes idiomas si es conveniente) componentes individuales de la aplicación sin afectar al resto.

4) Se puede mejorar el rendimiento. Cada microservicio ahora puede usar su lenguaje más apropiado. Los componentes de computación criptográfica pesados ​​pueden, por ejemplo, optimizarse en C, mientras que los servicios de API en Python y las tareas de larga ejecución en Go.

5) Seguridad y aislamiento de código mejorados. Cada microservicio se puede ejecutar en su propio contenedor Docker, lo que proporciona aislamiento de privilegios, segregación de datos y redes y, de suma importancia para una fase de crecimiento, un enorme potencial de escalabilidad.

¿Son los microservicios la respuesta entonces?

Por supuesto, no existe tal cosa como un almuerzo gratis. Una arquitectura de microservicios también viene con su propio conjunto de desafíos difíciles:

1) Complejidad operativa. Los ingenieros de DevOps definitivamente son necesarios para suavizar las complejidades del nuevo proceso de implementación.

2) Sobrecarga de hardware. Los microservicios a menudo se ejecutan en contenedores Docker; tan pronto como prolifera la cantidad de microservicios, se vuelve cada vez más difícil ejecutar la aplicación completa en el mismo hardware que antes.

3) Intercomunicación: cada solicitud puede necesitar interactuar con uno o más microservicios a través de la red. Esto puede aumentar la latencia y puede estar sujeto a fallas temporales. Para implementar servicios resilientes y mejorar la escalabilidad de todo el sistema, es necesario mover las interacciones a la mensajería asincrónica (por ejemplo, usando Apache Kafka y / o RabbitMQ)

4) Consistencia eventual. Este es probablemente el desafío más difícil de una arquitectura de microservicios. Dado un solo microservicio, es posible crear transacciones RDBMS dentro de sus límites . Sin embargo, desafortunadamente, un problema común en las arquitecturas distribuidas es lidiar con múltiples transacciones que no están dentro de los mismos límites. Como resultado, el sistema puede terminar en un estado ilegal e irrecuperable. Con el fin de mitigar estos problemas, Conio adopta diferentes estrategias:

  1. Siguiendo las prácticas del diseño basado en dominios, descomponga los dominios de nivel superior en subdominios y confínelos en contextos limitados ; cada contexto limitado se implementa como un microservicio, donde se aplican los límites de las transacciones. Esto resuelve la posibilidad de tener inconsistencias para subdominios específicos.
  2. Implemente interacciones asincrónicas idempotentes, que tarde o temprano resuelven inconsistencias.
  3. Siempre que sea posible, evite cualquier acción que pueda involucrar múltiples subdominios.

5) Informes complejos. Dado que cada subdominio vive dentro de un contexto limitado específico, los informes complejos que involucran múltiples subdominios pueden requerir consultar datos de múltiples fuentes de datos: esto puede tener un impacto negativo tanto en la expresividad de los dominios como en la escalabilidad del sistema. Aquí en Conio hemos adoptado una arquitectura CQRS para respaldar la actividad de backoffice y los informes de análisis de negocios.

6 ) Sistema de registro. Cada elemento de un sistema distribuido contribuye a la creación del registro de todo el sistema. Sin embargo, es necesario implementar herramientas que puedan crear las conexiones necesarias entre todos esos registros separados para tener un registro unificado para cada interacción. Aquí en Conio usamos la pila ELK (ElasticSearch, Logstash, Kibana) para almacenar y consultar datos de registro: cada registro está enriquecido con los identificadores de correlación necesarios que permiten el registro unificado mencionado anteriormente.

Nunca detengas la evolución

¿Nuestra opinión? La descomposición del código base único inicial debe verse como una tarea a largo plazo , con mejoras continuas. En Conio, nos tomó un par de años y, paso a paso, pasamos de una base de código masiva a más de 100 microservicios . Hemos llegado a un punto en el que nos sentimos orgullosos de los resultados, pero al mismo tiempo seguimos explorando. Hay múltiples posibles nuevas optimizaciones: ¿pasar de Docker Swarm a Kubernetes? ¿Migrar servicios backend-for-frontend a funciones lambda sin servidor? ¿Cambiar a un flujo de operación de implementación continua completa? Las posibilidades son infinitas.

Aquí hemos tocado varios temas y tecnologías. En los próximos artículos, compartiremos más detalles sobre nuestros hallazgos y avances. Si lo desea, no dude en comentarnos y contarnos su experiencia.

Deja una respuesta

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