Definiendo la arquitectura de aplicaciones basadas en Contenedores y Microservicios
Los microservicios ofrecen grandes beneficios, pero también plantean enormes desafíos. Los patrones de arquitectura de microservicios son los pilares fundamentales para crear aplicaciones basadas en microservicios.
Anteriormente vimos conceptos básicos sobre contenedores y Docker. Esa era la información mínima necesaria para comenzar. Sin embargo, aun cuando los contenedores son habilitadores y se adaptan perfectamente a los microservicios, no son obligatorios este tipo de arquitectura y muchos de los conceptos que veremos, también podrían aplicarse sin contenedores. Sin embargo, esta guía se centra en la intersección de ambos debido a la importancia de los contenedores.
Las aplicaciones empresariales pueden ser complejas y a menudo se componen de múltiples servicios en lugar de una sola aplicación basada en servicios. Para esos casos, se deben comprender los enfoques arquitectónicos adicionales, como los microservicios y ciertos patrones del diseño orientado por el dominio (DDD – Domain-Driven Design), más los conceptos de orquestación de contenedores. Tenga en cuenta que este capítulo describe no sólo microservicios en contenedores, sino también cualquier aplicación contenerizada.
En el modelo de contenedores, cada contenedor, o instancia de una imagen, representa un proceso único. Al definir una imagen como los límites de un proceso, se pueden crear primitivas que se pueden usar para escalar el proceso o ejecutarlo por lotes (en modo batch).
Cuando se diseña una imagen, verá la definición de un ENTRYPOINT en el Dockerfile. Esto define el proceso cuya vida determinará la vida útil del contenedor. Cuando el proceso finaliza, también termina la vida del contenedor. Los contenedores pueden representar procesos de larga ejecución como servidores web, pero también pueden representar procesos de corta duración como trabajos por lotes, que anteriormente se podrían haber implementado como Azure WebJobs.
Si el proceso falla, el contenedor finaliza y el orquestador se hace cargo. Si el orquestador se configuró para mantener cinco instancias en ejecución y una falla, el orquestador creará otra instancia de contenedor para reemplazar el proceso fallido. En un trabajo por lotes, el proceso se inicia con parámetros. Cuando el proceso se completa, el trabajo está terminado. Más adelante profundizaremos sobre los orquestadores.
Es posible encontrar un escenario en el que desee ejecutar varios procesos en un contenedor único. En ese caso, ya que sólo puede haber un punto de entrada por contenedor, se podría ejecutar un script dentro del contenedor que inicie tantos programas como sea necesario. Por ejemplo, puede usar un sistema como Supervisor, o una herramienta similar, para encargarse de iniciar múltiples procesos dentro de un contenedor único. Sin embargo, aunque puede encontrar arquitecturas que contienen múltiples procesos por contenedor, este enfoque no es muy común.
Es posible que desee construir una única aplicación web o servicio que se desplieguen monolíticamente y hacerlo como un contenedor. La aplicación en sí misma puede no ser monolítica internamente, sino estructurarse como varias librerías, componentes o incluso capas (capa de aplicación, capa de dominio, capa de acceso a datos, etc.). Externamente, sin embargo, es un contenedor único: un proceso único, una aplicación web o un servicio únicos.
Para gestionar esta arquitectura, se despliega un solo contenedor para representar la aplicación. Para aumentar la capacidad, simplemente se agregan más copias, en servidores o máquinas virtuales diferentes, con un balanceador de carga al frente. La simplicidad proviene de administrar un despliegue único, en un solo contenedor o máquina virtual.
Figura 4-1. Ejemplo de la arquitectura de una aplicación monolítica contenerizada
Puede incluir múltiples componentes, librerías o capas internas en cada contenedor, como se ilustra en la Figura 4-1. Sin embargo, aunque este patrón monolítico podría entrar en conflicto con el principio del contenedor "un contenedor hace una cosa y lo hace en un proceso", podría estar justificado en algunos casos.
La desventaja de este enfoque resulta evidente si la aplicación crece y es necesario escalarla. Si toda la aplicación se puede escalar, no es realmente un problema. Sin embargo, en la mayoría de los casos, sólo algunas partes de la aplicación son los “cuellos de botella” que requieren escalado, mientras que el resto se usa menos.
Por ejemplo, en una aplicación típica de comercio electrónico, es probable que necesite escalar el subsistema de información de productos, porque hay muchos más clientes buscando productos que los que compran. Hay más clientes que usan su carrito de compras que los que usan el canal de pagos. Menos clientes agregan comentarios o ven su historial de compras. Y es posible que sólo tenga un puñado de empleados que necesiten administrar el contenido y las campañas de marketing. Si escala el diseño monolítico, todo el código para estas tareas diferentes se despliega varias veces y se escala en el mismo grado, lo que representa un uso poco eficiente de los recursos.
Hay muchas formas de escalar una aplicación: duplicación horizontal, división de diferentes áreas de la aplicación y creación de particiones por conceptos similares o data del negocio. Pero, además del problema de escalar todos los componentes, los cambios en un solo componente requieren una nueva prueba completa de toda la aplicación y un despliegue completo de todas las instancias.
Sin embargo, el enfoque monolítico es de uso frecuente, porque el desarrollo de la aplicación es inicialmente más fácil que para los enfoques de microservicios. Por lo tanto, muchas organizaciones desarrollan usando este enfoque arquitectónico. Si bien algunas organizaciones han tenido buenos resultados, otras están alcanzando los límites. Muchas organizaciones diseñaron sus aplicaciones utilizando este modelo porque las herramientas y la infraestructura hicieron que fuera muy difícil construir arquitecturas orientadas a servicios (SOA) hace años y no vieron la necesidad hasta que la aplicación creció.
Desde la perspectiva de la infraestructura, cada servidor puede ejecutar muchas aplicaciones y tener una relación aceptable de eficiencia en el uso de recursos, como se muestra en la Figura 4-2.
Figura 4-2. Enfoque monolítico: Host ejecutando multiples aplicaciones, cada una como un contenedor
Las aplicaciones monolíticas en Microsoft Azure se pueden implementar utilizando máquinas virtuales dedicadas para cada instancia. Además, al usar los conjuntos de escalado de Azure, se pueden escalar fácilmente las máquinas virtuales. También se puede usar un Azure App Service para aplicaciones monolíticas y escalar instancias fácilmente sin necesidad de administrar las máquinas virtuales. Desde 2016, Azure App Services también puede ejecutar instancias individuales de contenedores Docker, lo que simplifica el despliegue.
Como entorno de control de calidad o entorno limitado de producción, puede desplegar múltiples máquinas virtuales como host Docker y balancearlas usando el balanceador de carga de Azure, como se muestra en la Figura 4-3. Esto le permite gestionar el escalado, aunque sólo de forma gruesa, porque toda la aplicación vive en un solo contenedor.
Figura 4-3. Ejemplo de múltiples hosts escalando una aplicación de contenedor único
El despliegue en los distintos hosts se puede gestionar con técnicas de despliegue tradicionales. Los hosts de Docker se pueden administrar con comandos como docker run o docker-compose realizados manualmente, o mediante la automatización, como los procesos de entrega continua (CD – Continuous Delivery).
El uso de contenedores para gestionar el despliegue de aplicaciones monolíticas tiene sus ventajas. Escalar instancias de contenedor es mucho más rápido y más fácil que desplegar máquinas virtuales adicionales. Incluso si usa los conjuntos de escalado, las máquinas virtuales requieren tiempo para arrancar. Cuando se despliegan como instancias de aplicaciones tradicionales en lugar de contenedores, la configuración de la aplicación se administra como parte de la máquina virtual, lo que no es ideal.
El despliegue de actualizaciones como imágenes Docker es mucho más rápido y eficiente en el uso de la red. Las imágenes de Docker generalmente comienzan en segundos, lo que acelera el proceso. Destruir una instancia de imagen de Docker es tan fácil como emitir un comando docker stop y generalmente se completa en menos de un segundo.
Como los contenedores son inmutables por diseño, nunca tendrá que preocuparse por máquinas virtuales corruptas. Por el contrario, una máquina virtual tiene muchas partes móviles que pueden verse afectadas por factores externos no controlados.
Si bien las aplicaciones monolíticas pueden beneficiarse de Docker, apenas estamos echándole un vistazo a los beneficios. Las ventajas adicionales de la gestión de contenedores provienen del despliegue con orquestadores, que administran las diversas instancias y el ciclo de vida de cada contenedor. Descomponer una aplicación monolítica en subsistemas que puedan escalarse, desarrollarse y desplegarse individualmente es el punto de entrada al mundo de los microservicios.
Si quiere evaluar un contenedor desplegado en Azure o cuando la aplicación requiere sólo un contenedor, el Azure App Service proporciona una manera excelente de ofrecer servicios escalables basados en un solo contenedor. Es sencillo usar un Azure App Service. Proporciona una buen integración con Git para que sea más fácil tomar su código, compilarlo en Visual Studio y desplegarlo directamente en Azure.
Figura 4-4. Publicando una aplicación de contenedor único a un Azure App Service desde Visual Studio
Sin Docker, si necesitara otras facilidades, frameworks o dependencias que no son compatibles con Azure App Service, tendría que esperar hasta que el equipo de Azure actualizara esas dependencias en el App Service. O tendría que cambiar a otros servicios como Azure Service Fabric, Azure Cloud Services o incluso máquinas virtuales, donde tiene más control y puede instalar un componente o un framework necesario para su aplicación.
El soporte de contenedores en Visual Studio 2017 le da la posibilidad de incluir lo que desee en el entorno de la aplicación, como se muestra en la Figura 4-4. Como lo está ejecutando en un contenedor, si agrega una dependencia a su aplicación, puede incluir la dependencia en su Dockerfile o en la imagen Docker.
Como también puede ver en la Figura 4-4, el flujo de publicación canaliza la imagen a través de un registro de contenedores. Puede ser Azure Container Registry (un registro cercano a sus despliegues en Azure y asegurado por grupos y cuentas de Azure Active Directory) o cualquier otro registro de Docker, como Docker Hub o un registro local.
En la mayoría de los casos, puede pensar en un contenedor como una instancia de un proceso. Un proceso no mantiene un estado persistente. Si bien un contenedor puede escribir en su almacenamiento local, suponer que una instancia se mantendrá indefinidamente sería como suponer que una única ubicación en la memoria será duradera. Se debe entender que las imágenes, como los procesos, pueden tener múltiples instancias que eventualmente serán eliminadas. Si se administran con un orquestador, también debe suponer que se pueden mover de un nodo o máquina virtual a otra.
Se pueden usar las siguientes soluciones para gestionar datos persistentes en aplicaciones Docker:
Desde el host Docker, como Docker Volumes:
Desde almacenamiento remoto:
Desde el contenedor Docker:
Docker proporciona una función denominada overlay file system. Esto implementa una tarea copy-on-write que almacena información actualizada en el sistema de ficheros raíz del contenedor. Esta información se añade a la imagen original en la que se basa el contenedor. Si se borra el contenedor del sistema, se pierden esas modificaciones. Por lo tanto, si bien es posible salvar el estado de un contenedor dentro de su almacenamiento local, el diseño de un sistema en torno a esto entraría en conflicto con la premisa del diseño de contenedor, que por defecto es stateless.
Sin embargo, los volúmenes Docker presentados anteriormente son ahora la forma preferida de manejar los datos locales en Docker. Si necesita más información sobre el almacenamiento en contenedores, consulte en Docker storage drivers y About images, containers and storage drivers.
A continuación, se proporcionan más detalles sobre estas opciones.
Los volumes son directorios mapeados desde el sistema operativo host a directorios en los contenedores. Cuando el código en el contenedor accede al directorio mapeado, está accediendo realmente a un directorio en el sistema operativo del host. Este directorio no está vinculado a la vida útil del contenedor en sí mismo y el directorio es administrado por Docker y esta aislado de la funcionalidad básica del host. Por lo tanto, los volúmenes de datos están diseñados para que persistan independientemente de la vida útil del contenedor. Si elimina un contenedor o una imagen del host Docker, los datos que persisten en el volumen no se borran.
Los volúmenes pueden ser nombrados o anónimos (por defecto). Los volúmenes nombrados son la evolución de los Data Volume Containers y facilitan el intercambio de datos entre contenedores. Los volúmenes también soportan volume drivers, que le permiten almacenar datos en hosts remotos, entre otras opciones.
Las bind mounts están disponibles desde hace mucho tiempo y permiten el mapeo de cualquier carpeta a un punto de montaje en un contenedor. Las bind mounts tienen más limitaciones que los volúmenes y algunos problemas importantes de seguridad, por lo que los volúmenes son la opción recomendada.
Los tmpfs mounts son básicamente carpetas virtuales que viven sólo en la memoria del host y nunca se escriben en el sistema de archivos. Son rápidos y seguros, pero usan memoria y obviamente sólo están pensados para datos no persistentes.
Como se muestra en la Figura 4-5, los volúmenes regulares de Docker se almacenan fuera de los propios contenedores, pero dentro de los límites físicos del host. Sin embargo, los contenedores Docker no pueden acceder a un volumen desde un host a otro. En otras palabras, con estos volúmenes no es posible gestionar los datos compartidos entre contenedores que se ejecutan en diferentes hosts Docker, aunque podría lograrse con un volume driver compatible con hosts remotos.
Figura 4-5. Volúmenes Dpcker y fuentes de datos externos para aplicaciones en contenedores
Además, cuando los contenedores Docker son manejados por un orquestador, los contenedores se pueden "mover" entre hosts, dependiendo de las optimizaciones realizadas por el cluster. Por lo tanto, no se recomienda utilizar volúmenes para datos del negocio. Pero son un buen mecanismo para trabajar con ficheros de trazas, temporales o similares que no afectarán la consistencia de los datos del negocio.
Las fuentes de datos remotas como Azure SQL Database, Azure Cosmos DB o un caché remoto como Redis, se pueden usar en aplicaciones en contenedores, de la misma forma como cuando se desarrollan sin contenedores. Esta es una forma comprobada de almacenar datos de aplicaciones de negocio.
Azure Storage. Por lo general, los datos del negocio deberán ubicarse en recursos externos o bases de datos, como Azure Storage. Azure Storage, en concreto, proporciona los siguientes servicios en la nube:
Bases de datos relacionales y NoSQL. Hay muchas opciones para bases de datos externas, desde bases de datos relacionales como SQL Server, PostgreSQL, Oracle o bases de datos NoSQL como Azure Cosmos DB, MongoDB, etc. Estas bases de datos no van a ser explicadas como parte de esta guía ya que son un tema completamente diferente.
La arquitectura orientada a servicios (SOA) fue un término usado en exceso y ha significado diferentes cosas para distintas personas. Pero como denominador común, SOA significa que una aplicación se estructura descomponiéndola en múltiples servicios (más comúnmente como servicios HTTP) que pueden clasificarse en diferentes tipos, como subsistemas o capas.
Esos servicios ahora se pueden implementar como contenedores Docker, lo que resuelve problemas de despliegue, porque todas las dependencias están incluidas en la imagen del contenedor. Sin embargo, cuando necesite escalar aplicaciones SOA, puede tener problemas de escalabilidad y disponibilidad si está desplegando en base a hosts Docker únicos. Aquí es donde el software de gestión de cluster Docker o un orquestador le ayudarán, tal como se explica en secciones posteriores donde describimos enfoques de despliegue para microservicios.
Los contenedores Docker son útiles, pero no necesarios, tanto para las arquitecturas orientadas a servicios tradicionales como para las arquitecturas más avanzadas de microservicios.
Los microservicios se derivan de SOA, pero SOA es diferente de la arquitectura de microservicios. Las características como grandes agentes centrales (central brokers), orquestadores centrales a nivel de organización y el Enterprise Service Bus (ESB) son típicos en SOA. Pero en la mayoría de los casos, estos son anti patrones en la comunidad de microservicios. De hecho, algunas personas argumentan que "la arquitectura de microservicio es SOA implementada correctamente".
Esta guía se centra en microservicios, porque un enfoque SOA es menos riguroso que los requisitos y las técnicas utilizados en una arquitectura de microservicios. Si sabe cómo crear una aplicación basada en microservicios, también sabrá cómo crear una aplicación más simple orientada a servicios.
Una arquitectura de microservicios es un enfoque para construir una aplicación de servidor como un conjunto de pequeños servicios. Es decir, una arquitectura de microservicios está orientada fundamentalmente hacia el back-end, aunque también se está usando el enfoque para el front-end. Cada servicio se ejecuta en su propio proceso y se comunica con otros procesos utilizando protocolos como HTTP/HTTPS, WebSockets o AMQP. Cada microservicio implementa una funcionalidad específica, de principio o fin, del negocio o del dominio, dentro de los límites de cierto contexto. Además, cada uno se debe poder desarrollar y desplegar de forma independiente. Finalmente, cada microservicio debe poseer su modelo de datos y su lógica de dominio relacionados (soberanía y gestión descentralizada de datos) que pueden estar basados en diferentes tecnologías de almacenamiento de datos (SQL, NoSQL) y diferentes lenguajes de programación.
¿Qué tamaño debería tener un microservicio? Al desarrollar un microservicio, el tamaño no debe ser el punto importante. En cambio, lo importante debería ser crear servicios ligeramente acoplados para tener autonomía en el desarrollo, despliegue y escalamiento en cada servicio. Por supuesto, al identificar y diseñar microservicios, se deben hacer lo más pequeños posible, siempre que no tenga demasiadas dependencias directas con otros microservicios. Más importante que el tamaño del microservicio es la cohesión interna que debe tener y su independencia de otros servicios.
¿Por qué una arquitectura de microservicios? En resumen, porque proporciona agilidad a largo plazo. Los microservicios facilitan el mantenimiento en sistemas complejos, grandes y que requieren alta escalabilidad, porque permiten crear aplicaciones basadas en muchos servicios, que se despliegan independientemente y que tienen ciclos de vida granulares y autónomos.
Como beneficio adicional, los microservicios se pueden escalar de forma independiente. En lugar de tener una aplicación única monolítica, que se debe escalar como una unidad, se pueden escalar individualmente microservicios específicos. De esta forma, puede escalar sólo el área funcional que necesita más potencia de procesamiento o ancho de banda de red para soportar la demanda, en lugar de ampliar otras áreas de la aplicación que no necesitan escalarse. Eso significa ahorros de costes porque se aprovechan mejor los recursos y se necesita menos hardware.
Figura 4-6. Despliegue monolítico versus el enfoque de microservicios
Como muestra la Figura 4-6, el enfoque de microservicios permite agilizar los cambios e iterar rápidamente en cada microservicio, ya que es más fácil cambiar áreas pequeñas y específicas de aplicaciones que, en conjunto, son grandes, complejas y escalables.
Una arquitectura basada en microservicios granulares, es un facilitador para las prácticas de integración y entrega continuas (CI/CD). También ayuda a entregar más rápido las nuevas funciones en la aplicación, ya que la composición fina de las funciones le permite ejecutar y probar microservicios en forma aislada y desarrollarlos de manera autónoma, manteniendo claros los contratos entre ellos. Siempre que no cambie las interfaces o los contratos, puede cambiar la implementación interna de cualquier microservicio o agregar nuevas funcionalidades sin romper otros microservicios.
Es importante tener en cuenta los siguientes aspectos, para tener éxito al entrar en producción con un sistema basado en microservicios:
De estos, sólo se cubren o se presentan los tres primeros en esta guía. Los dos últimos puntos, que están relacionados con el ciclo de vida de las aplicaciones, se tratan en la guía Containerized Docker Application Lifecycle with Microsoft Platform and Tools.
Una regla importante en la arquitectura de microservicios es que cada microservicio debe poseer sus datos y lógica de dominio. Del mismo modo que una aplicación completa posee su lógica y datos, también cada microservicio debe tener su lógica y datos bajo un ciclo de vida autónomo, con despliegue independiente de otros.
Esto significa que el modelo conceptual del dominio puede diferir entre subsistemas o microservicios. Por ejemplo, las aplicaciones empresariales, donde las aplicaciones de gestión de relaciones con los clientes (CRM – Customer Relationship Management), los subsistemas de compras transaccionales y los subsistemas de atención al cliente, utilizan cada uno atributos y datos particulares del cliente, pero cada uno ve al cliente de una forma particular y emplea un Bounded Context diferente.
Este principio es similar en el diseño orientado por el dominio (DDD – Domain-Driven Design), donde cada Bounded Context o subsistema autónomo o servicio debe poseer su modelo de dominio (datos más lógica y comportamiento). Cada Bounded Context por DDD se correlaciona con un microservicio empresarial (uno o varios servicios). (Ampliamos este punto sobre el patrón de Bounded Context en la siguiente sección).
Por otro lado, el enfoque tradicional (datos monolíticos) utilizado en muchas aplicaciones, es tener una base de datos única y centralizada o sólo unas pocas bases de datos. A menudo se trata de una base de datos SQL normalizada que se utiliza para toda la aplicación y todos sus subsistemas internos, como se muestra en la Figura 4-7.
Figure 4-7. Comparación de la soberanía de datos: base de datos monolítica versus microservicios
El enfoque de la base de datos centralizada parece más simple inicialmente y también parece permitir la reutilización de entidades en diferentes subsistemas para que todo sea coherente. Pero la realidad es que se suele terminar con tablas enormes que sirven a muchos subsistemas diferentes y que incluyen atributos y columnas que no son necesarios en la mayoría de los casos. Es como tratar de usar el mismo mapa para hacer senderismo, un viaje largo en automóvil y aprender geografía.
Una aplicación monolítica con una sola base de datos relacional tiene dos ventajas importantes: las transacciones ACID y el lenguaje SQL, ambos funcionan en todas las tablas y datos relacionados con su aplicación. Este enfoque proporciona una forma de hacer fácilmente una consulta que combina datos de múltiples tablas.
Sin embargo, el acceso a los datos se vuelve mucho más complejo cuando se trabaja en una arquitectura de microservicios. Pero, de todas formas, se deben usar transacciones ACID dentro de un microservicio o Bounded Context y considerar que los datos son privados para cada microservicio y sólo se puede acceder a ellos a través de su API. El encapsulamiento de los datos garantiza que los microservicios se acoplen de manera flexible y puedan evolucionar independientemente el uno del otro. Si varios servicios accedieran a los mismos datos, las actualizaciones del modelo de datos requerirían actualizaciones coordinadas de todos los servicios. Esto rompería la autonomía del ciclo de vida del microservicio. Pero las estructuras de datos distribuidas significan que no se puede realizar una transacción ACID única en microservicios. Esto a su vez significa que debe usar consistencia eventual cuando un proceso del negocio abarca múltiples microservicios. Esto es mucho más difícil de implementar que hacer simples “joins” en SQL, porque no se pueden establecer restricciones de integridad referencial ni usar transacciones distribuidas entre bases de datos diferentes, como veremos más adelante. De forma similar, muchas otras características de bases de datos relacionales no están disponibles cuando se trabaja con múltiples microservicios.
Yendo aún más lejos, los microservicios diferentes podrían usar diferentes tipos de bases de datos. Las aplicaciones modernas almacenan y procesan diversos tipos de datos y una base de datos relacional no siempre es la mejor opción. Para algunos casos, una base de datos NoSQL como Azure DocumentDB o MongoDB podría ser un modelo más conveniente y ofrecer un mejor rendimiento y escalabilidad que una base de datos SQL como SQL Server o Azure SQL Database. En otros casos, una base de datos relacional podría seguir siendo la mejor opción. Por lo tanto, las aplicaciones basadas en microservicios a menudo usan una mezcla de bases de datos SQL y NoSQL, que a veces se denomina enfoque de persistencia políglota.
Una arquitectura particionada y de persistencia políglota para los datos tiene muchos beneficios. Estos incluyen servicios poco acoplados y un mejor rendimiento, escalabilidad, costes y capacidad de administración. Sin embargo, puede presentar algunos desafíos de administración de datos distribuidos, como explicaremos en "Identificación de los límites del modelo de dominio" más adelante en este capítulo.
El concepto de microservicio se deriva del patrón Bounded Context (BC) en el diseño orientado por el dominio (DDD). DDD maneja modelos grandes dividiéndolos en múltiples BC y siendo explícito sobre sus límites. Cada BC debe tener su propio modelo y base de datos, es decir, cada microservicio es el dueño de sus datos. Además, cada BC generalmente tiene sus propios términos, que establecen el lenguaje ubicuo que facilita la comunicación entre los desarrolladores y los expertos del dominio.
Esos términos (principalmente entidades de dominio) en el lenguaje ubicuo pueden tener diferentes significados en diferentes Bounded Contexts, incluso cuando diferentes entidades de dominio comparten la misma identidad (es decir, la ID única que se utiliza para leer la entidad del almacenamiento). Por ejemplo, en un BC de perfiles de usuario, la entidad “Usuario” puede compartir el identificador con la entidad “Comprador” en el Bounded Context de pedidos.
Por lo tanto, un microservicio es como un Bounded Context, pero también se trata de un servicio distribuido. Se construye como un proceso separado para cada BC y debe usar los protocolos distribuidos mencionados anteriormente, como HTTP/HTTPS, WebSockets o AMQP. El patrón de Bounded Context, sin embargo, no especifica si es un servicio distribuido o simplemente es un límite lógico (como un subsistema genérico) dentro de una aplicación que se despliega monolíticamente.
Es importante destacar que la definición de un servicio por cada BC es un buen punto de inicio. Pero el diseño no tiene por qué restringirse a eso. En ocasiones, es necesario diseñar un BC o un microservicio comercial componiendo varios servicios físicos. Pero, en última instancia, ambos patrones, el Bounded Context y el microservicio, están estrechamente relacionados.
DDD se beneficia de los microservicios al obtener límites reales en forma de microservicios distribuidos. Pero las ideas como mantener modelos privados a los microservicios y no compartirlos, es lo que también se busca en un Bounded Context.
Es bueno detenerse en este punto y analizar la diferencia entre arquitectura lógica y arquitectura física y cómo se aplica esto al diseño de aplicaciones basadas en microservicios.
Para comenzar, el desarrollo de microservicios no requiere ninguna tecnología específica. Por ejemplo, los contenedores Docker no son obligatorios para crear una arquitectura basada en microservicios. Esos microservicios también podrían ejecutarse como procesos simples. Es decir, microservicios es una arquitectura lógica.
Además, incluso cuando se podría implementar un microservicio como un servicio único, proceso o contenedor (por simplicidad, ese es el enfoque adoptado en la versión inicial de eShopOnContainers), esta paridad entre el microservicio del negocio y el servicio o contenedor no es necesariamente requerida en todos los casos, cuando se desarrolla una aplicación grande y compleja compuesta de muchas docenas o incluso cientos de servicios.
Aquí es donde hay una diferencia entre la arquitectura lógica y física de una aplicación. La arquitectura lógica y los límites lógicos de un sistema no necesariamente se correlacionan uno a uno con la arquitectura física o de despliegue. Puede suceder, pero a menudo no lo hace.
Aunque es posible que se hayan identificado algunos microservicios del negocio o Bounded Contexts, esto no significa que la mejor forma de implementarlos sea siempre mediante la creación de un servicio único (como una API web de ASP.NET) o un contenedor Docker único para cada microservicio del negocio. Resulta demasiado rígido tener una regla que diga que cada microservicio debe implementarse como un servicio o contenedor único.
Por lo tanto, un microservicio del negocio o un Bounded Context se refieren a una arquitectura lógica, que puede coincidir, o no, con la arquitectura física. El punto importante es que un microservicio del negocio o un BC debe ser autónomo y permitir que el código y el estado se actualicen, desplieguen y escalen independientemente.
Como se muestra en la Figura 4-8, el microservicio del catálogo podría estar compuesto por varios servicios o procesos. Estos podrían ser múltiples servicios API Web de ASP.NET o cualquier otro tipo de servicios utilizando HTTP o cualquier otro protocolo. Más importante aún, los servicios podrían compartir los mismos datos, siempre que estos servicios sean coherentes con respecto al mismo dominio del negocio.
Figura 4-8. Microservicio del negocio con varios servicios físicos
Los servicios del ejemplo comparten el mismo modelo de datos porque el servicio API Web tiene los mismos datos que el servicio de búsqueda. Entonces, en la implementación física del microservicio, se está dividiendo esa funcionalidad, para poder escalar cada uno de esos servicios hacia arriba o hacia abajo según sea necesario. Tal vez el servicio API Web generalmente necesite más instancias que el servicio de búsqueda, o viceversa.
En resumen, la arquitectura lógica de microservicios no siempre tiene que coincidir con la arquitectura física de despliegue. En esta guía, cada vez que mencionamos un microservicio, nos referimos a un microservicio del negocio o lógico, que se puede asignar a uno o más servicios (físicos). En la mayoría de los casos, este será un servicio único, pero podrían ser más.
Definir los límites de un microservicio es probablemente el primer desafío que encontramos. Cada microservicio tiene que ser parte de su aplicación y cada microservicio debe ser autónomo con todos los beneficios y retos que eso implica. ¿Pero cómo se identifican esos límites?
En primer lugar, debemos enfocarnos en el modelo lógico de dominio de la aplicación y los datos relacionados. Se debe intentar identificar las islas de datos desacopladas y los diferentes contextos dentro de la aplicación. Cada contexto podría tener un lenguaje de negocio diferente (diferentes términos del negocio). Los contextos de deberían definir y administrar de forma independiente. Los términos y entidades utilizados en esos contextos podrían sonar similares, pero también podría descubrir que, en un contexto particular, un concepto de negocio se usa para un propósito diferente en otro contexto e, incluso, el mismo concepto podría tener nombres diferentes. Por ejemplo, un “Usuario” puede referirse a un usuario en el contexto de identidad o membresía, a un cliente en el contexto de CRM, a un comprador en el contexto de pedidos y así sucesivamente.
La forma para identificar los límites de múltiples contextos, con un dominio diferente para cada uno, es exactamente la forma como puede identificar los límites para cada microservicio del negocio y su modelo de dominio y datos relacionados. Siempre se intenta minimizar el acoplamiento entre esos microservicios. Más adelante en esta guía, entraremos en los detalles sobre esta identificación y diseño del modelo de dominio, en la sección Identificación de los límites del modelo de dominio para cada microservicio.
El segundo desafío es cómo implementar consultas que recuperen datos de varios microservicios, al tiempo que se evita la conversación excesiva con los microservicios desde aplicaciones cliente remotas. Un ejemplo podría ser una pantalla de una aplicación móvil que necesita mostrar información del usuario que proviene del carrito de compras, el catálogo y la identidad del usuario. Otro ejemplo sería un informe complejo que involucre muchas tablas ubicadas en múltiples microservicios. La solución correcta depende de la complejidad de las consultas. Pero, en cualquier caso, será necesaria una forma de consolidar información, para mejorar la eficiencia en las comunicaciones del sistema. Las soluciones más populares son las siguientes.
API Gateway. Para la consolidación de datos de múltiples microservicios con bases de datos diferentes, el enfoque recomendado es un microservicio de agregación denominado API Gateway. Sin embargo, se debe tener cuidado con el despliegue de este patrón, ya que puede ser un “cuello de botella” en su sistema y violar el principio de autonomía de los microservicios. Para mitigar esta posibilidad, puede tener múltiples API Gateways de grano fino, cada una de ellas centrada en un "sector" vertical o área de negocio del sistema. Más adelante se explica con mayor detalle el patrón API Gateway, en la sección sobre uso de una API Gateway.
CQRS con tablas de consulta. Otra solución para agregar datos de múltiples microservicios es el patrón Materialized View. En este enfoque, se genera de antemano, antes de que sucedan las consultas reales, una tabla de solo lectura con los datos (desnormalizados) que provienen de múltiples microservicios. La tabla tiene un formato adecuado a las necesidades de la aplicación del cliente.
Considere algo así como la pantalla de una aplicación móvil. Si tiene una única base de datos, puede juntar los datos para esa pantalla usando una consulta SQL que realiza una combinación compleja que involucra múltiples tablas. Sin embargo, cuando tiene múltiples bases de datos y cada una pertenece a un microservicio diferente, no puede consultar esas bases de datos creado un “join” SQL. Esa consulta compleja se convierte en todo un desafío. Se puede resolver ese requerimiento usando un enfoque CQRS: Se crea una tabla desnormalizada en una base de datos diferente, que se usa sólo para consultas. La tabla se puede diseñar específicamente para los datos que se necesitan para esa consulta compleja, con una relación de uno a uno entre los campos que necesita la pantalla de la aplicación y las columnas en la tabla de consulta. También podría usar esa tabla para generar informes.
Este enfoque no solo resuelve el problema original (cómo consultar y agregar información de varios microservicios); también mejora el rendimiento considerablemente en comparación con un join SQL complejo, porque ya se tienen los datos que necesita la aplicación, en la tabla de consulta. Por supuesto, el uso de Command and Query Responsibility Segregation (CQRS) con tablas de consulta / lectura significa trabajo adicional y será necesario que adoptar la consistencia eventual. Sin embargo, cuando hay requerimientos de rendimiento y alta escalabilidad en escenarios colaborativos (o escenarios competitivos, según el punto de vista) se debe aplicar CQRS con múltiples bases de datos.
"Datos fríos" en bases de datos centrales. Para informes complejos y consultas que no necesitan datos en tiempo real, se suelen exportar sus "datos calientes" (datos transaccionales de los microservicios, en tiempo real) como "datos fríos" (históricos, que no se modifican) a grandes bases de datos que se usan sólo para informes. Ese sistema de base de datos central puede ser un sistema basado en Big Data, como Hadoop, un depósito de datos basado en Azure SQL Data Warehouse o incluso una base de datos SQL única, utilizada sólo para informes (si el tamaño no es un problema).
Tenga en cuenta que esta base de datos centralizada, se usará sólo para consultas e informes que no necesitan datos en tiempo real. Las actualizaciones y transacciones originales, como su fuente original, tienen que estar en las bases de datos de los microservicios. La forma para sincronizar los datos sería mediante comunicación basada en eventos (tratada en las siguientes secciones) o mediante el uso de otras herramientas de importación/exportación de bases de datos. Si se utiliza una comunicación basada en eventos, ese proceso de integración sería similar a como se propagan los datos en el caso de las tablas de consulta CQRS.
Sin embargo, si el diseño de la aplicación implica consolidar información constantemente de múltiples microservicios para consultas complejas, podría ser un síntoma de un mal diseño: un microservicio debería estar lo más aislado posible de otros microservicios. (Excluyendo informes y análisis, que siempre deberían usar bases de datos centrales de datos fríos.) Tener frecuentemente este problema, podría ser una razón para fusionar microservicios. Se debe equilibrar la autonomía de la evolución y el despliegue de cada microservicio, con fuertes dependencias, cohesión y agregación de datos.
Como se indicó anteriormente, los datos de cada microservicio deben ser privados y sólo se debe poder acceder a ellos usando el API del microservicio. Por lo tanto, un desafío que se presenta es cómo implementar procesos de negocio que involucran a varios microservicios mientras se mantiene la consistencia entre ellos.
Para analizar este problema, veamos un ejemplo de la aplicación de referencia eShopOnContainers. El microservicio Catalog mantiene información sobre todos los productos, incluido su precio. El microservicio Basket administra los datos temporales sobre los artículos en los carritos de compra de los usuarios, que incluye los precios de cuando se agregaron al carrito. Cuando el precio de un producto se actualiza en el catálogo, ese precio también se debe actualizar en los carritos activos que lo contienen, además el sistema probablemente debería advertir al usuario que el precio de un artículo en su carrito ha cambiado desde que lo agregó.
En una hipotética versión monolítica de esta aplicación, cuando el precio cambia en la tabla Product, el subsistema de catálogo podría simplemente usar una transacción ACID para actualizar el precio actual en la tabla Basket.
Sin embargo, en una aplicación basada en microservicios, las tablas de Product y Basket son propiedad de sus microservicios respectivos. Ningún microservicio debería usar tablas o almacenamiento propiedad de otro microservicio, en sus propias transacciones, ni siquiera en consultas directas, como se muestra en la Figura 4-9.
Figura 4-9. Un microservicio no puede acceder directamente a una tabla de otro
El microservicio Catalog no debe actualizar la tabla Basket directamente, ya que la tabla Basket es propiedad del servicio Basket. Para realizar una actualización al microservicio Basket, el microservicio Catalog debe usar la consistencia eventual, basada probablemente en una comunicación asíncrona, como eventos de integración (mensajes y comunicaciones basadas en eventos). Así es como la aplicación de referencia eShopOnContainers consigue este tipo de consistencia en microservicios.
Como se establece en el teorema de CAP, se debe elegir entre la disponibilidad y la consistencia formal de ACID. La mayoría de los escenarios basados en microservicios exigen disponibilidad y alta escalabilidad en lugar de consistencia fuerte. Las aplicaciones de misión crítica deben permanecer en funcionamiento y los desarrolladores pueden buscar formas para no requerir consistencia fuerte, usando técnicas para trabajar con consistencia débil o eventual. Este es el enfoque adoptado por la mayoría de las arquitecturas basadas en microservicios.
Además, las transacciones de tipo ACID o two-phase commit (típicas en escenarios de bases de datos distribuidas) no sólo son contrarias a los principios de los microservicios, sino que la mayoría de las bases de datos NoSQL (como Azure Cosmos DB, MongoDB, etc.) no admiten transacciones two-phase commit. Sin embargo, es esencial mantener la consistencia de los datos entre los servicios y las bases de datos. Este desafío también está relacionado con la cuestión de cómo propagar los cambios en múltiples microservicios cuando ciertos datos deben ser redundantes, por ejemplo, cuando necesita tener el nombre o la descripción del producto en el microservicio Catalog y en el microservicio Basket.
Por lo tanto, y como conclusión, una buena solución para este problema es utilizar la consistencia eventual entre los microservicios, coordinados a través de una comunicación basada en eventos y un sistema de publicación y suscripción. Estos temas se tratan más adelante, en la sección Comunicación asíncrona basada en eventos.
La comunicación a través de los límites de los microservicios es un verdadero desafío. En este contexto, la comunicación no se refiere a qué protocolo usar (HTTP y REST, AMQP, mensajes, etc.). En cambio, se refiere a qué estilo de comunicación usar y, especialmente, qué tan acoplados deben ser los microservicios. Dependiendo del nivel de acoplamiento, cuando ocurre una falla, el impacto de esa falla en el sistema cambiará significativamente.
En un sistema distribuido, como una aplicación basada en microservicios, con tantas partes móviles y servicios distribuidos en muchos servidores o hosts, los componentes eventualmente fallarán. Se producirán fallas parciales e incluso interrupciones más grandes, por lo que se deben diseñar los microservicios y la comunicación entre ellos, considerando los riesgos comunes en este tipo de sistemas.
Un enfoque popular es implementar microservicios basados en HTTP (REST), debido a su simplicidad. Un enfoque basado en HTTP es perfectamente aceptable; el problema aquí está relacionado con cómo se usa. Si se utilizan peticiones HTTP y las respuestas sólo para interactuar con los microservicios desde aplicaciones cliente o desde API Gateways, eso está bien. Pero si crea largas cadenas de llamadas HTTP síncronas entre microservicios, comunicándose entre ellos como si fueran objetos en una aplicación monolítica, la aplicación finalmente tendrá problemas.
Por ejemplo, imagine que la aplicación cliente realiza una llamada al API HTTP de un microservicio, como por ejemplo el de pedidos. Si este microservicio a su vez llama a microservicios adicionales usando HTTP dentro del mismo ciclo de petición/respuesta, se está creando una cadena de llamadas HTTP. Puede sonar razonable al principio. Sin embargo, hay puntos importantes a considerar al seguir este camino:
De hecho, si los microservicios internos se están comunicando creando cadenas de peticiones HTTP como se describe, se podría argumentar que, en realidad, tiene una aplicación monolítica, pero basada en HTTP entre procesos en lugar de mecanismos de comunicación interproceso.
Por lo tanto, para cumplir el principio de autonomía de los microservicios y tener mejor capacidad de recuperación, se debe minimizar el uso de cadenas de comunicación tipo petición/respuesta entre microservicios. Se recomienda utilizar sólo la interacción asíncrona para la comunicación entre microservicios, ya sea mediante comunicación asíncrona basada en eventos y mensajes, o mediante el uso de HTTP polling (asíncrono) independiente de la petición/respuesta original.
Más adelante se explica con mayor detalles el uso de la comunicación asíncrona, en las secciones La integración asíncrona refuerza la autonomía de los microservicios y Comunicación asíncrona basada en mensajes.
http://martinfowler.com/bliki/CQRS.html
https://docs.microsoft.com/azure/architecture/patterns/materialized-view
http://www.dataversity.net/acid-vs-base-the-shifting-ph-of-database-transaction-processing/
https://docs.microsoft.com/azure/architecture/patterns/compensating-transaction
http://udidahan.com/2014/07/30/service-oriented-composition-with-video/
Cuando se identifican los límites y el tamaño del modelo para cada microservicio, el objetivo no es llegar a la separación más granular posible, aunque sí se debe tender hacia los microservicios pequeños. El objetivo debe ser llegar a la separación más significativa guiada por el conocimiento de su dominio. El énfasis no está en el tamaño, sino en las capacidades requeridas para el negocio. Por ejemplo, si para un área de la aplicación hay una cohesión evidente, basada en un alto número de dependencias, eso indica la necesidad de un microservicio único. La cohesión es una forma de identificar cómo separar o agrupar microservicios. En última instancia, mientras se adquiere mayor conocimiento sobre el dominio, se debe adaptar el tamaño de los microservicios de forma iterativa. Encontrar el tamaño correcto no es un proceso que se logra en un solo paso.
Sam Newman, un reconocido promotor de microservicios y autor del libro Building Microservices, destaca que se deben diseñar los microservicios basados en el patrón de Bounded Context (BC), como se presentó anteriormente. A veces, un BC podría estar compuesto de varios servicios físicos, pero no al revés.
Un modelo de dominio, con sus entidades específicas, tiene sentido dentro de un BC concreto o microservicio. Un BC delimita la aplicabilidad de un modelo de dominio y brinda a los miembros del equipo de desarrollo una comprensión clara y compartida de lo que debe se debe desarrollar dentro del BC o fuera, de manera independiente. Estos son, precisamente, los mismos objetivos que para los microservicios.
Otra herramienta que evidencia las decisiones de diseño es la ley de Conway, que establece que una aplicación reflejará los límites sociales de la organización que la produjo. Pero a veces sucede lo contrario: la organización de la empresa se define por el software. Es posible que sea necesario revertir la ley de Conway y definir los límites de la forma en que desea organizar la empresa, inclinándose hacia la consultoría de procesos del negocio.
Para identificar los Bounded Contexts, se puede usar el patrón DDD de Mapeo de contexto (Context Mapping). Con el Context Mapping se identifican los distintos contextos en la aplicación y sus límites. Es común tener un contexto y un límite diferente para cada subsistema pequeño, por ejemplo. El Mapa de contexto es una forma de definir y hacer explícitos esos límites entre dominios. Un BC es autónomo e incluye los detalles de un solo dominio, tales como sus entidades de dominio y define los contratos de integración con otros BC. Esto es similar a la definición de un microservicio: es autónomo, implementa cierta capacidad de dominio y debe proporcionar interfaces. Esta es la razón por la que la asignación de contexto y el patrón de Bounded Context son buenos enfoques para identificar los límites del modelo de dominio de los microservicios.
Al diseñar una aplicación grande, se suele fragmentar el modelo de dominio: por ejemplo, un experto del dominio de catálogo, nombrará las entidades de manera diferente en los dominios de catálogo e inventario, que un experto del dominio de envíos. O la entidad “Usuario” puede ser diferente en tamaño y número de atributos cuando se trata de un experto en CRM que quiere almacenar cada detalle sobre el cliente que, para un experto en el dominio de pedidos, que sólo necesita datos parciales sobre el cliente. Es muy difícil eliminar la ambigüedad de todos los términos de dominio relacionados en una aplicación grande. Pero lo más importante es no intentar unificarlos, si no, más bien, aceptar las diferencias y la riqueza proporcionadas por cada dominio. Tratar de tener una base de datos unificada para toda la aplicación y unificar el vocabulario, será incómodo y no le parecerá correcto a ninguno de los expertos de los dominios. Por lo tanto, los BC (implementados como microservicios) le ayudarán a aclarar dónde puede usar ciertos términos de dominio y dónde tendrá que dividir el sistema y crear BC adicionales con diferentes dominios.
Sabremos si se lograron unos límites y tamaños correctos de cada BC y modelo de dominio, cuando se tengan pocas relaciones fuertes entre los distintos modelos de dominio y, generalmente, no sea necesario combinar información de múltiples modelos al realizar operaciones típicas de la aplicación.
Quizás la mejor respuesta a la pregunta de qué tan grande debe ser un modelo de dominio para cada microservicio, sea la siguiente: debe tener un BC autónomo, lo más aislado posible, que permita trabajar sin tener que cambiar constantemente a otros contextos (los modelos de otros microservicios). En la figura 4-10 se puede ver cómo, cada uno de los microservicios (múltiples BC) tiene su propio modelo y cómo se pueden definir sus entidades, según los requisitos específicos de cada dominio de la aplicación.
Figura 4-10. Identificando entidades y límites de los modelos de los microservicios
En la Figura 4-10 se muestra un escenario relacionado con un sistema de gestión de conferencias en línea. Se han identificado varios BC que podrían implementarse como microservicios, según los dominios que los expertos de dominio definieron. Como se puede ver, hay entidades que están presentes en un solo modelo de microservicios, como Pagos en el microservicio Pagos. Esos serán fáciles de implementar.
Sin embargo, también podría tener entidades que tengan una forma diferente pero que compartan la misma identidad entre los múltiples modelos de dominio de los múltiples microservicios. Por ejemplo, la entidad Usuario se identifica en el microservicio de Gestión de Conferencias. Ese mismo usuario, con la misma identidad, es el que recibe el nombre de Compradores en el microservicio de pedidos, o el que recibe el nombre de Pagador en el microservicio de pago, e incluso el que se llama Cliente en el microservicio de Servicio al cliente. Esto se debe a que, dependiendo del lenguaje ubicuo que utiliza cada experto de dominio, un usuario puede tener una perspectiva diferente incluso con diferentes atributos. La entidad de usuario en el modelo de microservicios Gestión de conferencias puede tener como atributos la mayoría de los datos personales. Sin embargo, ese mismo usuario en forma de Pagador en el microservicio de Pagos o en forma de Cliente en el microservicio de Atención al Cliente podría no necesitar la misma lista de atributos.
Un enfoque similar se ilustra en la Figura 4-11.
Figura 4-11. Descomponiendo modelos de datos tradicionales en múltiples modelos de dominio
Puede ver cómo el usuario está presente en el modelo de microservicio Gestión de Conferencias como la entidad Usuario y también está presente en la forma de la entidad Comprador en el microservicio de Precios, con atributos alternativos o detalles sobre el usuario cuando en realidad es un comprador. Cada microservicio o BC podría no necesitar todos los datos relacionados con la entidad Usuario, sino sólo una parte, dependiendo del problema a resolver o del contexto. Por ejemplo, en el modelo de microservicio Precios, no necesita la dirección o la ID del usuario, sólo la identificación (como identidad) y el estado, lo que tendrá un impacto en los descuentos al fijar el precio de los asientos por comprador.
La entidad Seat tiene el mismo nombre, pero diferentes atributos en cada modelo de dominio. Sin embargo, Seat comparte identidad basada en la misma ID, como ocurre con el usuario y el comprador.
Básicamente, existe un concepto compartido de “usuario” que existe en múltiples servicios (dominios), y todos comparten la identidad de ese usuario. Pero en cada modelo de dominio puede haber detalles adicionales o diferentes sobre la entidad Usuario. Por lo tanto, es necesario que haya una forma de asignar una entidad de usuario de un dominio (microservicio) a otro.
Hay varios beneficios para no compartir la misma entidad “usuario” con el mismo número de atributos en todos los dominios. Un beneficio es reducir la duplicación, de modo que los modelos de microservicio no tengan datos que no necesiten. Otro beneficio es tener un microservicio maestro que posee un cierto tipo de datos por entidad, de modo que las actualizaciones y las consultas para ese tipo de datos sean dirigidas sólo por ese microservicio.
En una arquitectura de microservicios, cada microservicio expone un conjunto de endpoints (urls de acceso) de funciones, por lo general muy específicas. Esto puede afectar la comunicación de cliente a microservicio, como se explica en esta sección.
Un enfoque posible es usar una arquitectura de comunicación directa de cliente a microservicio. En este enfoque, una aplicación cliente puede realizar solicitudes directamente a algunos de los microservicios, como se muestra en la Figura 4-12.
Figura 4-12. Usando una arquitectura de comunicación directa de cliente-a-microservicio
En este caso, cada microservicio tiene un endpoint público, a veces con un puerto TCP diferente para cada microservicio. Un ejemplo de una URL para un servicio en particular podría ser la siguiente URL en Azure:
http://eshoponcontainers.westus.cloudapp.azure.com:88/
En un entorno de producción basado en un cluster, esa URL se correlacionaría con el balanceador de carga del cluster, que a su vez se encargaría de distribuir las peticiones entre los microservicios. En entornos de producción, se puede tener un controlador de entrega para aplicaciones (ADC – Application Delivery Controller), como el Azure Application Gateway, entre la Internet y sus microservicios. Esto actúa como una capa de transparente que no sólo se encarga del balanceo de carga, sino que también agrega seguridad a los servicios al ofrecer una conexión SSL. Esto también mejora la carga de trabajo de los hosts, ya que se delega el manejo de la conexión SSL y otras tareas de enrutamiento al Azure Application Gateway. En cualquier caso, un balanceador de carga y un ADC son transparentes, desde el punto de vista de la arquitectura lógica de aplicación.
Una arquitectura de comunicación directa de cliente a microservicio podría ser suficiente para una aplicación pequeña, especialmente si el cliente es una aplicación web, como una aplicación ASP.NET MVC. Sin embargo, cuando se crean aplicaciones grandes y complejas, por ejemplo, cuando maneja docenas de tipos de microservicios y, especialmente, cuando los clientes son aplicaciones móviles o aplicaciones web SPA, ese enfoque tiene algunos inconvenientes.
Al desarrollar una aplicación grande basada en microservicios, hay que considerar los siguientes aspectos:
La interacción con múltiples microservicios para construir una pantalla única en la interfaz de usuario, aumenta la cantidad de peticiones/respuestas de la Internet. Esto aumenta la latencia y la complejidad en el lado de la interfaz de usuario. Lo ideal es consolidar las respuestas en el lado del servidor para reducir el número de viajes, con esto reduce la latencia, ya que se regresan múltiples datos en paralelo y algunos elementos de la interfaz de usuario podrían mostrar datos tan pronto como estén listos.
Resolver los aspectos comunes como seguridad y autorización en cada microservicio puede requerir un esfuerzo de desarrollo significativo. Una posible solución es tener esos servicios dentro del host Docker o cluster interno, para restringir el acceso directo a ellos desde el exterior y para resolverlos en un lugar centralizado, como un API Gateway.
Los protocolos que se suelen utilizar en el lado del servidor (como AMQP o RPC) generalmente no son compatibles con las aplicaciones cliente. Por lo tanto, las solicitudes se deben realizar a través de protocolos como HTTP/HTTPS y luego ser traducidas a los otros protocolos. Un enfoque tipo man-in-the-middle puede ayudar en esta situación.
Las API de múltiples microservicios podrían no ser adecuadas para las necesidades de todas aplicaciones cliente. Por ejemplo, las necesidades de una aplicación móvil pueden ser diferentes a las necesidades de una aplicación web. Para las aplicaciones móviles, es posible que deba optimizar aún más para que las respuestas de datos sean más eficientes. Esto se puede hacer consolidando datos de múltiples microservicios y devolviendo un solo conjunto de datos y, algunas veces, eliminando cualquier dato en la respuesta que no sea necesario para la aplicación móvil. Además de que, por supuesto, también se puede comprimir esa información. Por eso, una fachada o API entre la aplicación móvil y los microservicios puede ser lo más adecuado para este escenario.
Cuando se diseñan y construyen aplicaciones grandes o complejas, basadas en microservicios y con múltiples aplicaciones cliente, puede ser un buen enfoque considerar es uso de una API Gateway. Este es un servicio que proporciona un punto único de entrada para ciertos grupos de microservicios. Es similar al patrón Fachada del diseño orientado a objetos, pero en este caso, es parte de un sistema distribuido. El patrón API Gateway también se conoce como el "back-end para front-end" (BFF) porque se construye pensando en las necesidades de la aplicación cliente.
La Figura 4-13 muestra cómo encaja un API Gateway en una arquitectura basada en microservicios.
Es importante resaltar que, en ese diagrama, se estaría usando un servicio de API Gateway único frente a múltiples aplicaciones de cliente. Ese hecho puede representar un riesgo importante porque la API Gateway crecerá y evolucionará según muchos requisitos diferentes de las aplicaciones del cliente. Eventualmente, estará sobrecargado por esas necesidades diferentes y podría, en efecto, ser muy similar a una aplicación o servicio monolítico. Por eso que es muy recomendable dividir la API Gateway en múltiples servicios o API Gateways más pequeñas, por ejemplo, una por tipo dispositivo.
Figura 4-13. Usando una API Gateway implementada como un servicio Web API personalizado
En este ejemplo, la API Gateway se implementaría como una API Web que se ejecuta en un contenedor.
Como se mencionó anteriormente, se deben implementar varias API Gateways para tener una fachada diferente, ajustada a las necesidades de cada aplicación cliente. Cada API Gateway puede proporcionar un API diferente diseñada para cada aplicación cliente, posiblemente en función del tipo de dispositivo, que, por debajo, llame a múltiples microservicios internos.
Para evitar que la consolidación de información en la API Gateway pueda terminar como un agregador monolítico y, entonces, violar la autonomía que buscamos con los microservicios, los API Gateway también deben segregarse en función de los límites de las áreas del negocio, para no actuar como un agregador para toda la aplicación.
Una API Gateway granular también puede ser un microservicio por sí mismo, e incluso puede tener un nombre tomado del dominio o área del negocio y sus datos relacionados. Definir los límites de las API Gateway en función de las áreas del negocio o del dominio ayudará a lograr un mejor diseño.
La granularidad en el nivel de la API Gateway puede ser especialmente útil para aplicaciones de interfaz de usuario compuestas, basadas en microservicios, ya que el concepto de una API Gateway de grano fino es similar a un servicio de composición interfaz de usuario. Esto lo veremos más adelante en Creando una interfaz de usuario compuesta basada en microservicios.
Por lo tanto, el uso de una API Gateway suele ser un buen enfoque para muchas aplicaciones grandes y medianas, pero no como un único agregador monolítico o API Gateway única y centralizada.
Otro enfoque es usar un producto como Azure API Management como se muestra en la Figura 4-14. Este enfoque no sólo resuelve las necesidades de la API Gateway, sino que también ofrece funciones como la recopilación de estadísticas de uso de las API. Tenga en cuenta que la API Gateway es sólo un componente en una solución de gestión de APIs.
Figura 4-14. Usando el Azure API Management para su API Gateway
En este caso, al usar un producto como Azure API Management, el hecho de que tenga una única puerta de enlace API no es tan arriesgado porque estos tipos de API Gateway son "más delgadas", lo que significa que no implementan código C# personalizado, que podría evolucionar hacia un componente monolítico.
Este tipo de producto actúa más como un proxy inverso para las peticiones, donde también se puede filtrar las API de los microservicios internos y aplicar las políticas de autorización a las API publicadas en esta capa única.
Las métricas disponibles de un sistema de gestión de APIs ayudan a comprender cómo se utilizan y cómo funcionan. Permiten ver informes analíticos casi en tiempo real e identificar tendencias que podrían afectar el negocio. Además, se pueden tener registros sobre la actividad de petición/respuesta para un mayor análisis, tanto en línea como histórico.
Con Azure API Management, se pueden proteger las API mediante claves, tokens o filtros de IP. Esto facilita la aplicación de cuotas y límites de velocidad flexibles y detallados, modificar la forma y el comportamiento de las API mediante políticas y mejorar el rendimiento con el almacenamiento en caché de las respuestas.
En esta guía y en la aplicación de referencia (eShopOnContainers), estamos limitando la arquitectura a una más sencilla y personalizada, para enfocarnos en contenedores simples sin usar productos PaaS como Azure API Management. Sin embargo, para aplicaciones grandes basadas en microservicios que se implementan en Azure, es altamente recomendable que revise y adopte Azure API Management como base para sus API Gateways.