Ya está abierta la inscripción para Microservicios de marzo de 2023. Consulta la agenda e inscríbete aquí .
Editor – Esta serie de siete artículos ya está completa:
También puede descargar el conjunto completo de artículos, además de información sobre la implementación de microservicios utilizando NGINX Plus, como un libro electrónico: Microservicios: Desde el diseño hasta la implementación .
Consulte también nuestra página de Soluciones de Microservicios .
Actualmente, los microservicios están recibiendo mucha atención: artículos, blogs, debates en las redes sociales y presentaciones en conferencias. Se dirigen rápidamente hacia el pico de expectativas infladas en el ciclo Hype de Gartner . Al mismo tiempo, hay escépticos en la comunidad de software que descartan los microservicios como algo nada nuevo. Los detractores afirman que la idea es simplemente un cambio de marca de SOA. Sin embargo, a pesar de todo el revuelo y el escepticismo, el patrón de arquitectura de microservicios tiene beneficios significativos, especialmente cuando se trata de permitir el desarrollo y la entrega ágiles de aplicações empresariales complejas.
Esta publicación de blog es la primera de una serie de siete partes sobre el diseño, la creación y la implementación de microservicios . Aprenderá sobre el enfoque y cómo se compara con el patrón de arquitectura monolítica más tradicional. Esta serie describirá los distintos elementos de una arquitectura de microservicios. Aprenderá sobre los beneficios y desventajas del patrón de Arquitectura de Microservicios, si tiene sentido para su proyecto y cómo aplicarlo.
Veamos primero por qué debería considerar el uso de microservicios.
Imaginemos que estás empezando a crear una nueva aplicação para pedir taxis destinada a competir con Uber y Hailo. Después de algunas reuniones preliminares y recopilación de requisitos, creará un nuevo proyecto manualmente o utilizando un generador que viene con Rails, Spring Boot, Play o Maven. Esta nueva aplicação tendría una arquitectura hexagonal modular, como en el siguiente diagrama:
En el núcleo de la aplicação se encuentra la lógica empresarial, que se implementa mediante módulos que definen servicios, objetos de dominio y eventos. Alrededor del núcleo hay adaptadores que interactúan con el mundo externo. Los ejemplos de adaptadores incluyen componentes de acceso a bases de datos, componentes de mensajería que producen y consumen mensajes y componentes web que exponen API o implementan una interfaz de usuario.
A pesar de tener una arquitectura lógicamente modular, la aplicação está empaquetada e implementada como un monolito. El formato real depende del lenguaje y del framework de la aplicación. Por ejemplo, muchas aplicações Java se empaquetan como archivos WAR y se implementan en servidores de aplicação como Tomcat o Jetty. Otras aplicações Java están empaquetadas como JAR ejecutables autónomos. De manera similar, las aplicações Rails y Node.js están empaquetadas como una jerarquía de directorios.
Las aplicações escritas en este estilo son extremadamente comunes. Son fáciles de desarrollar ya que nuestros IDE y otras herramientas están enfocadas a construir una única aplicação. Este tipo de aplicações también son fáciles de probar. Puede implementar pruebas de extremo a extremo simplemente iniciando la aplicação y probando la interfaz de usuario con Selenium. Las aplicações monolíticas también son fáciles de implementar. Solo tienes que copiar la aplicação empaquetada a un servidor. También puedes escalar la aplicação ejecutando varias copias detrás de un balanceador de carga. En las primeras etapas del proyecto funciona bien.
Desafortunadamente, este enfoque simple tiene una enorme limitación. Las aplicações exitosas tienen el hábito de crecer con el tiempo y eventualmente volverse enormes. Durante cada sprint, el equipo de desarrollo implementa algunas historias más, lo que, por supuesto, significa agregar muchas líneas de código. Después de unos años, su pequeña y sencilla aplicação se habrá convertido en un monstruoso monolito . Para dar un ejemplo extremo, recientemente hablé con un desarrollador que estaba escribiendo una herramienta para analizar las dependencias entre los miles de JAR en su aplicação de varios millones de líneas de código (LOC). Estoy seguro de que fue necesario el esfuerzo concertado de un gran número de desarrolladores durante muchos años para crear una bestia así.
Una vez que su aplicação se haya convertido en un monolito grande y complejo, su organización de desarrollo probablemente se encontrará en un mundo de problemas. Cualquier intento de desarrollo y entrega ágil fracasará. Uno de los principales problemas es que la aplicação es extremadamente compleja. Es simplemente demasiado grande para que cualquier desarrollador pueda comprenderlo en su totalidad. Como resultado, corregir errores e implementar nuevas funciones correctamente se vuelve difícil y requiere mucho tiempo. Lo que es más, esto tiende a ser una espiral descendente. Si el código base es difícil de entender, los cambios no se realizarán correctamente. Terminarás con una monstruosa e incomprensible bola de barro .
El gran tamaño de la aplicação también ralentizará el desarrollo. Cuanto mayor sea la aplicação, mayor será el tiempo de inicio. Por ejemplo, en una encuesta reciente algunos desarrolladores informaron tiempos de inicio de hasta 12 minutos. También he escuchado anécdotas de aplicações que tardan hasta 40 minutos en iniciarse. Si los desarrolladores tienen que reiniciar periódicamente el servidor de aplicação , pasarán gran parte de su día esperando y su productividad se verá afectada.
Otro problema con una aplicação monolítica grande y compleja es que constituye un obstáculo para la implementación continua. Hoy en día, el estado del arte en materia de aplicações SaaS es impulsar cambios en producción muchas veces al día. Esto es extremadamente difícil de hacer con un monolito complejo ya que es necesario volver a implementar toda la aplicação para actualizar cualquier parte de ella. Los largos tiempos de inicio que mencioné antes tampoco ayudarán. Además, como el impacto de un cambio no suele entenderse muy bien, es probable que tengas que hacer pruebas manuales exhaustivas. Por lo tanto, es prácticamente imposible realizar una implementación continua.
Las aplicações monolíticas también pueden ser difíciles de escalar cuando diferentes módulos tienen requisitos de recursos conflictivos. Por ejemplo, un módulo podría implementar una lógica de procesamiento de imágenes con uso intensivo de CPU y lo ideal sería implementarlo en instancias optimizadas para cómputo de Amazon EC2 . Otro módulo podría ser una base de datos en memoria y sería más adecuado para instancias EC2 optimizadas para memoria . Sin embargo, debido a que estos módulos se implementan juntos, es necesario hacer concesiones en la elección del hardware.
Otro problema con las aplicações monolíticas es la confiabilidad. Debido a que todos los módulos se ejecutan dentro del mismo proceso, un error en cualquier módulo, como una pérdida de memoria, puede potencialmente hacer caer todo el proceso. Además, como todas las instancias de la aplicação son idénticas, ese error afectará la disponibilidad de toda la aplicação.
Por último, pero no por ello menos importante, las aplicações monolíticas hacen que sea extremadamente difícil adoptar nuevos marcos y lenguajes. Por ejemplo, imaginemos que tienes 2 millones de líneas de código escritas utilizando el marco XYZ. Sería extremadamente costoso (tanto en tiempo como en costo) reescribir toda la aplicação para usar el nuevo marco ABC, incluso si ese marco fuera considerablemente mejor. Como resultado, existe una enorme barrera para la adopción de nuevas tecnologías. Estás atrapado con las elecciones tecnológicas que hiciste al comienzo del proyecto.
En resumen: tienes una aplicação crítica para el negocio que ha crecido hasta convertirse en un monolito monstruoso que muy pocos desarrolladores, si es que hay alguno, entienden. Está escrito utilizando tecnología obsoleta e improductiva que dificulta la contratación de desarrolladores talentosos. La aplicação es difícil de escalar y no es confiable. Como resultado, el desarrollo y la entrega ágiles de aplicações son imposibles.
Entonces, ¿qué puedes hacer al respecto?
Muchas organizaciones, como Amazon, eBay y Netflix , han resuelto este problema adoptando lo que ahora se conoce como el patrón de Arquitectura de Microservicios . En lugar de construir una única aplicação monstruosa y monolítica, la idea es dividir la aplicação en un conjunto de servicios más pequeños e interconectados.
Un servicio normalmente implementa un conjunto de características o funcionalidades distintivas, como gestión de pedidos, gestión de clientes, etc. Cada microservicio es una aplicação que tiene su propia arquitectura hexagonal que consta de lógica empresarial junto con varios adaptadores. Algunos microservicios expondrían una API que es consumida por otros microservicios o por los clientes de la aplicación. Otros microservicios podrían implementar una interfaz web. En tiempo de ejecución, cada instancia suele ser una máquina virtual en la nube o un contenedor Docker.
Por ejemplo, una posible descomposición del sistema descrito anteriormente se muestra en el siguiente diagrama:
Cada área funcional de la aplicação ahora está implementada por su propio microservicio. Además, la aplicação web se divide en un conjunto de aplicações web más simples (como una para pasajeros y otra para conductores en nuestro ejemplo de solicitud de taxis). Esto facilita la implementación de experiencias diferenciadas para usuarios, dispositivos o casos de uso especializados específicos.
Cada servicio de backend expone una API REST y la mayoría de los servicios consumen API proporcionadas por otros servicios. Por ejemplo, la gestión de conductores utiliza el servidor de notificaciones para informar a un conductor disponible sobre un posible viaje. Los servicios de UI invocan a los demás servicios para representar páginas web. Los servicios también pueden utilizar comunicación asincrónica basada en mensajes. La comunicación entre servicios se abordará con más detalle más adelante en esta serie.
Algunas API REST también están expuestas a las aplicaciones móviles utilizadas por los conductores y pasajeros. Sin embargo, las aplicaciones no tienen acceso directo a los servicios de backend. En cambio, la comunicación está mediada por un intermediario conocido como API Gateway . La API Gateway se encarga de tareas como el balanceo de carga, el almacenamiento en caché, el control de acceso, la medición de API y la monitorización, y puede implementarse eficazmente mediante NGINX . Los artículos posteriores de la serie abordarán la API Gateway .
El patrón de Arquitectura de Microservicios corresponde al escalamiento del eje Y del Cubo de Escala , que es un modelo 3D de escalabilidad del excelente libro El Arte de la Escalabilidad . Los otros dos ejes de escala son el escalamiento del eje X, que consiste en ejecutar múltiples copias idénticas de la aplicação detrás de un balanceador de carga, y el escalamiento del eje Z (o partición de datos), donde se utiliza un atributo de la solicitud (por ejemplo, la clave principal de una fila o la identidad de un cliente) para enrutar la solicitud a un servidor en particular.
Las aplicações normalmente utilizan los tres tipos de escalamiento juntos. El escalamiento del eje Y descompone la aplicação en microservicios como se muestra arriba en la primera figura de esta sección. En tiempo de ejecución, el escalamiento del eje X ejecuta múltiples instancias de cada servicio detrás de un balanceador de carga para lograr rendimiento y disponibilidad. Algunas aplicações también podrían usar la escala del eje Z para particionar los servicios. El siguiente diagrama muestra cómo se podría implementar el servicio de gestión de viajes con Docker ejecutándose en Amazon EC2.
En tiempo de ejecución, el servicio de gestión de viajes consta de varias instancias de servicio. Cada instancia de servicio es un contenedor Docker. Para lograr una alta disponibilidad, los contenedores se ejecutan en múltiples máquinas virtuales en la nube. Frente a las instancias de servicio se encuentra un balanceador de carga, como NGINX, que distribuye las solicitudes entre las instancias. El equilibrador de carga también puede manejar otras cuestiones como almacenamiento en caché , control de acceso , medición de API y monitoreo .
El patrón de Arquitectura de Microservicios impacta significativamente la relación entre la aplicação y la base de datos. En lugar de compartir un único esquema de base de datos con otros servicios, cada servicio tiene su propio esquema de base de datos. Por un lado, este enfoque está en desacuerdo con la idea de un modelo de datos a nivel de toda la empresa. Además, a menudo da lugar a la duplicación de algunos datos. Sin embargo, tener un esquema de base de datos por servicio es esencial si desea beneficiarse de los microservicios, porque garantiza un acoplamiento flexible. El siguiente diagrama muestra la arquitectura de la base de datos para la aplicação de ejemplo.
Cada uno de los servicios tiene su propia base de datos. Además, un servicio puede utilizar un tipo de base de datos que mejor se adapte a sus necesidades, la denominada arquitectura de persistencia políglota. Por ejemplo, Driver Management, que encuentra conductores cercanos a un pasajero potencial, debe utilizar una base de datos que admita consultas geográficas eficientes.
En la superficie, el patrón de arquitectura de microservicios es similar a SOA. Con ambos enfoques, la arquitectura consta de un conjunto de servicios. Sin embargo, una forma de pensar en el patrón de Arquitectura de Microservicios es que es SOA sin la comercialización y el bagaje percibido de las especificaciones de servicios web (WS-*) y un Bus de Servicios Empresariales (ESB). Las aplicações basadas en microservicios prefieren protocolos más simples y livianos como REST, en lugar de WS-*. También evitan en gran medida el uso de ESB y en su lugar implementan funcionalidades similares a las de ESB en los propios microservicios. El patrón de Arquitectura de Microservicios también rechaza otras partes de SOA, como el concepto de esquema canónico.
El patrón de Arquitectura de Microservicios tiene una serie de beneficios importantes. En primer lugar, aborda el problema de la complejidad. Descompone lo que de otro modo sería una monstruosa aplicação monolítica en un conjunto de servicios. Si bien la cantidad total de funcionalidad no ha cambiado, la aplicação se ha dividido en partes o servicios manejables. Cada servicio tiene un límite bien definido en forma de una API basada en mensajes o RPC. El patrón de Arquitectura de Microservicios impone un nivel de modularidad que, en la práctica, es extremadamente difícil de lograr con una base de código monolítica. En consecuencia, los servicios individuales son mucho más rápidos de desarrollar y mucho más fáciles de entender y mantener.
En segundo lugar, esta arquitectura permite que cada servicio sea desarrollado independientemente por un equipo centrado en ese servicio. Los desarrolladores son libres de elegir cualquier tecnología que tenga sentido, siempre que el servicio respete el contrato de API. Por supuesto, la mayoría de las organizaciones querrían evitar la anarquía total y limitar las opciones tecnológicas. Sin embargo, esta libertad significa que los desarrolladores ya no están obligados a utilizar las tecnologías posiblemente obsoletas que existían al comienzo de un nuevo proyecto. Al escribir un nuevo servicio, tienen la opción de utilizar tecnología actual. Además, como los servicios son relativamente pequeños, resulta posible reescribir un servicio antiguo utilizando la tecnología actual.
En tercer lugar, el patrón de Arquitectura de Microservicios permite que cada microservicio se implemente de forma independiente. Los desarrolladores nunca necesitan coordinar la implementación de cambios que son locales a su servicio. Este tipo de cambios se pueden implementar tan pronto como se hayan probado. El equipo de UI puede, por ejemplo, realizar pruebas A/B e iterar rápidamente sobre los cambios de UI. El patrón de Arquitectura de Microservicios hace posible la implementación continua.
Finalmente, el patrón de Arquitectura de Microservicios permite escalar cada servicio de forma independiente. Puede implementar solo la cantidad de instancias de cada servicio que satisfagan sus restricciones de capacidad y disponibilidad. Además, puede utilizar el hardware que mejor se adapte a los requisitos de recursos de un servicio. Por ejemplo, puede implementar un servicio de procesamiento de imágenes con uso intensivo de CPU en instancias EC2 Compute Optimized e implementar un servicio de base de datos en memoria en instancias EC2 Memory Optimized.
Como escribió Fred Brooks hace casi 30 años, no existen soluciones milagrosas. Como cualquier otra tecnología, la arquitectura de microservicios tiene desventajas. Un inconveniente es el nombre en sí. El término microservicio pone demasiado énfasis en el tamaño del servicio. De hecho, hay algunos desarrolladores que abogan por crear servicios LOC de 10 a 100 niveles extremadamente detallados. Si bien los servicios pequeños son preferibles, es importante recordar que son un medio para un fin y no el objetivo principal. El objetivo de los microservicios es descomponer suficientemente la aplicação para facilitar el desarrollo y la implementación de aplicação ágiles.
Otro inconveniente importante de los microservicios es la complejidad que surge del hecho de que una aplicação de microservicios es un sistema distribuido. Los desarrolladores deben elegir e implementar un mecanismo de comunicación entre procesos basado en mensajería o RPC. Además, deben escribir código para gestionar fallos parciales, ya que el destino de una solicitud podría ser lento o no estar disponible. Si bien nada de esto es ciencia espacial, es mucho más complejo que en una aplicação monolítica donde los módulos se invocan entre sí mediante llamadas a métodos o procedimientos a nivel de lenguaje.
Otro desafío con los microservicios es la arquitectura de base de datos particionada. Las transacciones comerciales que actualizan múltiples entidades comerciales son bastante comunes. Este tipo de transacciones son triviales de implementar en una aplicação monolítica porque hay una única base de datos. Sin embargo, en una aplicação basada en microservicios, es necesario actualizar varias bases de datos propiedad de diferentes servicios. El uso de transacciones distribuidas normalmente no es una opción, y no sólo por el teorema CAP . Simplemente no son compatibles con muchas de las bases de datos NoSQL y agentes de mensajería altamente escalables de la actualidad. Al final, tienes que usar un enfoque basado en la consistencia, lo que resulta más desafiante para los desarrolladores.
Probar una aplicação de microservicios también es mucho más complejo. Por ejemplo, con un framework moderno como Spring Boot, es fácil escribir una clase de prueba que inicie una aplicação web monolítica y pruebe su API REST. En cambio, una clase de prueba similar para un servicio necesitaría iniciar dicho servicio y todos los servicios de los que depende (o al menos configurar stubs para esos servicios). Una vez más, esto no es ciencia espacial, pero es importante no subestimar la complejidad que implica hacerlo.
Otro desafío importante con el patrón de Arquitectura de Microservicios es implementar cambios que abarquen múltiples servicios. Por ejemplo, imaginemos que está implementando una historia que requiere cambios en los servicios A, B y C, donde A depende de B y B depende de C. En una aplicação monolítica, podría simplemente cambiar los módulos correspondientes, integrar los cambios e implementarlos de una sola vez. Por el contrario, en un patrón de arquitectura de microservicios es necesario planificar y coordinar cuidadosamente la implementación de cambios en cada uno de los servicios. Por ejemplo, necesitaría actualizar el servicio C, seguido del servicio B y, finalmente, el servicio A. Afortunadamente, la mayoría de los cambios suelen afectar solo a un servicio y los cambios de múltiples servicios que requieren coordinación son relativamente raros.
Implementar una aplicação basada en microservicios también es mucho más complejo. Una aplicação monolítica simplemente se implementa en un conjunto de servidores idénticos detrás de un balanceador de carga tradicional. Cada instancia de aplicação se configura con las ubicaciones (host y puertos) de los servicios de infraestructura, como la base de datos y un agente de mensajes. Por el contrario, una aplicação de microservicio normalmente consta de una gran cantidad de servicios. Por ejemplo, Hailo tiene 160 servicios diferentes y Netflix tiene más de 600 según Adrian Cockcroft [Editor – Hailo ha sido adquirido por MyTaxi] . Cada servicio tendrá múltiples instancias de tiempo de ejecución. Se trata de muchas más partes móviles que necesitan configurarse, implementarse, escalarse y monitorearse. Además, también necesitará implementar un mecanismo de descubrimiento de servicios (que se analizará en una publicación posterior) que permita a un servicio descubrir las ubicaciones (hosts y puertos) de cualquier otro servicio con el que necesite comunicarse. Los enfoques manuales y basados tradicionalmente en tickets de problemas no pueden escalar a este nivel de complejidad. En consecuencia, implementar con éxito una aplicação de microservicios requiere un mayor control de los métodos de implementación por parte de los desarrolladores y un alto nivel de automatización.
Un enfoque hacia la automatización es utilizar un PaaS estándar como Cloud Foundry . Una PaaS proporciona a los desarrolladores una forma sencilla de implementar y administrar sus microservicios. Los aísla de preocupaciones como la adquisición y configuración de recursos de TI. Al mismo tiempo, los profesionales de sistemas y redes que configuran el PaaS pueden garantizar el cumplimiento de las mejores prácticas y de las políticas de la empresa. Otra forma de automatizar la implementación de microservicios es desarrollar lo que esencialmente es su propio PaaS. Un punto de partida típico es utilizar una solución de agrupamiento, como Kubernetes , junto con una tecnología como Docker. Más adelante en esta serie, veremos cómo los enfoques de entrega de aplicação basados en software como NGINX Plus, que maneja fácilmente el almacenamiento en caché, el control de acceso, la medición de API y el monitoreo a nivel de microservicio, pueden ayudar a resolver este problema.
Desarrollar aplicações complejas es inherentemente difícil. Una arquitectura monolítica sólo tiene sentido para aplicações simples y livianas. Terminarás en un mundo de dolor si lo usas para aplicações complejas. El patrón de arquitectura de microservicios es la mejor opción para aplicações complejas y en evolución a pesar de los inconvenientes y los desafíos de implementación.
En publicaciones de blog posteriores, profundizaré en los detalles de varios aspectos del patrón de arquitectura de microservicios y analizaré temas como el descubrimiento de servicios, las opciones de implementación de servicios y las estrategias para refactorizar una aplicação monolítica en servicios.
Manténganse al tanto…
Editor – Esta serie de siete artículos ya está completa:
También puede descargar el conjunto completo de artículos, además de información sobre la implementación de microservicios utilizando NGINX Plus, como un libro electrónico: Microservicios: Desde el diseño hasta la implementación .
El bloguero invitado Chris Richardson es el fundador del CloudFoundry.com original, una de las primeras PaaS (plataforma como servicio) de Java para Amazon EC2. Ahora asesora a organizaciones para mejorar la forma en que desarrollan e implementan aplicações.
"Esta publicación de blog puede hacer referencia a productos que ya no están disponibles o que ya no reciben soporte. Para obtener la información más actualizada sobre los productos y soluciones F5 NGINX disponibles, explore nuestra familia de productos NGINX . NGINX ahora es parte de F5. Todos los enlaces anteriores de NGINX.com redirigirán a contenido similar de NGINX en F5.com.