¿Transacciones a través de microservicios REST?

195

Supongamos que tenemos un usuario, microservicios REST de Wallet y una puerta de enlace API que une todo. Cuando Bob se registra en nuestro sitio web, nuestra puerta de enlace API debe crear un usuario a través del microservicio de usuario y una billetera a través del microservicio de billetera.

Ahora, aquí hay algunos escenarios donde las cosas podrían salir mal:

  • La creación del usuario Bob falla: está bien, solo devolvemos un mensaje de error al Bob. Estamos usando transacciones SQL para que nadie haya visto a Bob en el sistema. Todo está bien :)

  • Se crea el usuario Bob, pero antes de que se pueda crear nuestra billetera, nuestra puerta de enlace API falla. Ahora tenemos un usuario sin billetera (datos inconsistentes).

  • Se crea el usuario Bob y a medida que estamos creando la billetera, la conexión HTTP se cae. La creación de la billetera podría haber tenido éxito o no.

¿Qué soluciones están disponibles para evitar que ocurra este tipo de inconsistencia de datos? ¿Existen patrones que permitan que las transacciones abarquen múltiples solicitudes REST? He leído la página de Wikipedia sobre confirmación en dos fases que parece tocar este tema, pero no estoy seguro de cómo aplicarlo en la práctica. Este Atomic Distributed Transactions: un documento de diseño RESTful también parece interesante, aunque aún no lo he leído.

Alternativamente, sé que REST podría no ser adecuado para este caso de uso. ¿Tal vez la forma correcta de manejar esta situación es eliminar REST por completo y usar un protocolo de comunicación diferente como un sistema de cola de mensajes? ¿O debería exigir coherencia en el código de mi aplicación (por ejemplo, al tener un trabajo en segundo plano que detecta inconsistencias y las corrige o al tener un atributo de "estado" en mi modelo de Usuario con valores de "creación", "creado", etc.)?

Olivier Lalonde
fuente
3
Enlace interesante: news.ycombinator.com/item?id=7995130
Olivier Lalonde
3
Si un usuario no tiene sentido sin una billetera, ¿por qué crear un microservicio separado para él? ¿Puede ser que algo no esté bien con la arquitectura en primer lugar? ¿Por qué necesita una puerta de enlace API genérica, por cierto? ¿Hay alguna razón específica para ello?
Vladislav Rastrusny
44
@VladislavRastrusny fue un ejemplo ficticio, pero se podría pensar en el servicio de billetera como manejado por Stripe, por ejemplo.
Olivier Lalonde
Puede usar un administrador de procesos para rastrear la transacción (patrón del administrador de procesos) o hacer que cada microservicio sepa cómo desencadenar una reversión (patrón del administrador de saga) o realizar algún tipo de confirmación en dos fases ( blog.aspiresys.com/software-product-engineering / producteering / ... )
andrew pate
@VladislavRastrusny "Si un usuario no tiene sentido sin una billetera, por qué crear un microservicio separado para él", por ejemplo, aparte del hecho de que un usuario no puede existir sin una billetera, no tiene ningún código en común. Por lo tanto, dos equipos van a desarrollar e implementar microservicios de usuario y billetera de forma independiente. ¿No es el objetivo de hacer microservicios en primer lugar?
Nik

Respuestas:

148

Lo que no tiene sentido:

  • transacciones distribuidas con servicios REST . Los servicios REST, por definición, no tienen estado, por lo que no deben ser participantes en un límite transaccional que abarque más de un servicio. Su caso de uso de registro de usuario tiene sentido, pero el diseño con microservicios REST para crear datos de usuario y billetera no es bueno.

Lo que te dará dolores de cabeza:

  • EJB con transacciones distribuidas . Es una de esas cosas que funcionan en teoría pero no en la práctica. En este momento estoy tratando de hacer que una transacción distribuida funcione para EJB remotos en instancias de JBoss EAP 6.3. Hemos estado hablando con el soporte de RedHat durante semanas, y todavía no funcionó.
  • Soluciones de compromiso de dos fases en general . Creo que el protocolo 2PC es un gran algoritmo (hace muchos años lo implementé en C con RPC). Requiere mecanismos integrales de recuperación de fallas, con reintentos, repositorio de estado, etc. Toda la complejidad está oculta dentro del marco de la transacción (ej .: JBoss Arjuna). Sin embargo, 2PC no es a prueba de fallas. Hay situaciones que la transacción simplemente no puede completar. Luego, debe identificar y corregir las inconsistencias de la base de datos manualmente. Puede tener lugar una vez en un millón de transacciones si tiene suerte, pero puede ocurrir una vez cada 100 transacciones, dependiendo de su plataforma y escenario.
  • Sagas (transacciones compensatorias) . Existe la sobrecarga de implementación de crear las operaciones de compensación y el mecanismo de coordinación para activar la compensación al final. Pero la compensación tampoco es a prueba de fallas. Todavía puede terminar con inconsistencias (= algo de dolor de cabeza).

¿Cuál es probablemente la mejor alternativa?

  • Consistencia eventual . Ni las transacciones distribuidas similares a ACID ni las transacciones compensatorias son a prueba de fallas, y ambas pueden generar inconsistencias. La consistencia eventual es a menudo mejor que la "inconsistencia ocasional". Existen diferentes soluciones de diseño, tales como:
    • Puede crear una solución más robusta utilizando comunicación asincrónica. En su caso, cuando Bob se registra, la puerta de enlace API podría enviar un mensaje a una cola de NewUser y responder de inmediato al usuario diciendo "Recibirá un correo electrónico para confirmar la creación de la cuenta". Un servicio al consumidor de cola podría procesar el mensaje, realizar los cambios en la base de datos en una sola transacción y enviar el correo electrónico a Bob para notificar la creación de la cuenta.
    • El microservicio de usuario crea el registro de usuario y un registro de cartera en la misma base de datos . En este caso, la tienda de billetera en el microservicio de usuario es una réplica de la tienda de billetera maestra solo visible para el microservicio de billetera. Hay un mecanismo de sincronización de datos que se basa en disparadores o se activa periódicamente para enviar cambios de datos (por ejemplo, nuevas billeteras) desde la réplica al maestro, y viceversa.

Pero, ¿y si necesita respuestas sincrónicas?

  • Remodelar los microservicios . Si la solución con la cola no funciona porque el consumidor del servicio necesita una respuesta inmediata, prefiero remodelar la funcionalidad de usuario y billetera para que se coloque en el mismo servicio (o al menos en la misma máquina virtual para evitar transacciones distribuidas ) Sí, está un paso más lejos de los microservicios y más cerca de un monolito, pero te ahorrará un poco de dolor de cabeza.
Paulo Merson
fuente
44
La consistencia eventual funcionó para mí. En este caso, la cola "NewUser" debe ser de alta disponibilidad y resistente.
Ram Bavireddi
@RamBavireddi ¿Kafka o RabbitMQ admiten colas resistentes?
v.oddou
@ v.oddou Sí, lo hacen.
Ram Bavireddi
2
@PauloMerson No estoy seguro de cómo diferir las transacciones de Compensación a la consistencia eventual. ¿Qué sucede si, en su consistencia eventual, la creación de la billetera falla?
balsick
2
@balsick Uno de los desafíos de la configuración de consistencia eventual es el aumento de la complejidad del diseño. A menudo se requieren verificaciones de consistencia y eventos de corrección. El diseño de la solución varía. En la respuesta, sugiero la situación en la que se crea el registro de Wallet en la base de datos al procesar un mensaje enviado a través de un intermediario de mensajes. En este caso, podríamos establecer un canal de letra muerta, es decir, si el procesamiento de ese mensaje genera un error, podemos enviar el mensaje a una cola de letra muerta y notificar al equipo responsable de "Wallet".
Paulo Merson
66

Esta es una pregunta clásica que me hicieron recientemente durante una entrevista Cómo llamar a múltiples servicios web y aún así preservar algún tipo de manejo de errores en el medio de la tarea. Hoy, en la informática de alto rendimiento, evitamos los compromisos de dos fases. Hace muchos años leí un artículo sobre lo que se llamó el "modelo de Starbuck" para las transacciones: piense en el proceso de ordenar, pagar, preparar y recibir el café que ordena en Starbuck ... Simplifico las cosas, pero un modelo de compromiso de dos fases sugiera que todo el proceso sería una única transacción de envoltura para todos los pasos involucrados hasta que reciba su café. Sin embargo, con este modelo, todos los empleados esperarían y dejarían de trabajar hasta que obtenga su café. ¿Ves la foto?

En cambio, el "modelo Starbuck" es más productivo siguiendo el modelo de "mejor esfuerzo" y compensando los errores en el proceso. Primero, ¡se aseguran de que pagues! Luego, hay colas de mensajes con su pedido adjunto a la taza. Si algo sale mal en el proceso, como si no obtuviera su café, no es lo que ordenó, etc., iniciamos el proceso de compensación y nos aseguramos de que obtenga lo que desea o le reembolse, este es el modelo más eficiente para aumentar la productividad.

A veces, Starbuck está desperdiciando un café, pero el proceso general es eficiente. Hay otros trucos para pensar cuando crea sus servicios web, como diseñarlos de una manera que se puedan llamar cualquier número de veces y aún así proporcionar el mismo resultado final. Entonces, mi recomendación es:

  • No sea demasiado bueno al definir sus servicios web (no estoy convencido de la exageración de los microservicios que está ocurriendo en estos días: demasiados riesgos de ir demasiado lejos);

  • Async aumenta el rendimiento, así que prefiera ser async, envíe notificaciones por correo electrónico siempre que sea posible.

  • Cree servicios más inteligentes para hacerlos "recuperables" cualquier cantidad de veces, procesando con un uid o taskid que seguirá el orden de abajo hacia arriba hasta el final, validando las reglas de negocio en cada paso;

  • Utilice colas de mensajes (JMS u otros) y desvíe a los procesadores de manejo de errores que aplicarán operaciones de "reversión" aplicando operaciones opuestas, por cierto, trabajar con orden asíncrono requerirá algún tipo de cola para validar el estado actual del proceso, así que considera eso;

  • En último recurso, (ya que puede no suceder con frecuencia), póngalo en una cola para el procesamiento manual de errores.

Volvamos al problema inicial que se publicó. Cree una cuenta y cree una billetera y asegúrese de que todo esté hecho.

Digamos que se llama a un servicio web para orquestar toda la operación.

El pseudo código del servicio web se vería así:

  1. Llame al microservicio de creación de cuenta, pásele alguna información y una identificación de tarea única 1.1 El microservicio de creación de cuenta primero verificará si esa cuenta ya se creó. Una identificación de tarea está asociada con el registro de la cuenta. El microservicio detecta que la cuenta no existe, por lo que la crea y almacena la identificación de la tarea. NOTA: este servicio se puede llamar 2000 veces, siempre realizará el mismo resultado. El servicio responde con un "recibo que contiene información mínima para realizar una operación de deshacer si es necesario".

  2. Llame a la creación de Wallet, dándole el ID de la cuenta y el ID de la tarea. Digamos que una condición no es válida y la creación de la billetera no se puede realizar. La llamada regresa con un error pero no se creó nada.

  3. El orquestador es informado del error. Sabe que necesita abortar la creación de la cuenta, pero no lo hará por sí mismo. Le pedirá al servicio de billetera que lo haga pasando su "recibo de deshacer mínimo" recibido al final del paso 1.

  4. El servicio de cuenta lee el recibo de deshacer y sabe cómo deshacer la operación; el recibo de deshacer puede incluso incluir información sobre otro microservicio que podría haberse denominado para hacer parte del trabajo. En esta situación, el recibo de deshacer podría contener el ID de la cuenta y posiblemente alguna información adicional requerida para realizar la operación opuesta. En nuestro caso, para simplificar las cosas, digamos simplemente eliminar la cuenta usando su ID de cuenta.

  5. Ahora, digamos que el servicio web nunca recibió el éxito o el fracaso (en este caso) de que se realizó la acción de deshacer la creación de la cuenta. Simplemente volverá a llamar al servicio de deshacer de la cuenta. Y este servicio normalmente nunca debe fallar porque su objetivo es que la cuenta ya no exista. Por lo tanto, comprueba si existe y ve que no se puede hacer nada para deshacerlo. Por lo tanto, devuelve que la operación es un éxito.

  6. El servicio web le informa al usuario que la cuenta no se pudo crear.

Este es un ejemplo sincrónico. Podríamos haberlo manejado de una manera diferente y poner el caso en una cola de mensajes dirigida a la mesa de ayuda si no queremos que el sistema recupere completamente el error ". He visto que esto se realiza en una compañía donde no hay suficiente Se podrían proporcionar ganchos al sistema de back-end para corregir situaciones. El servicio de asistencia recibió mensajes que contenían lo que se realizó con éxito y tenía suficiente información para arreglar cosas como nuestro recibo de deshacer podría usarse de una manera totalmente automática.

He realizado una búsqueda y el sitio web de Microsoft tiene una descripción de patrón para este enfoque. Se llama patrón de transacción compensatoria:

Patrón de transacción compensatoria

usuario8098437
fuente
2
¿Cree que podría ampliar esta respuesta para proporcionar consejos más específicos al OP? Tal como está, esta respuesta es algo vaga y difícil de entender. Aunque entiendo cómo se sirve el café en Starbucks, no me queda claro qué aspectos de este sistema deben emularse en los servicios REST.
jwg
He agregado un ejemplo relacionado con el caso inicialmente proporcionado en la publicación original.
user8098437
2
Acabo de agregar un enlace al patrón de transacción de compensación como lo describe Microsoft.
user8098437
3
Para mí, esta es la mejor respuesta. Tan simple
Oscar Nevarez
1
Tenga en cuenta que las transacciones de compensación pueden ser completamente imposibles en ciertos escenarios complejos (como se destaca de manera brillante en los documentos de Microsoft). En este ejemplo, imagine antes de que la creación de la billetera pueda fallar, alguien podría leer los detalles sobre la cuenta asociada haciendo una llamada GET en el servicio de la Cuenta, que idealmente no debería existir en primer lugar ya que la creación de la cuenta había fallado. Esto puede conducir a la inconsistencia de datos. Este problema de aislamiento es bien conocido en el patrón SAGAS.
Anmol Singh Jaggi
32

Todos los sistemas distribuidos tienen problemas con la consistencia transaccional. La mejor manera de hacer esto es, como dijiste, tener una confirmación en dos fases. Haga que la billetera y el usuario se creen en un estado pendiente. Después de crearlo, realice una llamada por separado para activar al usuario.

Esta última llamada debe ser repetible de forma segura (en caso de que su conexión se caiga).

Esto requerirá que la última llamada conozca ambas tablas (para que se pueda hacer en una sola transacción JDBC).

Alternativamente, es posible que desee pensar por qué está tan preocupado por un usuario sin billetera. ¿Crees que esto causará un problema? Si es así, tal vez tenerlas como llamadas de descanso separadas es una mala idea. Si un usuario no debería existir sin una billetera, entonces probablemente debería agregar la billetera al usuario (en la llamada POST original para crear el usuario).

Rob Conklin
fuente
Gracias por la sugerencia. Los servicios de usuario / billetera eran ficticios, solo para ilustrar el punto. Pero estoy de acuerdo en que debería diseñar el sistema para evitar la necesidad de transacciones tanto como sea posible.
Olivier Lalonde
77
Estoy de acuerdo con el segundo punto de vista. Parece que su microservicio, que crea un usuario, también debería crear una billetera, porque esta operación representa la unidad atómica de trabajo. Además, puede leer este eaipatterns.com/docs/IEEE_Software_Design_2PC.pdf
Sattar Imamov
2
Esta es realmente una gran idea. Undos son un dolor de cabeza. Pero crear algo en estado pendiente es mucho menos invasivo. Se han realizado comprobaciones, pero todavía no se ha creado nada definitivo. Ahora solo necesitamos activar los componentes creados. Probablemente incluso podamos hacerlo de manera no transaccional.
Timo
10

En mi humilde opinión, uno de los aspectos clave de la arquitectura de microservicios es que la transacción se limita al microservicio individual (principio de responsabilidad única).

En el ejemplo actual, la creación del usuario sería una transacción propia. La creación del usuario empujaría un evento USER_CREATED a una cola de eventos. El servicio de Wallet se suscribirá al evento USER_CREATED y realizará la creación de Wallet.

mithrandir
fuente
1
Suponiendo que deseamos evitar cualquiera y todos los 2PC, y suponiendo que el servicio de Usuario escribe en una base de datos, no podemos hacer que el mensaje sea enviado por el Usuario a una cola de eventos, lo que significa que nunca puede llegar a El servicio Wallet.
Roman Kharkovski el
@RomanKharkovski Un punto importante de hecho. Una forma de abordarlo podría ser iniciar una transacción, guardar al Usuario, publicar el evento (no es parte de la transacción) y luego confirmar la transacción. (En el peor de los casos, altamente improbable, el commit falla, y los que responden al evento no podrán encontrar al usuario.)
Timo
1
Luego almacene el evento en la base de datos y en la entidad. Tenga un trabajo programado para procesar eventos almacenados y enviarlos al intermediario de mensajes. stackoverflow.com/a/52216427/4587961
Yan Khonski
7

Si mi billetera fuera solo otro grupo de registros en la misma base de datos sql que el usuario, entonces probablemente colocaría el código de creación de usuario y billetera en el mismo servicio y lo manejaría usando las instalaciones normales de transacción de la base de datos.

Me parece que está preguntando qué sucede cuando el código de creación de billetera requiere que toque otro sistema o sistemas. Yo diría que todo depende de lo complejo y arriesgado que sea el proceso de creación.

Si solo se trata de tocar otro almacén de datos confiable (digamos uno que no puede participar en sus transacciones sql), entonces, dependiendo de los parámetros generales del sistema, podría estar dispuesto a arriesgar la posibilidad infinitamente pequeña de que no ocurra una segunda escritura. Podría no hacer nada, pero plantear una excepción y tratar los datos inconsistentes a través de una transacción compensatoria o incluso algún método ad-hoc. Como siempre les digo a mis desarrolladores: "si este tipo de cosas están sucediendo en la aplicación, no pasarán desapercibidas".

A medida que aumenta la complejidad y el riesgo de la creación de billeteras, debe tomar medidas para mejorar los riesgos involucrados. Digamos que algunos de los pasos requieren llamar a múltiples API de socios.

En este punto, puede introducir una cola de mensajes junto con la noción de usuarios y / o billeteras parcialmente construidos.

Una estrategia simple y efectiva para asegurarse de que sus entidades eventualmente se construyan correctamente es hacer que los trabajos vuelvan a intentarlo hasta que tengan éxito, pero mucho depende de los casos de uso de su aplicación.

También pensaría mucho sobre por qué tuve un paso propenso a fallas en mi proceso de aprovisionamiento.

Robert Moskal
fuente
4

Una solución simple es crear un usuario utilizando el Servicio de usuario y usar un bus de mensajería donde el servicio de usuario emite sus eventos, y el Servicio de billetera se registra en el bus de mensajería, escucha el evento creado por el usuario y crea la billetera para el usuario. Mientras tanto, si el usuario accede a la IU de Wallet para ver su Wallet, verifique si el usuario acaba de crearse y muestre que la creación de su billetera está en progreso, verifique en algún momento

techagrammer
fuente
3

¿Qué soluciones están disponibles para evitar que ocurra este tipo de inconsistencia de datos?

Tradicionalmente, se utilizan gestores de transacciones distribuidas. Hace unos años, en el mundo de Java EE, podría haber creado estos servicios como EJB que se implementaron en diferentes nodos y su puerta de enlace API habría realizado llamadas remotas a esos EJB. El servidor de aplicaciones (si está configurado correctamente) asegura automáticamente, mediante el compromiso de dos fases, que la transacción se confirma o se revierte en cada nodo, de modo que se garantiza la coherencia. Pero eso requiere que todos los servicios se implementen en el mismo tipo de servidor de aplicaciones (para que sean compatibles) y en realidad solo funcionó con los servicios implementados por una sola compañía.

¿Existen patrones que permitan que las transacciones abarquen múltiples solicitudes REST?

Para SOAP (ok, no REST), existe la especificación WS-AT pero ningún servicio que haya tenido que integrar tiene soporte para eso. Para REST, JBoss tiene algo en camino . De lo contrario, el "patrón" es encontrar un producto que pueda conectar a su arquitectura o crear su propia solución (no recomendado).

He publicado dicho producto para Java EE: https://github.com/maxant/genericconnector

Según el documento al que hace referencia, también existe el patrón Try-Cancel / Confirm y el Producto asociado de Atomikos.

Los motores BPEL manejan la consistencia entre los servicios desplegados de forma remota mediante compensación.

Alternativamente, sé que REST podría no ser adecuado para este caso de uso. ¿Tal vez la forma correcta de manejar esta situación es eliminar REST por completo y usar un protocolo de comunicación diferente como un sistema de cola de mensajes?

Hay muchas formas de "vincular" recursos no transaccionales en una transacción:

  • Como sugiere, podría usar una cola de mensajes transaccionales, pero será asíncrona, por lo que si depende de la respuesta se volverá desordenada.
  • Podría escribir el hecho de que necesita llamar a los servicios de fondo a su base de datos y luego llamar a los servicios de fondo utilizando un lote. Nuevamente, asíncrono, por lo que puede ser complicado
  • Puede usar un motor de procesos de negocio como su puerta de enlace API para organizar los microservicios de back-end.
  • Puede usar EJB remoto, como se mencionó al principio, ya que admite transacciones distribuidas listas para usar.

¿O debería exigir coherencia en el código de mi aplicación (por ejemplo, al tener un trabajo en segundo plano que detecta inconsistencias y las corrige o al tener un atributo de "estado" en mi modelo de Usuario con valores de "creación", "creado", etc.)?

El defensor de Playing Devils: ¿por qué construir algo así, cuando hay productos que hacen eso por usted (ver arriba), y probablemente lo hacen mejor que usted, porque se prueban y prueban?

Hormiga Kutschera
fuente
2

Personalmente, me gusta la idea de Micro Services, módulos definidos por los casos de uso, pero como su pregunta menciona, tienen problemas de adaptación para los negocios clásicos como bancos, seguros, telecomunicaciones, etc.

Las transacciones distribuidas, como muchos mencionaron, no son una buena opción, la gente ahora apuesta por sistemas eventualmente consistentes, pero no estoy seguro de que esto funcione para bancos, seguros, etc.

Escribí un blog sobre mi solución propuesta, puede ser que esto pueda ayudarte ...

https://mehmetsalgar.wordpress.com/2016/11/05/micro-services-fan-out-transaction-problems-and-solutions-with-spring-bootjboss-and-netflix-eureka/

posthumecaver
fuente
0

La consistencia eventual es la clave aquí.

  • Se elige uno de los servicios para convertirse en el controlador principal del evento.
  • Este servicio manejará el evento original con confirmación única.
  • El manejador primario se responsabilizará de comunicar asincrónicamente los efectos secundarios a otros servicios.
  • El controlador principal hará la orquestación de otras llamadas de servicios.

El comandante está a cargo de la transacción distribuida y toma el control. Conoce las instrucciones que se ejecutarán y coordinará su ejecución. En la mayoría de los escenarios solo habrá dos instrucciones, pero puede manejar múltiples instrucciones.

El comandante asume la responsabilidad de garantizar la ejecución de todas las instrucciones, y eso significa que se retira. Cuando el comandante intenta realizar la actualización remota y no obtiene una respuesta, no tiene que volver a intentarlo. De esta forma, el sistema se puede configurar para que sea menos propenso a fallas y se cura solo.

Como tenemos reintentos tenemos idempotencia. La idempotencia es la propiedad de poder hacer algo dos veces de tal manera que los resultados finales sean los mismos que si se hubieran hecho una sola vez. Necesitamos idempotencia en el servicio remoto o en la fuente de datos para que, en el caso de que reciba la instrucción más de una vez, solo la procese una vez.

Consistencia eventual Esto resuelve la mayoría de los desafíos de transacciones distribuidas, sin embargo, debemos considerar algunos puntos aquí. A cada transacción fallida le seguirá un reintento, la cantidad de reintentos intentados depende del contexto.

La coherencia es eventual, es decir, mientras el sistema está fuera de estado constante durante un reintento, por ejemplo, si un cliente ha pedido un libro, ha realizado un pago y luego actualiza la cantidad de existencias. Si las operaciones de actualización de stock fallan y suponiendo que fue el último stock disponible, el libro seguirá estando disponible hasta que la operación de reintento para la actualización de stock haya tenido éxito. Después de que el reintento sea exitoso, su sistema será consistente.

Viyaan Jhiingade
fuente
-2

¿Por qué no utilizar la plataforma API Management (APIM) que admite scripts / programación? Por lo tanto, podrá crear un servicio compuesto en el APIM sin molestar a los micro servicios. He diseñado el uso de APIGEE para este propósito.

sra
fuente