Microservicios en contenedores

Nota:

Este libro se publicó en la primavera de 2017 y no se ha actualizado desde entonces. Mucho contenido del libro sigue siendo valioso, pero algunos de los materiales están obsoletos.

El desarrollo de aplicaciones cliente-servidor ha dado lugar a un enfoque en la creación de aplicaciones en capas que usan tecnologías específicas en cada capa. Estas aplicaciones se conocen a menudo como monolíticas y se empaquetan en hardware preescalado para cargas máximas. Los principales inconvenientes de este enfoque de desarrollo son el acoplamiento estricto entre los componentes de cada capa, que los componentes individuales no se pueden escalar fácilmente y el costo de las pruebas. Una actualización sencilla puede tener efectos imprevistos en el resto de la capa, por lo que un cambio en un componente de la aplicación requiere volver a probar e implementar toda la capa.

En la era de la nube, es especialmente preocupante que los componentes individuales no se puedan escalar fácilmente. Una aplicación monolítica contiene funcionalidad específica del dominio y suele estar dividida en capas funcionales, como front-end, lógica de negocios y almacenamiento de datos. Una aplicación monolítica se escala mediante la clonación de toda la aplicación en varias máquinas, como se muestra en la figura 8-1.

Monolithic application scaling approach

Figura 8-1: Enfoque de escalado de aplicaciones monolíticas

Microservicios

Los microservicios ofrecen un enfoque diferente para el desarrollo y la implementación de aplicaciones, un enfoque adecuado para los requisitos de agilidad, escala y confiabilidad de las aplicaciones en la nube modernas. Una aplicación de microservicios se descompone en componentes independientes que funcionan conjuntamente para ofrecer la funcionalidad general de la aplicación. El término microservicio hace hincapié en que las aplicaciones deben estar compuestas de servicios lo suficientemente pequeños como para reflejar puntos concretos que son motivo de preocupación, por lo que cada microservicio implementa una sola función. Además, cada microservicio tiene contratos bien definidos para que otros microservicios puedan comunicarse y compartir datos con él. Entre los ejemplos típicos de microservicios se incluyen los carros de la compra, el procesamiento de inventarios, los subsistemas de compra y el procesamiento de pagos.

Los microservicios se pueden escalar horizontalmente de forma independiente en comparación con las aplicaciones monolíticas gigantes que se escalan enteras. Esto significa que un área funcional específica que requiere más capacidad de procesamiento o ancho de banda de red para atender la demanda se puede escalar en lugar de escalar horizontalmente otras áreas de la aplicación innecesariamente. En la figura 8-2 se muestra este enfoque, donde los microservicios se implementan y escalan de forma independiente y crean instancias de servicios entre máquinas.

Diagram shows two apps with tiles representing different functional areas and six rectangles hosting various functional areas from both apps.

Figura 8-2: Enfoque de escalado de aplicaciones de microservicios

El escalado horizontal de microservicios puede ser casi instantáneo, lo que permite a una aplicación adaptarse a las cargas cambiantes. Por ejemplo, un único microservicio para la funcionalidad orientada a la web de una aplicación puede ser el único microservicio de la aplicación que sea necesario escalar horizontalmente para controlar el tráfico entrante adicional.

El modelo clásico para la escalabilidad de las aplicaciones es tener una capa sin estado y con equilibrio de carga con un almacén de datos externo compartido para almacenar datos persistentes. Los microservicios con estado administran sus propios datos persistentes, normalmente los almacenan localmente en los servidores en los que se colocan, para evitar la sobrecarga en el acceso a la red y la complejidad en las operaciones entre servicios. Esto permite el procesamiento más rápido posible de los datos y puede eliminar la necesidad de sistemas de almacenamiento en caché. Además, los microservicios con estado escalables suelen particionar los datos entre sus instancias, con el fin de administrar el tamaño de los datos y transferir el procesamiento más allá de lo que un único servidor puede admitir.

Los microservicios también admiten actualizaciones independientes. Este acoplamiento ligero entre microservicios proporciona una evolución rápida y confiable de las aplicaciones. Su naturaleza distribuida e independiente facilita la implementación gradual de actualizaciones, donde solo un subconjunto de instancias de un único microservicio se actualiza en un momento dado. Por tanto, si se detecta un problema, se puede revertir una actualización errónea antes de que todas las instancias se actualicen con el código o la configuración defectuosos. De forma similar, los microservicios suelen usar el control de versiones de esquema, de modo que los clientes ven una versión coherente cuando se aplican las actualizaciones, independientemente de la instancia del microservicio con la que se comuniquen.

Por tanto, las aplicaciones de microservicios tienen muchas ventajas con comparación con las aplicaciones monolíticas:

  • Cada microservicio es relativamente pequeño, fácil de administrar y desarrollar.
  • Cada microservicio se puede implementar y actualizar independientemente de otros servicios.
  • Cada microservicio se puede escalar horizontalmente de forma independiente. Por ejemplo, es posible que un servicio de catálogo o un servicio de cesta de la compra deba escalarse horizontalmente más que un servicio de pedidos. Por tanto, la infraestructura resultante consumirá los recursos de una forma más eficaz al escalar horizontalmente la capacidad.
  • Cada microservicio aísla cualquier problema. Por ejemplo, si hay un problema en un servicio, solo afecta a ese servicio. Los demás servicios pueden seguir atendiendo solicitudes.
  • Cada microservicio puede usar las tecnologías más recientes. Dado que los microservicios son autónomos y se ejecutan en paralelo, se pueden usar las tecnologías y los marcos más recientes, en lugar de estar obligado a usar un marco anterior que podría usar una aplicación monolítica.

No obstante, una solución basada en microservicios también tiene posibles inconvenientes:

  • Elegir cómo particionar una aplicación en microservicios puede resultar complicado, ya que cada microservicio debe ser completamente autónomo y completo, incluida la responsabilidad de sus orígenes de datos.
  • Los desarrolladores deben implementar comunicación entre los servicios, lo que agrega complejidad y latencia a la aplicación.
  • Normalmente, no se pueden realizar transacciones atómicas entre varios microservicios. Por tanto, los requisitos empresariales deben adoptar la coherencia final entre los microservicios.
  • En producción, la implementación y administración de un sistema compuesto por muchos servicios independientes supone una complejidad operativa.
  • La comunicación directa de cliente a microservicio puede dificultar la refactorización de los contratos de los microservicios. Por ejemplo, con el tiempo es posible que deba cambiarse la forma en la que la aplicación está particionada en servicios. Un único servicio puede dividirse en dos o más servicios y dos servicios pueden combinarse. Cuando los clientes se comunican directamente con microservicios, este trabajo de refactorización puede interrumpir la compatibilidad con las aplicaciones cliente.

Inclusión en contenedores

La contenedorización es un enfoque para el desarrollo de software en el que una aplicación y su conjunto de dependencias con control de versiones, además de su configuración de entorno extraída en archivos de manifiesto de implementación, se empaquetan juntos como una imagen de contenedor, se prueban como una unidad y se implementan en un sistema operativo host.

Un contenedor es un entorno operativo aislado, controlado por recursos y portable, donde una aplicación se puede ejecutar sin tocar los recursos de otros contenedores ni el host. Un contenedor es y actúa como una máquina virtual o un equipo físico recién instalado.

Hay muchas similitudes entre los contenedores y las máquinas virtuales, como se muestra en la figura 8-3.

Diagram shows a comparison between Virtual Machines and Containers, where virtual machines have three apps each siloed on a guest O S, with a hypervisor and a host O S, and the containers have three apps hosted in a container engine on a single OS.

Figura 8-3: Comparación entre máquinas virtuales y contenedores

Un contenedor ejecuta un sistema operativo, tiene un sistema de archivos y se puede acceder a él a través de una red, al igual que una máquina física o virtual. Sin embargo, la tecnología y los conceptos que usan los contenedores son muy diferentes de los de las máquinas virtuales. Las máquinas virtuales incluyen las aplicaciones, las dependencias necesarias y un sistema operativo invitado completo. Los contenedores incluyen la aplicación y sus dependencias, pero comparten el sistema operativo con otros contenedores, que se ejecutan como procesos aislados en el sistema operativo host (excepto los contenedores Hyper-V, que se ejecutan dentro de una máquina virtual especial por contenedor). Por tanto, los contenedores comparten recursos y normalmente requieren menos recursos que las máquinas virtuales.

La ventaja de un enfoque de desarrollo e implementación orientado a contenedores es que elimina la mayoría de los problemas que surgen por configuraciones de entorno incoherentes. Además, los contenedores permiten una funcionalidad rápida de escalado vertical de las aplicaciones mediante la creación de nuevos contenedores según sea necesario.

Los conceptos clave para crear y trabajar con contenedores son:

  • Host de contenedor: la máquina física o virtual configurada para hospedar contenedores. El host ejecutará uno o varios contenedores.
  • Imagen de contenedor: una imagen consta de una unión de sistemas de archivos por capas apilados y es la base de un contenedor. Una imagen no tiene estado y nunca cambia a medida que se implementa en entornos diferentes.
  • Contenedor: un contenedor es una instancia de una imagen en tiempo de ejecución.
  • Imagen del sistema operativo de contenedor: los contenedores se implementan a partir de imágenes. La imagen del sistema operativo de contenedor es la primera de (quizá) muchas capas de imagen que componen un contenedor. Un sistema operativo de contenedor es inmutable y no se puede modificar.
  • Repositorio de contenedor: cada vez que se crea una imagen de contenedor, esta y sus dependencias se almacenan en un repositorio local. Estas imágenes se pueden reutilizar muchas veces en el host de contenedor. Las imágenes de contenedor también pueden almacenarse en un registro público o privado, como Docker Hub, de forma que se puedan usar en varios hosts de contenedor diferentes.

Las empresas adoptan cada vez más contenedores cuando implementan aplicaciones basadas en microservicios y Docker se ha convertido en la implementación de contenedor estándar que la mayoría de las plataformas de software y proveedores de nube han adoptado.

La aplicación de referencia eShopOnContainers usa Docker para hospedar cuatro microservicios de back-end contenedorizados, como se muestra en la figura 8-4.

eShopOnContainers reference application back-end microservices

Figure 8-4: Microservicios de back-end de la aplicación de referencia eShopOnContainers

La arquitectura de los servicios de back-end de la aplicación de referencia se descompone en varios subsistemas autónomos en forma de microservicios y contenedores que colaboran. Cada microservicio proporciona una único área de funcionalidad: un servicio de identidad, un servicio de catálogo, un servicio de pedidos y un servicio de cesta.

Cada microservicio tiene su propia base de datos, lo que permite separarlo totalmente de otros microservicios. Cuando es necesario, la coherencia entre las bases de datos de diferentes microservicios se logra mediante eventos de nivel de aplicación. Para obtener más información, consulte Comunicación entre microservicios.

Para obtener más información sobre la arquitectura de microservicios, consulte Microservicios de .NET: arquitectura de aplicaciones de .NET contenedorizadas.

Comunicación entre cliente y microservicios

La aplicación móvil eShopOnContainers se comunica con los microservicios de back-end en contenedores por medio de la comunicación directa de cliente a microservicio, como se muestra en la figura 8-5.

Diagram shows an app hosted on a mobile device connected to three Backend Microservices, each with its own Web A P I Container.

Figura 8-5: Comunicación directa de cliente a microservicio

Con la comunicación directa de cliente a microservicio, la aplicación móvil realiza solicitudes a cada microservicio directamente a través de su punto de conexión público, con un puerto TCP diferente por microservicio. En producción, el punto de conexión se suele asignar al equilibrador de carga del microservicio, que distribuye las solicitudes entre las instancias disponibles.

Sugerencia

Considere la posibilidad de usar la comunicación de puerta de enlace de API. La comunicación directa de cliente a microservicio puede tener inconvenientes cuando se crea una aplicación basada en microservicios grande y compleja, pero está más que indicada para una aplicación pequeña. Considere la posibilidad de usar la comunicación de puerta de enlace de API cuando diseñe una aplicación basada en microservicios de gran tamaño con decenas de microservicios. Para obtener más información, consulte Microservicios de .NET: arquitectura de aplicaciones de .NET contenedorizadas.

Comunicación entre microservicios

Una aplicación basada en microservicios es un sistema distribuido, que se puede ejecutar en varias máquinas. Lo habitual es que cada instancia de servicio sea un proceso. Por tanto, los servicios deben interactuar usando un protocolo de comunicación entre procesos, como HTTP, TCP y Advanced Message Queuing Protocol (AMQP), o protocolos binarios, en función de la naturaleza de cada servicio.

Los dos enfoques comunes para la comunicación entre microservicios son la comunicación REST basada en HTTP para consultar datos y la mensajería asincrónica ligera para comunicar actualizaciones en varios microservicios.

La comunicación controlada por eventos basada en la mensajería asincrónica es fundamental para propagar los cambios en varios microservicios. Con este enfoque, un microservicio publica un evento cuando sucede algo importante, por ejemplo, cuando actualiza una entidad empresarial. Otros microservicios se suscriben a estos eventos. Así, cuando un microservicio recibe un evento, actualiza sus propias entidades empresariales, lo que a su vez puede suponer que se publiquen más eventos. Esta funcionalidad de publicación-suscripción se suele lograr con un bus de eventos.

Un bus de eventos permite la comunicación de tipo suscripción-publicación entre microservicios, sin necesidad de que los componentes se reconozcan entre sí, como se muestra en la figura 8-6.

Publish-subscribe with an event bus

Figura 8-6: Publicación-suscripción con un bus de eventos

Desde la perspectiva de una aplicación, el bus de eventos es simplemente un canal de publicación-suscripción expuesto a través de una interfaz. Sin embargo, la forma en la que se implementa el bus de eventos puede variar. Por ejemplo, la implementación de un bus de eventos podría usar RabbitMQ, Azure Service Bus u otros buses de servicio, como NServiceBus y MassTransit. En la figura 8-7 se muestra cómo se usa un bus de eventos en la aplicación de referencia eShopOnContainers.

Asynchronous event-driven communication in the reference application

Figura 8-7: Comunicación asincrónica controlada por eventos en la aplicación de referencia

El bus de eventos de eShopOnContainers, implementado con RabbitMQ, proporciona funcionalidad de publicación-suscripción de uno a varios. Esto significa que, después de publicar un evento, puede haber varios suscriptores escuchando el mismo evento. La figura 8-9 ilustra esta relación.

One-to-many communication

Figura 8-9: Comunicación uno a varios

Este enfoque de comunicación de uno a varios usa eventos para implementar transacciones empresariales que abarcan varios servicios, lo que garantiza la coherencia final entre los servicios. Una transacción coherente consta de una serie de pasos distribuidos. Por tanto, cuando el microservicio de perfil de usuario recibe el comando UpdateUser, actualiza los detalles del usuario en su base de datos y publica el evento UserUpdated en el bus de eventos. Tanto el microservicio de cesta como el microservicio de pedidos se han suscrito para recibir este evento y, en respuesta, actualizan la información del comprador en sus respectivas bases de datos.

Nota:

El bus de eventos de eShopOnContainers, implementado con RabbitMQ, está pensado para usarse solo como prueba de concepto. Para los sistemas de producción, se deben tener en cuenta implementaciones alternativas de bus de eventos.

Para obtener más información sobre la implementación del bus de eventos, consulte Microservicios de .NET: arquitectura de aplicaciones de .NET contenedorizadas.

Resumen

Los microservicios ofrecen un enfoque para el desarrollo y la implementación de aplicaciones que es adecuado para los requisitos de agilidad, escala y confiabilidad de las aplicaciones en la nube modernas. Una de las principales ventajas de los microservicios es que se pueden escalar horizontalmente de forma independiente, lo que significa que se puede escalar un área funcional específica que requiere más capacidad de procesamiento o ancho de banda de red para atender la demanda sin escalar innecesariamente áreas de la aplicación que no experimentan una mayor demanda.

Un contenedor es un entorno operativo aislado, controlado por recursos y portable, donde una aplicación se puede ejecutar sin tocar los recursos de otros contenedores ni el host. Las empresas adoptan cada vez más contenedores cuando implementan aplicaciones basadas en microservicios y Docker se ha convertido en la implementación de contenedor estándar que la mayoría de las plataformas de software y proveedores de nube han adoptado.