rest - transacciones - microservicios netflix
¿Transacciones a través de microservicios REST? (10)
¿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) garantiza 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 que en realidad solo funcionen 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 la tubería . 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 coherencia 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 utilizar 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", "creados", 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?
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 creamos 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 la 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", "creados", etc.)?
¿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 usando APIGEE para este propósito.
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.
Esta es una pregunta clásica que me hicieron recientemente durante una entrevista Cómo llamar a múltiples servicios web y aún 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 lo haría. 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 puedan llamarse 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 número 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 las colas de mensajes (JMS u otros) y desvíe a los procesadores de manejo de errores que aplicarán operaciones para "revertir" 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 pseudocó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 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 devuelve al usuario que no se pudo crear la cuenta.
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 utilizarse de una manera totalmente automatizada.
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:
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 encargará de comunicar de manera asíncrona 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 efectuar la actualización remota y no obtiene una respuesta, no tiene que volver a intentarlo. De esta manera, 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 ordenado 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.
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 las empresas clásicas como bancos, seguros, telecomunicaciones, etc.
Las transacciones distribuidas, como muchos mencionaron, no son una buena opción, la gente ahora apuesta más por sistemas 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 ...
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 (por ejemplo, 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. Es posible que no haga nada, pero planteo una excepción y trato con 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 volver a intentar los trabajos 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.
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).
Una solución simple es crear un usuario usando 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 interfaz de usuario de Wallet para ver su billetera, verifique si el usuario acaba de crearse y muestre que la creación de su billetera está en progreso, verifique en algún momento
Lo que no tiene sentido:
- transacciones distribuidas con servicios REST . Los servicios REST, por definición, no tienen estado, por lo que no deben participar en un límite transaccional que abarca 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 la implementación de la creación de 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 de compensación 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, 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.