Patrones de datos nativos en la nube

Sugerencia

Este contenido es un extracto del libro electrónico “Architecting Cloud Native .NET Applications for Azure” (Diseño de la arquitectura de aplicaciones .NET nativas en la nube para Azure), disponible en Documentos de .NET o como un PDF descargable y gratuito que se puede leer sin conexión.

Cloud Native .NET apps for Azure eBook cover thumbnail.

Como hemos visto en este libro, un enfoque nativo de la nube cambia la forma de diseñar, implementar y administrar aplicaciones. También cambia la forma de administrar y almacenar datos.

La figura 5-1 contrasta las diferencias.

Data storage in cloud-native applications

Figura 5-1. Administración de datos en aplicaciones nativas de la nube

Los desarrolladores experimentados reconocerán fácilmente la arquitectura en el lado izquierdo de la figura 5-1. En esta aplicación monolítica, los componentes de servicio de negocio se unen en un nivel de servicios compartidos y comparten datos de una única base de datos relacional.

En muchos sentidos, una base de datos única hace que la administración de datos sea simple. La consulta de datos en varias tablas es sencilla. Los cambios en los datos se actualizan juntos o se revierten todos. Las transacciones ACID garantizan una coherencia sólida e inmediata.

Al diseñar para nativos de la nube, adoptamos un enfoque diferente. En el lado derecho de la figura 5-1, observe cómo la funcionalidad de negocio se segrega en microservicios pequeños e independientes. Cada microservicio encapsula una funcionalidad de negocio específica y sus propios datos. La base de datos monolítica se descompone en un modelo de datos distribuido con muchas bases de datos más pequeñas, cada una de ellas alineada con un microservicio. Cuando se calman las aguas, emergemos con un diseño que expone una base de datos por microservicio.

Base de datos por microservicio, ¿por qué?

Esta base de datos por microservicio proporciona muchas ventajas, especialmente para los sistemas que deben evolucionar rápidamente y admitir una escala masiva. Con este modelo:

  • Los datos de dominio se encapsulan dentro del servicio
  • El esquema de datos puede evolucionar sin afectar directamente a otros servicios
  • Cada almacén de datos se puede escalar de forma independiente
  • Un error del almacén de datos en un servicio no afectará directamente a otros servicios

La segregación de datos también permite que cada microservicio implemente el tipo de almacén de datos que mejor se optimiza para su carga de trabajo, necesidades de almacenamiento y patrones de lectura y escritura. Las opciones incluyen almacenes de datos relacionales, de documentos, de clave-valor e incluso basados en grafos.

En la figura 5-2 se presenta el principio de persistencia políglota en un sistema nativo de la nube.

Polyglot data persistence

Figura 5-2. Persistencia de datos políglota

Observe en la ilustración anterior cómo cada microservicio admite un tipo diferente de almacén de datos.

  • El microservicio del catálogo de productos consume una base de datos relacional para dar cabida a la rica estructura relacional de sus datos subyacentes.
  • El microservicio del carro de la compra consume una memoria caché distribuida que admite su sencillo almacén de datos de clave-valor.
  • El microservicio de pedidos consume una base de datos de documentos NoSql para operaciones de escritura junto con un almacén de claves y valores muy desnormalizado para dar cabida a grandes volúmenes de operaciones de lectura.

Aunque las bases de datos relacionales siguen siendo relevantes para microservicios con datos complejos, las bases de datos NoSQL han adquirido una popularidad considerable. Proporcionan una escala masiva y alta disponibilidad. Su naturaleza sin esquema permite a los desarrolladores salir de una arquitectura de clases de datos con tipo y ORM que hacen que el cambio sea costoso y lento. Más adelante en este capítulo tratamos las bases de datos NoSQL.

Aunque encapsular datos en microservicios independientes puede aumentar la agilidad, el rendimiento y la escalabilidad, también presenta muchos desafíos. En la siguiente sección analizamos estos desafíos junto con patrones y prácticas para ayudar a superarlos.

Consultas entre servicios

Aunque los microservicios son independientes y se centran en funcionalidades específicas, como inventario, envío o pedidos, con frecuencia requieren una integración con otros microservicios. A menudo, la integración implica que un microservicio consulte a otro por los datos. En la figura 5-3 se muestra el escenario.

Querying across microservices

Figura 5-3. Consulta entre microservicios

En la ilustración anterior vemos un microservicio de cesta de la compra que agrega un elemento a la cesta de la compra de un usuario. Aunque el almacén de datos de este microservicio contiene datos de cestas y artículos de línea, no mantiene datos de productos ni precios. En su lugar, esos elementos de datos son propiedad de los microservicios de catálogo y precios. Este aspecto presenta un problema. ¿Cómo puede el microservicio de la cesta de la compra agregar un producto a la cesta de la compra del usuario cuando no tiene datos de productos ni precios en su base de datos?

Una opción que se describe en el capítulo 4 es una llamada HTTP directa desde la cesta de la compra a los microservicios de catálogo y precios. Sin embargo, en el capítulo 4 dijimos que las llamadas HTTP sincrónicas acoplan microservicios, lo que reduce su autonomía y disminuye sus ventajas arquitectónicas.

También podríamos implementar un patrón de solicitud-respuesta con colas entrantes y salientes independientes para cada servicio. Sin embargo, este patrón es complicado y requiere una canalización para correlacionar los mensajes de solicitud y respuesta. Aunque desacopla las llamadas de microservicio de back-end, el servicio de llamada todavía debe esperar sincrónicamente a que se complete la llamada. Congestión de la red, errores transitorios o un microservicio sobrecargado pueden dar lugar a operaciones de ejecución larga e incluso fallidas.

En su lugar, un patrón ampliamente aceptado para quitar dependencias entre servicios es el patrón de vista materializada, que se muestra en la figura 5-4.

Materialized view pattern

Figura 5-4. Patrón de vista materializada

Con este patrón colocará una tabla de datos local (conocida como modelo de lectura) en el servicio de cesta de la compra. Esta tabla contiene una copia desnormalizada de los datos necesarios de los microservicios de producto y precio. Copiar los datos directamente en el microservicio de la cesta de la compra elimina la necesidad de costosas llamadas entre servicios. Con los datos locales para el servicio mejora el tiempo de respuesta y la fiabilidad del servicio. Además, tener su propia copia de los datos hace que el servicio de cesta de la compra sea más resistente. Si el servicio de catálogo no estuviera disponible, no afectaría directamente al servicio de la cesta de la compra. La cesta de la compra puede seguir funcionando con los datos de su propio almacén.

El truco de este enfoque es que ahora tiene datos duplicados en el sistema. Sin embargo, la duplicación estratégica de datos en sistemas nativos en la nube es una práctica establecida y no se considera un antipatrón o una mala práctica. Tenga en cuenta que uno y solo un servicio puede poseer un conjunto de datos y tener autoridad sobre él. Deberá sincronizar los modelos de lectura cuando se actualice el sistema de registro. La sincronización normalmente se implementa a través de mensajería asincrónica con un patrón de publicación/suscripción, como se muestra en la figura 5.4.

Distributed transactions

Si la consulta de datos entre microservicios es difícil, la implementación de una transacción entre varios microservicios es aún más compleja. El desafío inherente de mantener la coherencia de los datos entre orígenes de datos independientes en distintos microservicios no se puede menospreciar. La falta de transacciones distribuidas en aplicaciones nativas de la nube significa que debe administrar transacciones distribuidas mediante programación. Se mueve de un mundo de coherencia inmediata al de coherencia final.

En la figura 5-5 se muestra el problema.

Transaction in saga pattern

Figura 5-5. Implementación de una transacción entre microservicios

En la ilustración anterior, cinco microservicios independientes participan en una transacción distribuida que crea un pedido. Cada microservicio mantiene su propio almacén de datos e implementa una transacción local para su almacén. Para crear el pedido, debe realizarse correctamente la transacción local de cada microservicio individual, o bientodas deben anularse y revertir la operación. Aunque la compatibilidad transaccional integrada está disponible dentro de cada uno de los microservicios, no hay compatibilidad con una transacción distribuida que abarcaría los cinco servicios para mantener la coherencia de los datos.

En su lugar, debe construir esta transacción distribuida mediante programación.

Un patrón popular para agregar compatibilidad transaccional distribuida es el patrón Saga. Se implementa mediante la agrupación de transacciones locales mediante programación y secuencialmente invocando cada una de ellas. Si se produce un error en alguna de las transacciones locales, el Saga anula la operación e invoca un conjunto de transacciones de compensación. Las transacciones de compensación deshacen los cambios realizados por las transacciones locales anteriores y restaura la coherencia de los datos. En la figura 5-6 se muestra una transacción fallida con el patrón Saga.

Roll back in saga pattern

Figura 5-6. Revertir una transacción

En la ilustración anterior, se produjo un error en la operación Actualizar inventario en el microservicio Inventario. El Saga invoca un conjunto de transacciones de compensación (en rojo) para ajustar los recuentos de inventario, cancelar el pago y el pedido y devolver los datos de cada microservicio a un estado coherente.

Los patrones de Saga se suelen coreografiar como una serie de eventos relacionados o se orquestan como un conjunto de comandos relacionados. En el capítulo 4 analizamos el patrón del agregador de servicios que sería la base para una implementación orquestada de Saga. También hemos analizado los eventos junto con Azure Service Bus y Azure Event Grid, que serían la base para una implementación coreográfica de Saga.

Datos de gran volumen

Las aplicaciones nativas de la nube de gran tamaño a menudo admiten requisitos de datos de gran volumen. En estos escenarios, las técnicas de almacenamiento de datos tradicionales pueden provocar cuellos de botella. En el caso de los sistemas complejos que se implementan a gran escala, tanto la segregación de responsabilidades de comandos y consultas (CQRS) como Event Sourcing pueden mejorar el rendimiento de la aplicación.

CQRS

CQRS es un patrón arquitectónico que puede ayudar a maximizar el rendimiento, la escalabilidad y la seguridad. El patrón separa las operaciones que leen datos de las operaciones que escriben datos.

En escenarios normales se usa el mismo modelo de entidad y el mismo objeto de repositorio de datos para las operaciones de lectura y escritura.

Sin embargo, un escenario de datos de gran volumen puede beneficiarse de modelos y tablas de datos independientes para lecturas y escrituras. Para mejorar el rendimiento, la operación de lectura podría consultar con una representación muy desnormalizada de los datos para evitar costosas y repetitivas combinaciones de tablas y bloqueos de tablas. La operación de escritura, conocida como un comando, se actualizaría con una representación totalmente normalizada de los datos que garantizarían la coherencia. A continuación, debe implementar un mecanismo para mantener sincronizadas ambas representaciones. Normalmente, cada vez que se modifica la tabla de escritura, publica un evento que replica la modificación en la tabla de lectura.

En la figura 5-7 se muestra una implementación del patrón CQRS.

Command and Query Responsibility Segregation

Figura 5-7. Implementación de CQRS

En la ilustración anterior se implementan modelos de consulta y comandos independientes. Cada operación de escritura de datos se guarda en el almacén de escritura y, a continuación, se propaga al almacén de lectura. Preste mucha atención a cómo funciona el proceso de propagación de datos bajo el principio de coherencia final. El modelo de lectura se sincroniza finalmente con el modelo de escritura, pero puede haber algún retraso en el proceso. Tratamos la coherencia final en la sección siguiente.

Esta separación permite que las lecturas y escrituras se escalen de forma independiente. Las operaciones de lectura usan un esquema optimizado para consultas, mientras que las escrituras usan un esquema optimizado para actualizaciones. Las consultas de lectura se aplican a datos desnormalizados, mientras que se puede aplicar una lógica de negocio compleja al modelo de escritura. Además, podría imponer una seguridad más estricta en las operaciones de escritura que las que exponen las lecturas.

La implementación de CQRS puede mejorar el rendimiento de las aplicaciones para los servicios nativos de la nube. Sin embargo, resulta en un diseño más complejo. Aplique este principio de forma cuidadosa y estratégica a las secciones de la aplicación nativa de la nube que se beneficiarán de ella. Para más información sobre CQRS, consulte el libro de Microsoft ".NET Microservices: Architecture for Containerized .NET Applications" (Microservicios de .NET: Arquitectura para aplicaciones .NET contenedorizadas).

Aprovisionamiento de eventos

Otro enfoque para optimizar escenarios de datos de gran volumen implica Event Sourcing.

Normalmente, un sistema almacena el estado actual de una entidad de datos. Si un usuario cambia su número de teléfono, por ejemplo, el registro del cliente se actualiza con el nuevo número. Siempre se conoce el estado actual de una entidad de datos, pero cada actualización sobrescribe el estado anterior.

En la mayoría de los casos, este modelo funciona bien. Sin embargo, en sistemas de gran volumen, la sobrecarga del bloqueo transaccional y las frecuentes operaciones de actualización pueden afectar al rendimiento de la base de datos, la capacidad de respuesta y limitar la escalabilidad.

Event Sourcing adopta un enfoque diferente para capturar datos. Cada operación que afecta a los datos se conserva en un almacén de eventos. En lugar de actualizar el estado de un registro de datos, anexamos cada cambio a una lista secuencial de eventos pasados, similar al libro de contabilidad de un contable. El Almacén de eventos se convierte en el sistema de registro de los datos. Se usa para propagar varias vistas materializadas dentro del contexto delimitado de un microservicio. En la figura 5.8 se muestra el patrón.

Event Sourcing

Figura 5-8. Aprovisionamiento de eventos

En la ilustración anterior, observe cómo se anexa cada entrada (en azul) del carro de la compra de un usuario a un almacén de eventos subyacente. En la vista materializada adyacente, el sistema proyecta el estado actual reproduciendo todos los eventos asociados a cada carro de la compra. Esta vista, o modelo de lectura, se vuelve a exponer de nuevo a la interfaz de usuario. Los eventos también se pueden integrar con aplicaciones y sistemas externos o consultarse para determinar el estado actual de una entidad. Con este enfoque, mantiene el historial. No solo conoce el estado actual de una entidad, sino también cómo alcanzó este estado.

En términos mecánicos, el aprovisionamiento de eventos simplifica el modelo de escritura. No hay actualizaciones ni eliminaciones. Anexar cada entrada de datos como un evento inmutable minimiza los conflictos de contención, bloqueo y simultaneidad asociados a bases de datos relacionales. La creación de modelos de lectura con el patrón de vista materializada permite desacoplar la vista del modelo de escritura y elegir el mejor almacén de datos para optimizar las necesidades de la interfaz de usuario de la aplicación.

Para este patrón, tome en consideración un almacén de datos que admita directamente el aprovisionamiento de eventos. Azure Cosmos DB, MongoDB, Cassandra, CouchDB y RavenDB son buenos candidatos.

Al igual que con todos los patrones y tecnologías, implemente estratégicamente y cuando sea necesario. Aunque el aprovisionamiento de eventos puede proporcionar un mayor rendimiento y escalabilidad, tiene una curva de aprendizaje compleja.