Las transacciones son una parte esencial de las aplicaciones. Sin ellas, sería imposible mantener la coherencia de los datos.
Uno de los tipos de transacciones más potentes es el llamado Two-Phase Commit, que se resume cuando el commit de una primera transacción depende de la finalización de una segunda. Es útil sobre todo cuando hay que actualizar varias entidades al mismo tiempo, como confirmar un pedido y actualizar las existencias a la vez.
Sin embargo, cuando realizas orquestación de microservicios, por ejemplo, las cosas se complican. Cada servicio es un sistema aparte con su propia base de datos, y ya no puedes aprovechar la simplicidad de los two-phase-commits locales para mantener la consistencia de todo tu sistema.
Cuando se pierde esta capacidad, RDBMS se convierte en una opción bastante mala para el almacenamiento, ya que se podría lograr la misma "transacción atómica de entidad única" pero docenas de veces más rápido con sólo usar un Base de datos NoSQL como Couchbase. Por eso, la mayoría de las empresas que trabajan con microservicios también utilizan NoSQL.
Para ejemplificar este problema, considere la siguiente arquitectura de Microservicios de alto nivel de un sistema de comercio electrónico:
En el ejemplo anterior, no se puede hacer un pedido, cobrar al cliente, actualizar el stock y enviarlo a reparto todo en una única transacción ACID. Para ejecutar todo este flujo de forma coherente, sería necesario crear una transacción distribuida.
Todos sabemos lo difícil que es implementar cualquier cosa distribuida, y las transacciones, por desgracia, no son una excepción. Tratar con estados transitorios, consistencia eventual entre servicios, aislamientos y reversiones son escenarios que deben ser considerados durante la fase de diseño.
Afortunadamente ya se nos ocurrieron algunos buenos patrones para ello, ya que llevamos más de 20 años implementando transacciones distribuidas. El que me gustaría hablar hoy se llama patrón Saga.
El patrón SAGA
A patrón de la saga es una secuencia de transacciones locales en la que cada transacción actualiza datos dentro de un único servicio. La primera transacción de una saga se inicia mediante una solicitud externa correspondiente a la operación del sistema y, a continuación, cada paso posterior se desencadena por la finalización del anterior.
Uno de los patrones más conocidos para las transacciones distribuidas se llama Saga. El primer artículo al respecto se publicó en 1987 y las sagas han sido un solución desde entonces.
Utilizando nuestro ejemplo anterior de comercio electrónico, en un diseño de muy alto nivel una implementación del patrón saga tendría el siguiente aspecto:
Hay un par de maneras diferentes de implementar una transacción saga, pero los dos más populares son:
- Eventos/Coreografía: Cuando no existe una coordinación central, cada servicio produce y escucha los eventos de otros servicios y decide si se debe emprender una acción o no.
- Mando/Orquestación: cuando un servicio coordinador se encarga de centralizar la toma de decisiones de la saga y secuenciar la lógica empresarial.
Profundicemos un poco más en cada aplicación para entender cómo funcionan las sagas.
Eventos/Coreografía
En el enfoque Eventos/Choreography, el primer servicio ejecuta una transacción y luego publica un evento. Este evento es escuchado por uno o varios servicios que ejecutan transacciones locales y publican (o no) nuevos eventos.
La transacción distribuida finaliza cuando el último servicio ejecuta su transacción local y no publica ningún evento o el evento publicado no es escuchado por ninguno de los participantes de la saga.
Veamos cómo sería el patrón de la saga en nuestro ejemplo de comercio electrónico:
- Servicio de pedidos guarda un nuevo pedido, establece el estado como pendiente y publicar un evento llamado EVENTO_CREADO_PEDIDO.
- En Servicio de pago escucha EVENTO_CREADO_PEDIDOcobrar al cliente y publicar el evento EVENTO_PEDIDO_FACTURADO.
- En Servicio de existencias escucha EVENTO_PEDIDO_FACTURADOactualizar el stock, preparar los productos comprados en el pedido y publicar EVENTO_PEDIDO_PREPARADO.
- Servicio de entrega escucha EVENTO_PEDIDO_PREPARADO y luego recoge y entrega el producto. Al final, publica un PEDIDO_ENTREGADO_EVENTO
- Por fin, Servicio de pedidos escucha PEDIDO_ENTREGADO_EVENTO y establecer el estado de la orden como concluida.
En el caso anterior, si se necesita hacer un seguimiento del estado del pedido, Order Service podría simplemente escuchar todos los eventos y actualizar su estado.
Retrocesos en transacciones distribuidas
Revertir una transacción distribuida no sale gratis. Normalmente hay que implementar otra transacción compensatoria por lo que se ha hecho antes.
Supongamos que Stock Service ha fallado durante una transacción. Veamos cómo sería la reversión:
- Servicio de existencias produce EVENTO_FALTA_DE_EXISTENCIAS_DE_PRODUCTOS;
- Ambos Servicio de pedidos y Servicio de pagose escuchar el mensaje anterior:
- PServicio de pago reembolsar al cliente
- Servicio de pedidos establecer el estado del pedido como fallido
Tenga en cuenta que es crucial definir un ID compartido común para cada transacción, de modo que cada vez que lance un evento, todos los oyentes puedan saber de inmediato a qué transacción se refiere.
Ventajas e inconvenientes de utilizar el patrón de diseño Evento/Coreografía de Saga
Events/Choreography es una forma natural de implementar un patrón de orquestación Saga. Es simple, fácil de entender, no requiere mucho esfuerzo para construir, y todo los participantes están poco acoplados, ya que no tienen conocimiento directo unos de otros. Si su transacción implica de 2 a 4 pasos, puede ser una muy buena opción.
Sin embargo, este enfoque puede volverse confuso rápidamente si sigues añadiendo pasos adicionales en tu transacción, ya que es difícil rastrear qué servicios escuchan qué eventos. Además, también podría añadir una dependencia cíclica entre servicios ya que tienen que suscribirse unos a otros eventos.
Por último, las pruebas serían difíciles de implementar utilizando este diseño, para simular el patrón de transacción debe tener todos los servicios en ejecución.
En la próxima entradaexplicaré cómo solucionar la mayoría de los problemas de la Saga de Enfoque de eventos/coreografía utilizando otra implementación de Saga llamada Mando/Orquestación.
Mientras tanto, si tienes alguna pregunta sobre el Saga Design Pattern sobre Microservicios, la arquitectura Saga, o aplicaciones Saga no dudes en preguntarme en @deniswsrosa
Dos preguntas acerca de cómo esto podría funcionar mejor con Couchbase.
En primer lugar, para cualquier almacenamiento que no tenga transacciones ACID multi-documento... Hasta donde yo sé, el patrón saga asume que cada llamada de servicio corresponde a una única transacción de base de datos que puede hacer commit o roll back. ¿Pero qué pasa si tengo una llamada a un servicio que escribe múltiples documentos en Couchbase? ¿Implica esto modelar cada escritura de documento como un evento distinto?
En segundo lugar, específicamente para couchbase, ¿qué pasa si también estamos utilizando couchbase lite? Un cliente móvil podría realizar una sincronización descendente en medio de una transacción saga, que luego podría retroceder, y el cliente probablemente no participe en el flujo de eventos. Supongo que podemos asumir que el cliente eventualmente se pondrá al día con el estado del servidor couchbase y tendrá que manejar cualquier conflicto que surja. Sin embargo, ¿qué pasa con la sincronización ascendente?
[...] el patrón saga es una secuencia de transacciones locales donde cada transacción actualiza datos dentro de un único servicio. [...]