Claves para implementar con éxito la Arquitectura de microservicios
Por: Norman Riquelme, Gerente de Tecnología en Apiux.
Es Ingeniero Civil Electrónico y tiene más de 20 años de experiencia en el desarrollo de sistemas.
Hace más de una década ha liderado las áreas de arquitectura de diversas compañías de software.
¡Bienvenido, futuro yellower! Esta es una nueva entrega de Apiux Devs. En el blog anterior repasamos las bases para entender la arquitectura de aplicaciones y las diferentes capas de la arquitectura hexagonal.
Ahora veremos la arquitectura orientada a microservicios. Antes de profundizar en este tema, repasemos los tipos de arquitectura.
En la imagen vemos dos tipos de arquitectura para una misma aplicación, una monolítica y una orientada a microservicios, observa que ambas utilizan un patrón de arquitectura hexagonal.
Fuente: Microservices From Design to Deployment, Chris Richardson & Floyd Smith, 2016
Una arquitectura monolítica corresponde a una gran aplicación que hace todo, funcionalidades, lógica de negocio, conexión a base de datos, etc.
Los microservicios, por otro lado, son un conjunto de componentes autónomos que colaboran entre sí para generar una solución. Cada uno ejecuta su propia lógica de negocio, son en esencia mini aplicaciones que cumplen una función específica.
Analicemos el ejemplo que vemos en la imagen anterior:
Este es un diseño de una arquitectura hexagonal para una aplicación monolítica de un sistema de transporte similar a Uber o Didi.
Como lo aprendimos aquí, la arquitectura hexagonal aísla el core lógico de la aplicación del exterior, tanto de los clientes como de servicios externos que pueda utilizar, y tiene absolutamente toda la lógica core en un solo componente, comunicándose mediante adaptadores por diferentes puertos de entrada o salida.
A pesar de tener una arquitectura modular a nivel lógico, la aplicación está empaquetada y desplegada como un monolito, por ejemplo un war, que puede desplegarse en un servidor de aplicaciones Tomcat, Jetty o Jboss, etc.
También podría ser una gran aplicación Rails o Node.js empaquetada como una jerarquía de directorios.
Las aplicaciones construidas de esta forma son más comunes de lo puedas imaginar porque son muy simples de desarrollar debido a que los IDE y otras herramientas están enfocadas en construir aplicaciones de este tipo.
Son fáciles de probar, se puede implementar pruebas end-to-end simplemente con iniciar la aplicación y probar la interfaz de usuario con una herramienta de pruebas como Selenium.
También son fáciles de implementar, solamente se debe copiar la aplicación empaquetada a un servidor.
Así mismo se puede escalar la aplicación ejecutando varias copias, en varias máquinas detrás de un balanceador de carga. En general, en las primeras etapas de un proyecto funciona bien.
Aplicaciones monolíticas = we’re in trouble
El problema es que este enfoque monolítico tiene una gran limitación. Como bien sabes, o pronto lo sabrás, las aplicaciones construidas de este modo tienen el mal hábito de crecer con el tiempo y transformarse en enormes engendros.
Durante cada sprint, el equipo de desarrollo implementa algunas historias de usuario más, lo que significa agregar muchas líneas de código. Después de unos años, la pequeña y sencilla aplicación se habrá convertido en un monstruoso monolito.
En sentido estricto, un monolito es un bloque de piedra de gran tamaño. Uno de los más famosos aparece en la película Odisea en el Espacio (2001), de Stanley Kubrick.
Una vez que la aplicación se ha convertido en un monolito grande y complejo, es realmente un dolor de cabeza realizar mantenciones sobre ella.
Cualquier intento de desarrollo ágil será cada vez más complejo, simplemente porque es demasiado grande para que un solo desarrollador la entienda por completo.
Como resultado, corregir errores e implementar nuevas funciones se vuelve difícil y lento. Esto produce un círculo vicioso. Si el código base es difícil de entender, los cambios no se realizarán correctamente y la aplicación se convertirá en un monstruo más grande y más difícil de entender.
Las aplicaciones monolíticas también son difíciles de escalar debido a que en ciertos momentos diferentes módulos están luchando por los recursos del servidor.
Por ejemplo, un módulo podría estar ejecutando algún procesamiento de imágenes con un uso intensivo de la CPU, mientras otro está intentando ejecutar transacciones en una base de datos. Como ambos comparten los recursos, puede producirse un bloqueo, colgando la aplicación.
Otro problema es la confiabilidad. Debido a que todos los módulos se están ejecutando dentro de un mismo proceso un error en cualquier módulo, como una overflow de memoria, podría botar la aplicación completa.
Además, dado que todas las instancias de la aplicación son idénticas, el error afectará la continuidad operativa del sistema completo, ya que todas fallarán del mismo modo en algún momento.
¿Solución? Arquitectura de microservicios
Muchas compañías, como Amazon, eBay y Netflix, han resuelto el problema de las aplicaciones monolíticas adoptando una Arquitectura de Microservicios.
De hecho, Netflix desarrolló la tecnología Spring Cloud sobre Spring Boot, justamente para eso.
En el último tiempo Redhat ha hecho lo propio creando Quarkus, que permite crear microservicios optimizados para ser desplegados en Kubernetes.
Como se observa en la imagen del lado izquierdo, en vez de construir una sola aplicación monstruosa y monolítica, construimos una aplicación que consiste en un conjunto de servicios más pequeños e interconectados.
Fuente: Microservices From Design to Deployment, Chris Richardson & Floyd Smith, 2016
Cada servicio implementa un conjunto de características o funcionalidades distintas, como gestión de órdenes de compra, gestión de clientes, gestión de productos, etc.
Cada microservicio es, en esencia, una miniaplicación que tiene su propia arquitectura hexagonal que contiene la lógica de negocio, pero mucho más pequeña y manejable.
Los microservicios exponen una API que será consumida por otros microservicios o por los clientes de la aplicación, ya sean aplicaciones web, aplicaciones móviles o sistemas externos.
En tiempo de ejecución, cada instancia puede ser una máquina virtual (VM) en la nube o un contenedor corriendo sobre Docker.
Estos microservicios pueden conectarse a diferentes bases de datos, estar construidos en diferentes lenguajes (Springboot, Node.js, Python, etc), de acuerdo a los requerimientos específicos del negocio.
La migración a nuevas versiones es fluida, ya que cada microservicio evoluciona por su cuenta. Los tiempos de liberación son mucho más rápidos, con equipos de desarrollo más pequeños.
Cada microservicio puede ser escalado de forma independiente. Aumentando los recursos en aquellos en que se necesite debido a mayor carga, mayor tiempo de ejecución, etc.
Los microservicios con menor uso requieren menos recursos y quizá no necesiten escalamiento. En una aplicación monolítica esto no es posible, necesariamente se debe escalar la aplicación completa, duplicando, triplicando o más la cantidad de servidores para lograr responder a un aumento en la carga.
Te puede interesar: ¿En busca de práctica profesional? Conoce la historia de crecimiento de Bryan, nuestro primer practicante
En resumen: ventajas de la arquitectura de microservicios
- Complejidad reducida
Resuelve el problema de la alta complejidad de las aplicaciones, ya que se promueve la descomposición de una gran aplicación monolítica en un grupo de servicios mucho más manejables.
- Evolución independiente
Cada microservicio puede ser desarrollado de forma independiente, y el equipo puede usar las tecnologías que considere más adecuadas para la implementación, siempre que cumpla con el contrato definido a través de su API.
- Despliegue independiente
Cada microservicio puede ser desplegado de forma independiente sin afectar otros servicios, eliminando la necesidad de que el equipo coordine la implementación de cambios que solo afectan a ese servicio específico, y permitiendo que estos cambios se introduzcan tan pronto como estén listos y debidamente probados, promoviendo la implementación continua.
- Escalamiento
Cada servicio puede ser escalado de forma independiente, permitiendo ajustar el número de instancias del servicio en cualquier momento para garantizar los SLA establecidos y responder a la demanda de forma adecuada.
Elementos claves para una estrategia exitosa
Para llevar a cabo con éxito una estrategia de microservicios es fundamental identificar el impacto de cada microservicio para diseñarlo adecuadamente.
Las soluciones deben ser soportadas por procesos y herramientas adecuadas, utilizando integración continua, por la sobrecarga administrativa que conlleva utilizar microservicios.
Debes pensar que en vez de administrar el despliegue de un solo gran war debes administrar el despliegue de decenas de microservicios.
Es importante que los equipos de arquitectura estén pensando en microservicios desde el principio, cuando están recién ideando la solución.
Debe existir un gobierno adecuado de los procesos de desarrollo para definir de forma granular los equipos, que deben trabajar coordinadamente.
Desventajas de los microservicios
No todo es tan bonito. Los microservicios resuelven muchos problemas, pero también generan algunos. Esto constituye un desafío que hay que resolver de forma adecuada para tener una solución robusta.
- Diseño: Debes invertir tiempo en identificar las dependencias entre los servicios. Y estar atento, porque cuando se termina un diseño, puede surgir la necesidad de incorporar nuevos microservicios para resolver los problemas de dependencia circular.
- Pruebas: Las pruebas de integración, así como las pruebas finales, pueden tornarse más complejas e importantes que nunca. Ten en cuenta que una falla en una parte de la arquitectura puede producir un error grave, y esto depende de la manera en que hayas diseñado la arquitectura de tus servicios para que sean compatibles entre sí.
- Despliegue: Cuando se actualizan las nuevas versiones se corre el riesgo de anular la compatibilidad con las versiones anteriores.
- Implementación: Efectivamente también es un desafío, al menos durante la configuración inicial. Para simplificarla, primero debes invertir mucho en la automatización, ya que la complejidad de los microservicios resulta agobiante para la implementación humana. Hay que tener en cuenta la manera y el orden en que se implementarán los servicios.
- Registro: Con los sistemas distribuidos, se necesitan registros centralizados para integrar todos los elementos. De lo contrario, es imposible controlar la expansión.
- Monitorización: Es indispensable tener una vista centralizada del sistema para identificar las causas de los problemas.
¿Y dónde ejecutarlos? Contenedores
Bueno, una vez que hemos diseñado nuestros microservicios, nos planteamos la pregunta de dónde ejecutarlos. El mecanismo natural para su ejecución son los contenedores.
Los contenedores son un mecanismo para empaquetar aplicaciones, pero abstrayéndolas del entorno de hardware en que se ejecutan, ya sea alguna nube (AWS, Azure, GCP) o en un datacenter local, permitiendo la movilidad entre diferentes plataformas.
Desde el punto de vista del desarrollo, permiten empaquetar una aplicación junto a todas sus dependencias, permitiendo desplegarla en segundos ya sea de forma automatizada o manual.
Aliviana la vida de los responsables de la continuidad operativa de un sistema ya que las aplicaciones mejoran su rendimiento debido a la posibilidad de elasticidad.
El despliegue puede ser automatizado a partir de reglas definidas en alguna herramienta de integración continua, por ejemplo, detectando push o merge en alguna rama de tu repositorio de código.
Permite también una transición fluida de infraestructura On-premise hacia alguna nube y viceversa.
Por último, también hay beneficios para la empresa, ya que potencian la cultura DevOps, aceleran el time-to-market de las aplicaciones, habilitan el uso de infraestructuras híbridas y reducen costos, sobre todo si utilizamos servicios de pago por uso en alguna nube.
Foto: vía Unsplash
Para ayudarnos a entender el funcionamiento de los contenedores existe Docker, que es una herramienta que permite ejecutar contenedores de forma local.
También nos permite generar nuevas definiciones de contenedores a partir de otra definición. Docker puede ser ejecutado en su equipo portátil, en un servidor físico o virtual de algún datacenter o en la nube
Docker está compuesto por un servicio a nivel de sistema operativo, una API REST que interactúa con este servicio y una aplicación cliente que permite ejecutar estos comandos de forma local.
Dentro de Docker podemos definir, entre otras cosas:
- Imagen: Es una aplicación empaquetada, por ejemplo un microservicio, que contiene todos los componentes necesarios para hacerla funcionar: el sistema operativo, librerías, configuraciones y la misma aplicación compilada. En Docker, esta aplicación se compone de varias capas:
FROM, que corresponde a la imagen base.
RUN, que ejecuta comandos válidos en la imagen base, que permiten instalar componentes adicionales.
CMD, que ejecuta el comando de salida de la imagen, generalmente es el punto de inicio de un servicio.
- Contenedor: Es una instancia de ejecución de una imagen, creada a partir de su definición. Es el que ejecuta finalmente la aplicación.
Ahora veamos la importancia del Dockerfile en este proceso. Se trata de un archivo de texto que contiene las instrucciones para la creación de una imagen, partiendo de la imagen base, uno o varios comandos RUN, la exposición de algún puerto en caso de servicios de ejecución continua como un microservicio, base de datos, etc. También el comando de ejecución.
Para finalizar, te dejo algunos comandos básicos que debes conocer para trabajar en Docker:
- Listar imágenes
docker image ls
- Construir imagen
docker build -t {name} .
- Etiquetar imagen
docker tag {imageId} {image}:{newVersion}
- Ejecutar contenedor
docker run -d -p {externalPort}:{internalPort} {image}:{version} –name {name}
- Contenedores en ejecución
docker ps
- Detener contenedor
docker stop {id}
Y también te dejo algunos recursos descargables para que comiences tu travesía por la arquitectura orientada a microservicios.
Te invito a revisar nuestras vacantes abiertas y aplicar a nuestros empleos. ¡Sé un yellower!