Compartir a través de


Diseño de una aplicación orientada a microservicios

Sugerencia

Este contenido es un extracto del libro electrónico, ".NET Microservices Architecture for Containerized .NET Applications" (Arquitectura de microservicios de .NET para aplicaciones de .NET contenedorizadas), disponible en Documentación de .NET o como un PDF descargable y gratuito que se puede leer sin conexión.

Miniatura de la portada del libro electrónico 'Arquitectura de microservicios de .NET para aplicaciones .NET contenedorizadas'.

Esta sección se centra en el desarrollo de una aplicación empresarial hipotética del lado servidor.

Especificaciones de la aplicación

La aplicación hipotética controla las solicitudes mediante la ejecución de lógica de negocios, el acceso a bases de datos y, a continuación, devuelve respuestas HTML, JSON o XML. Se dirá que la aplicación debe admitir varios clientes, incluidos los exploradores de escritorio que ejecutan aplicaciones de página única (SPA), aplicaciones web tradicionales, aplicaciones web móviles y aplicaciones móviles nativas. La aplicación también puede exponer una API para que los terceros los consuman. También debe ser capaz de integrar sus microservicios o aplicaciones externas de forma asincrónica, por lo que ese enfoque ayudará a la resistencia de los microservicios en caso de errores parciales.

La aplicación constará de estos tipos de componentes:

  • Componentes de presentación. Estos componentes son responsables de controlar la interfaz de usuario y consumir servicios remotos.

  • Lógica de dominio o de negocios. Este componente es la lógica de dominio de la aplicación.

  • Lógica de acceso a la base de datos. Este componente consta de componentes de acceso a datos responsables de acceder a bases de datos (SQL o NoSQL).

  • Lógica de integración de aplicaciones. Este componente incluye un canal de mensajería basado en agentes de mensajes.

La aplicación requerirá una alta escalabilidad, al tiempo que permite que sus subsistemas verticales escalen horizontalmente de forma autónoma, ya que determinados subsistemas requerirán más escalabilidad que otras.

La aplicación debe ser capaz de implementarse en varios entornos de infraestructura (varias nubes públicas y locales) y, idealmente, debe ser multiplataforma, capaz de pasar de Linux a Windows (o viceversa) fácilmente.

Contexto del equipo de desarrollo

También se supone lo siguiente sobre el proceso de desarrollo de la aplicación:

  • Tiene varios equipos de desarrollo centrados en diferentes áreas empresariales de la aplicación.

  • Los nuevos miembros del equipo deben ser productivos rápidamente y la aplicación debe ser fácil de entender y modificar.

  • La aplicación tendrá una evolución a largo plazo y reglas de negocio cambiantes.

  • Necesita una buena capacidad de mantenimiento a largo plazo, lo que significa tener agilidad al implementar nuevos cambios en el futuro, a la vez que puede actualizar varios subsistemas con un impacto mínimo en los demás subsistemas.

  • Le interesa la integración y la implementación continuas de la aplicación.

  • Quiere aprovechar las tecnologías emergentes (marcos, lenguajes de programación, etc.) a la vez que evoluciona la aplicación. No desea realizar migraciones completas de la aplicación al pasar a nuevas tecnologías, ya que eso daría lugar a costos elevados e afectaría a la previsibilidad y la estabilidad de la aplicación.

Elección de una arquitectura

¿Cuál debe ser la arquitectura de implementación de aplicaciones? Las especificaciones de la aplicación, junto con el contexto de desarrollo, sugieren encarecidamente que debe diseñar la aplicación descomponiéndolo en subsistemas autónomos en forma de microservicios y contenedores colaboradores, donde un microservicio es un contenedor.

En este enfoque, cada servicio (contenedor) implementa un conjunto de funciones cohesivas y estrechamente relacionadas. Por ejemplo, una aplicación puede constar de servicios como el servicio de catálogo, el servicio de pedidos, el servicio de cesta, el servicio de perfil de usuario, etc.

Los microservicios se comunican mediante protocolos como HTTP (REST), pero también de forma asincrónica (por ejemplo, mediante AMQP) siempre que sea posible, especialmente cuando se propagan actualizaciones con eventos de integración.

Los microservicios se desarrollan e implementan como contenedores independientemente entre sí. Este enfoque significa que un equipo de desarrollo puede desarrollar e implementar un microservicio determinado sin afectar a otros subsistemas.

Cada microservicio tiene su propia base de datos, lo que le permite desacoplar completamente de otros microservicios. Cuando sea necesario, la coherencia entre las bases de datos de diferentes microservicios se logra mediante eventos de integración de nivel de aplicación (a través de un bus de eventos lógicos), como se controla en Segregación de responsabilidades de comandos y consultas (CQRS). Por ese motivo, las restricciones de negocio deben adoptar la coherencia final entre los múltiples microservicios y bases de datos relacionadas.

eShopOnContainers: una aplicación de referencia para .NET y microservicios implementados mediante contenedores

Para que pueda centrarse en la arquitectura y las tecnologías en lugar de pensar en un dominio empresarial hipotético que no conoce, hemos seleccionado un dominio empresarial conocido, es decir, una aplicación simplificada de comercio electrónico (e-shop) que presenta un catálogo de productos, toma pedidos de clientes, comprueba el inventario y realiza otras funciones empresariales. Este código fuente de la aplicación basada en contenedores está disponible en el repositorio de GitHub eShopOnContainers .

La aplicación consta de varios subsistemas, entre los que se incluyen varias interfaces de usuario de tienda en los front-end (una aplicación web y una aplicación móvil nativa), junto con los microservicios y contenedores de back-end necesarios para todas las operaciones del lado del servidor, con varias puertas de enlace de API como puntos de entrada consolidados a los microservicios internos. En la figura 6-1 se muestra la arquitectura de la aplicación de referencia.

Diagrama de aplicaciones cliente con eShopOnContainers en un único host de Docker.

Figura 6-1. La arquitectura de la aplicación de referencia de eShopOnContainers para el entorno de desarrollo

En el diagrama anterior se muestra que los clientes móviles y SPA se comunican con los puntos de conexión de puerta de enlace de API única y, a continuación, se comunican con los microservicios. Los clientes web tradicionales se comunican con el microservicio MVC, que se comunica con los microservicios a través de la puerta de enlace de API.

Entorno de hospedaje. En la figura 6-1, verá varios contenedores implementados dentro de un único host de Docker. Eso sería el caso al implementar en un único host de Docker con el comando docker-compose up. Sin embargo, si usa un orquestador o un clúster de contenedor, cada contenedor podría ejecutarse en un host (nodo) diferente y cualquier nodo podría ejecutar cualquier número de contenedores, como se explicó anteriormente en la sección arquitectura.

Arquitectura de comunicación. La aplicación eShopOnContainers usa dos tipos de comunicación, según el tipo de acción funcional (consultas frente a actualizaciones y transacciones):

  • Comunicación de cliente HTTP a microservicio a través de puertas de enlace de API. Este enfoque se usa para las consultas y al aceptar comandos de actualización o transaccionales de las aplicaciones cliente. El enfoque mediante puertas de enlace de API se explica con detalle en secciones posteriores.

  • Comunicación asincrónica basada en eventos. Esta comunicación se produce a través de un bus de eventos para propagar las actualizaciones entre microservicios o para integrarse con aplicaciones externas. El bus de eventos se puede implementar con cualquier tecnología de infraestructura de agente de mensajería como RabbitMQ o mediante buses de servicio de nivel superior (nivel de abstracción), como Azure Service Bus, NServiceBus, MassTransit o Brighter.

La aplicación se implementa como un conjunto de microservicios en forma de contenedores. Las aplicaciones cliente pueden comunicarse con esos microservicios que se ejecutan como contenedores a través de las direcciones URL públicas publicadas por las puertas de enlace de API.

Soberanía de datos por microservicio

En la aplicación de ejemplo, cada microservicio posee su propia base de datos o origen de datos, aunque todas las bases de datos de SQL Server se implementan como un único contenedor. Esta decisión de diseño solo se tomó para facilitar a un desarrollador obtener el código de GitHub, clonarlo y abrirlo en Visual Studio o Visual Studio Code. O bien, facilita la compilación de las imágenes personalizadas de Docker mediante la CLI de .NET y la CLI de Docker y, a continuación, implementarlas y ejecutarlas en un entorno de desarrollo de Docker. En cualquier caso, el uso de contenedores para orígenes de datos permite a los desarrolladores compilar e implementarlos en cuestión de minutos sin tener que aprovisionar una base de datos externa ni ningún otro origen de datos con dependencias difíciles en la infraestructura (nube o local).

En un entorno de producción real, para lograr alta disponibilidad y escalabilidad, las bases de datos deben basarse en servidores de bases de datos en la nube o en el entorno local, pero no en contenedores.

Por lo tanto, las unidades de implementación de microservicios (e incluso para las bases de datos de esta aplicación) son contenedores de Docker y la aplicación de referencia es una aplicación de varios contenedores que adopta principios de microservicios.

Recursos adicionales

Ventajas de una solución basada en microservicios

Una solución basada en microservicios como esta tiene muchas ventajas:

Cada microservicio es relativamente pequeño, fácil de administrar y evolucionar. Concretamente:

  • Es fácil que un desarrollador comprenda y empiece a trabajar rápidamente con una buena productividad.

  • Los contenedores se crean con rapidez, lo que permite que los desarrolladores sean más productivos.

  • Un IDE como Visual Studio puede cargar proyectos más pequeños rápidamente, lo que hace que los desarrolladores sean productivos.

  • Cada microservicio se puede diseñar, desarrollar e implementar independientemente de otros microservicios, lo que proporciona agilidad, ya que es más fácil implementar nuevas versiones de microservicios con frecuencia.

Es posible escalar horizontalmente áreas individuales de la aplicación. Por ejemplo, es posible que sea necesario escalar horizontalmente el servicio de catálogo o el de cesta de la compra, pero no el proceso de pedidos. Una infraestructura de microservicios será mucho más eficaz con respecto a los recursos utilizados al escalar horizontalmente que una arquitectura monolítica.

Puede dividir el trabajo de desarrollo entre varios equipos. Cada servicio puede ser propiedad de un único equipo de desarrollo. Cada equipo puede administrar, desarrollar, implementar y escalar su servicio independientemente del resto de los equipos.

Los problemas son más aislados. Si hay un problema en un servicio, solo ese servicio se ve afectado inicialmente (excepto cuando se usa el diseño incorrecto, con dependencias directas entre microservicios) y otros servicios pueden seguir controlando las solicitudes. En cambio, un componente que no funciona correctamente en una arquitectura de implementación monolítica puede reducir todo el sistema, especialmente cuando implica recursos, como una pérdida de memoria. Además, cuando se resuelve un problema en un microservicio, puede implementar solo el microservicio afectado sin afectar al resto de la aplicación.

Puede usar las tecnologías más recientes. Dado que puede empezar a desarrollar servicios de forma independiente y ejecutarlos en paralelo (gracias a los contenedores y .NET), puede empezar a usar las tecnologías y marcos más recientes de forma expediente en lugar de estar bloqueados en una pila o marco antiguos para toda la aplicación.

Desventajas de una solución basada en microservicios

Una solución basada en microservicios como esta también tiene algunas desventajas:

Aplicación distribuida. La distribución de la aplicación agrega complejidad a los desarrolladores cuando diseñan y compilan los servicios. Por ejemplo, los desarrolladores deben implementar la comunicación entre servicios mediante protocolos como HTTP o AMQP, lo que agrega complejidad para las pruebas y el control de excepciones. También agrega latencia al sistema.

Complejidad de la implementación. Una aplicación que tiene docenas de tipos de microservicios y necesita alta escalabilidad (debe poder crear muchas instancias por servicio y equilibrar esos servicios en muchos hosts) significa un alto grado de complejidad de implementación para las operaciones y la administración de TI. Si no usa una infraestructura orientada a microservicios (como un orquestador y un programador), esa complejidad adicional puede requerir mucho más esfuerzos de desarrollo que la propia aplicación empresarial.

Transacciones atómicas. Las transacciones atómicas entre varios microservicios normalmente no son posibles. Los requisitos de negocio deben adoptar la coherencia final entre varios microservicios. Para obtener más información, consulte los desafíos del procesamiento de mensajes idempotentes.

Se han aumentado las necesidades de recursos globales (memoria total, unidades y recursos de red para todos los servidores o hosts). En muchos casos, cuando se reemplaza una aplicación monolítica por un enfoque de microservicios, la cantidad de recursos globales iniciales necesarios para la nueva aplicación basada en microservicios será mayor que las necesidades de infraestructura de la aplicación monolítica original. Este enfoque se debe a que el mayor grado de granularidad y servicios distribuidos requiere más recursos globales. Sin embargo, dado el bajo costo de los recursos en general y la ventaja de poder escalar horizontalmente determinadas áreas de la aplicación en comparación con los costos a largo plazo al evolucionar las aplicaciones monolíticas, el aumento del uso de recursos suele ser un buen equilibrio para las aplicaciones a largo plazo.

Problemas con la comunicación directa de cliente a microservicio. Cuando la aplicación es grande, con docenas de microservicios, hay desafíos y limitaciones si la aplicación requiere comunicaciones directas de cliente a microservicio. Un problema es una posible discrepancia entre las necesidades del cliente y las API expuestas por cada uno de los microservicios. En algunos casos, es posible que la aplicación cliente tenga que realizar muchas solicitudes independientes para redactar la interfaz de usuario, lo que puede ser ineficaz a través de Internet y sería poco práctico a través de una red móvil. Por lo tanto, las solicitudes de la aplicación cliente al sistema back-end deben minimizarse.

Otro problema con las comunicaciones directas de cliente a microservicio es que algunos microservicios podrían estar usando protocolos que no son fáciles de usar web. Un servicio podría usar un protocolo binario, mientras que otro servicio podría usar la mensajería AMQP. Esos protocolos no son compatibles con cortafuegos y se utilizan mejor internamente. Normalmente, una aplicación debe usar protocolos como HTTP y WebSockets para la comunicación fuera del firewall.

Otro inconveniente con este enfoque directo de cliente a servicio es que dificulta la refactorización de los contratos de esos microservicios. Con el tiempo, es posible que los desarrolladores quieran cambiar la forma en que el sistema se divide en servicios. Por ejemplo, pueden combinar dos servicios o dividir un servicio en dos o más servicios. Sin embargo, si los clientes se comunican directamente con los servicios, realizar este tipo de refactorización puede interrumpir la compatibilidad con las aplicaciones cliente.

Como se mencionó en la sección arquitectura, al diseñar y compilar una aplicación compleja basada en microservicios, puede considerar el uso de varias puertas de enlace de API específicas en lugar del enfoque de comunicación de cliente a microservicio más sencillo.

Creación de particiones de los microservicios. Por último, independientemente del enfoque que tome para la arquitectura de microservicios, otro desafío consiste en decidir cómo crear particiones de una aplicación de un extremo a otro en varios microservicios. Como se indica en la sección de arquitectura de la guía, hay varias técnicas y enfoques que puede adoptar. Básicamente, debe identificar las áreas de la aplicación que se desacoplan de las otras áreas y que tienen un número bajo de dependencias difíciles. En muchos casos, este enfoque se alinea con los servicios de creación de particiones por caso de uso. Por ejemplo, en nuestra aplicación de tienda electrónica, tenemos un servicio de pedidos responsable de toda la lógica de negocios relacionada con el proceso de pedido. También tenemos el servicio de catálogo y el servicio de cesta que implementan otras funcionalidades. Idealmente, cada servicio debe tener solo un pequeño conjunto de responsabilidades. Este enfoque es similar al principio de responsabilidad única (SRP) aplicado a las clases, que indica que una clase solo debe tener una razón para cambiar. Pero en este caso, se trata de microservicios, por lo que el ámbito será mayor que una sola clase. La mayor parte de todo, un microservicio debe ser autónomo, de un extremo a otro, incluida la responsabilidad de sus propios orígenes de datos.

Patrones de diseño y arquitectura externos frente a internos

La arquitectura externa es la arquitectura de microservicio compuesta por varios servicios, siguiendo los principios descritos en la sección arquitectura de esta guía. Sin embargo, dependiendo de la naturaleza de cada microservicio, e independientemente de la arquitectura de microservicios de alto nivel que elija, es habitual y a veces recomendable tener diferentes arquitecturas internas, cada una basada en diferentes patrones, para diferentes microservicios. Los microservicios incluso pueden usar diferentes tecnologías y lenguajes de programación. La figura 6-2 ilustra esta diversidad.

Diagrama que compara patrones de arquitectura externos e internos.

Figura 6-2. Arquitectura y diseño externos frente a internos

Por ejemplo, en nuestro ejemplo eShopOnContainers , los microservicios de catálogo, cesta y perfil de usuario son simples (básicamente, subsistemas CRUD). Por lo tanto, su arquitectura interna y diseño son sencillos. Sin embargo, es posible que tenga otros microservicios, como el microservicio de ordenación, que es más complejo y representa reglas de negocio cambiantes con un alto grado de complejidad del dominio. En casos como estos, es posible que quiera implementar patrones más avanzados dentro de un microservicio determinado, como los definidos con enfoques de diseño controlado por dominio (DDD), como estamos haciendo en el microservicio de pedidos de eShopOnContainers . (Revisaremos estos patrones DDD en la sección posterior que explica la implementación del microservicio de pedidos de eShopOnContainers ).

Otra razón para una tecnología diferente por microservicio podría ser la naturaleza de cada microservicio. Por ejemplo, podría ser mejor usar un lenguaje de programación funcional como F#, o incluso un lenguaje como R si tiene como destino dominios de inteligencia artificial y aprendizaje automático, en lugar de un lenguaje de programación más orientado a objetos como C#.

La línea inferior es que cada microservicio puede tener una arquitectura interna diferente basada en diferentes patrones de diseño. No todos los microservicios deben implementarse mediante patrones DDD avanzados, ya que eso sería sobreinsgenierlos. De forma similar, los microservicios complejos con lógica de negocios que cambian constantemente no deben implementarse como componentes CRUD, o bien puede acabar con código de baja calidad.

El nuevo mundo: varios patrones arquitectónicos y microservicios políglota

Hay muchos patrones arquitectónicos usados por arquitectos de software y desarrolladores. A continuación se muestran algunos (mezclando estilos de arquitectura y patrones de arquitectura):

También puede compilar microservicios con muchas tecnologías y lenguajes, como ASP.NET Core Web API, NancyFx, ASP.NET Core SignalR (disponible con .NET Core 2 o posterior), F#, Node.js, Python, Java, C++, GoLang, etc.

El punto importante es que ningún patrón o estilo de arquitectura concreto, ni ninguna tecnología determinada, es adecuado para todas las situaciones. En la figura 6-3 se muestran algunos enfoques y tecnologías (aunque no en ningún orden determinado) que se podrían usar en distintos microservicios.

Diagrama que muestra 12 microservicios complejos en una arquitectura global políglota.

Figura 6-3. Patrones multiarquitectónicos y el mundo de microservicios políglotas

El patrón de arquitectura múltiple y los microservicios políglota significa que puede mezclar y combinar lenguajes y tecnologías según las necesidades de cada microservicio y pueden seguir comunicándose entre sí. Como se muestra en la figura 6-3, en aplicaciones compuestas de muchos microservicios (contextos enlazados en terminología de diseño controlado por dominio o simplemente "subsistemas" como microservicios autónomos), puede implementar cada microservicio de una manera diferente. Cada uno puede tener un patrón de arquitectura diferente y usar diferentes lenguajes y bases de datos en función de la naturaleza de la aplicación, los requisitos empresariales y las prioridades. En algunos casos, los microservicios pueden ser similares. Pero esto no suele ser el caso, ya que los requisitos y límites de contexto de cada subsistema suelen ser diferentes.

Por ejemplo, para una aplicación de mantenimiento CRUD simple, es posible que no tenga sentido diseñar e implementar patrones DDD. Pero para su dominio principal o negocio principal, es posible que tenga que aplicar patrones más avanzados para abordar la complejidad empresarial con reglas de negocio cambiantes.

Especialmente cuando se trata de aplicaciones grandes compuestas por varios subsistemas, no se debe aplicar una arquitectura de nivel superior única basada en un único patrón de arquitectura. Por ejemplo, CQRS no debe aplicarse como una arquitectura de nivel superior para una aplicación completa, pero puede ser útil para un conjunto específico de servicios.

No hay una solución mágica ni un patrón de arquitectura correcto para cada caso determinado. No puede tener "un patrón de arquitectura para gobernarlos todos". En función de las prioridades de cada microservicio, debe elegir un enfoque diferente para cada uno, como se explica en las secciones siguientes.