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.)?
fuente
Respuestas:
Lo que no tiene sentido:
Lo que te dará dolores de cabeza:
¿Cuál es probablemente la mejor alternativa?
Pero, ¿y si necesita respuestas sincrónicas?
fuente
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í:
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".
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.
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.
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.
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.
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
fuente
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).
fuente
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.
fuente
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.
fuente
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
fuente
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.
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.
Hay muchas formas de "vincular" recursos no transaccionales en una transacción:
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?
fuente
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/
fuente
La consistencia eventual es la clave aquí.
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.
fuente
¿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.
fuente