Patrón Choreography

Haga que cada componente del sistema participe en el proceso de toma de decisiones sobre el flujo de trabajo de una transacción empresarial, en lugar de depender de un punto central de control.

Contexto y problema

En la arquitectura de microservicios, se suele dar el caso de que una aplicación basada en la nube se divide en varios servicios pequeños que trabajan conjuntamente para procesar una transacción empresarial de un extremo a otro. Para reducir el acoplamiento entre servicios, cada servicio es responsable de una única operación empresarial. Algunas ventajas incluyen el desarrollo más rápido, la base de código más reducida y la escalabilidad. Sin embargo, el diseño de un flujo de trabajo eficaz y escalable es un desafío y a menudo requiere una comunicación entre servicios compleja.

Los servicios se comunican entre sí mediante API bien definidas. Incluso una única operación empresarial puede dar lugar a varias llamadas punto a punto entre todos los servicios. Un patrón común para la comunicación es usar un servicio centralizado que actúe como el orquestador. Confirma todas las solicitudes entrantes y delega las operaciones en los servicios respectivos. Al hacerlo, también administra el flujo de trabajo de toda la transacción empresarial. Cada servicio completa una operación y no es consciente del flujo de trabajo global.

El patrón de orquestador reduce la comunicación punto a punto entre los servicios, pero tiene algunos inconvenientes debido al estrecho acoplamiento entre el orquestador y otros servicios que participan en el procesamiento de la transacción empresarial. Para ejecutar tareas en una secuencia, el orquestador necesita tener conocimientos de dominio sobre las responsabilidades de esos servicios. Si quiere agregar o quitar servicios, la lógica existente se interrumpirá y deberá reorganizar partes de la ruta de comunicación. Aunque puede configurar el flujo de trabajo y agregar o quitar servicios fácilmente con un orquestador bien diseñado, este tipo de implementación es complejo y difícil de mantener.

Procesamiento de una solicitud con un orquestador central

Solución

Permita a cada servicio decidir cuándo y cómo se procesa una operación empresarial, en lugar de depender de un orquestador central.

Una manera de implementar organización es usar el patrón de mensajería asincrónica para coordinar las operaciones empresariales.

Procesamiento de una solicitud mediante un organizador

Una solicitud de cliente publica mensajes en una cola de mensajes. A medida que llegan los mensajes, se insertan en los suscriptores o servicios interesados en dicho mensaje. Cada servicio suscrito realiza su operación como se indica en el mensaje y responde a la cola de mensajes con éxito o error de la operación. En caso de éxito, el servicio puede volver a insertar un mensaje en la misma cola o en una diferente para que otro servicio pueda continuar con el flujo de trabajo si es necesario. Si se produce un error en una operación, el bus de mensajes puede reintentar la operación.

De esta forma, los servicios organizan el flujo de trabajo entre sí sin depender de un orquestador ni tener comunicación directa entre ellos.

Dado que no hay comunicación punto a punto, este patrón ayuda a reducir el acoplamiento entre los servicios. Además, puede quitar el cuello de botella de rendimiento causado por el orquestador cuando tiene que tratar todas las transacciones.

Cuándo usar este patrón

Use el patrón de organización si pretende actualizar, quitar o agregar servicios nuevos con frecuencia. Se puede modificar toda la aplicación con menos esfuerzo y con una interrupción mínima en los servicios existentes.

Tenga en cuenta este patrón si experimenta cuellos de botella de rendimiento en el orquestador central.

Este patrón es un modelo natural para la arquitectura sin servidor donde todos los servicios pueden ser de corta duración o basados en eventos. Los servicios se pueden poner en marcha debido a un evento, realizan su tarea y se quitan cuando esta finaliza.

Problemas y consideraciones

La descentralización del orquestador puede producir problemas mientras administra el flujo de trabajo.

Si un servicio no puede completar una operación empresarial, puede ser difícil recuperarse de ese error. Se puede hacer que el servicio indique el error mediante la activación de un evento. Otro servicio se suscribe a esos eventos con errores y toma las medidas necesarias, como aplicar transacciones de compensación para deshacer las operaciones correctas en una solicitud. El servicio con errores también podría no desencadenar un evento para el error. En ese caso, considere la posibilidad de usar un mecanismo de reintento o de tiempo de espera para reconocer esa operación como un error. Para obtener un ejemplo, vea la sección Ejemplo.

Es fácil implementar un flujo de trabajo cuando se quieren procesar operaciones comerciales independientes en paralelo. Puede usar un solo bus de mensajes. Sin embargo, el flujo de trabajo puede ser complicado cuando la organización debe aparecer en una secuencia. Por ejemplo, el servicio C puede iniciar su operación solo después de que el servicio A y el servicio B hayan completado sus operaciones correctamente. Un enfoque consiste en tener varios buses de mensajes que reciban mensajes en el orden requerido. Para obtener más información, vea la sección Ejemplo.

El patrón de organización se convierte en un desafío si el número de servicios aumenta rápidamente. Dado el gran número de elementos móviles independientes, el flujo de trabajo entre servicios tiende a ser complejo. Además, el seguimiento distribuido se vuelve difícil.

El orquestador administra de forma centralizada la resistencia del flujo de trabajo y puede convertirse en un único punto de error. Por otro lado, para la organización, el rol se distribuye entre todos los servicios y la resistencia se vuelve menos estable.

Cada servicio no es solo responsable de la resistencia de su funcionamiento, sino también del flujo de trabajo. Esta responsabilidad puede ser pesada para el servicio y difícil de implementar. Cada servicio debe reintentar los errores transitorios, no transitorios y de tiempo de espera, de modo que la solicitud finalice correctamente, si es necesario. Además, el servicio debe ser diligente con respecto a la comunicación del éxito o fracaso de la operación para que otros servicios puedan actuar en consecuencia.

Ejemplo

En este ejemplo se muestra el patrón organización con la aplicación de entrega con drones. Cuando un cliente solicita una recogida, la aplicación asigna un dron e informa al cliente.

Logotipo de GitHub Un ejemplo de este patrón está disponible en GitHub.

Primer plano de una descripción de mapa generada automáticamente

Una sola transacción empresarial de cliente requiere tres operaciones empresariales distintas: la creación o actualización de un paquete, la asignación de un dron para entregar el paquete y la comprobación del estado de entrega. Estas operaciones las realizan tres microservicios: Servicios de empaquetado, programador de drones y entrega. En lugar de un orquestador central, los servicios usan la mensajería para colaborar y coordinar la solicitud entre ellos.

Diseño

La transacción empresarial se procesa en una secuencia a través de varios saltos. Cada salto tiene un bus de mensajes y el servicio de negocio correspondiente.

Cuando un cliente envía una solicitud de entrega a través de un punto de conexión HTTP, el servicio de ingesta la recibe, genera un evento de operación y la envía a un bus de mensajes. El bus invoca el servicio de negocio suscrito y envía el evento en una solicitud POST. Al recibir el evento, el servicio de negocio puede completar la operación correcta o incorrectamente o la solicitud puede agotar el tiempo de espera. Si se realiza correctamente, el servicio responde al bus con el código de estado correcto, genera un nuevo evento de operación y lo envía al bus de mensajes del próximo salto. En caso de que se produzca un error o se agote el tiempo de espera, el servicio informa de un error mediante el envío del código BadRequest al bus de mensajes que envió la solicitud POST original. El bus de mensajes vuelve a intentar la operación en función de una directiva de reintentos. Una vez transcurrido ese período, el bus de mensajes marca la operación con errores y se detiene el procesamiento de toda la solicitud.

Este flujo de trabajo continúa hasta que se haya procesado toda la solicitud.

El diseño utiliza varios buses de mensajes para procesar toda la transacción empresarial. Microsoft Azure Event Grid proporciona el servicio de mensajería. La aplicación se implementa en un clúster de Azure Kubernetes Service (AKS) con dos contenedores en el mismo pod. Un contenedor ejecuta el embajador que interactúa con Event Grid, mientras el otro ejecuta un servicio de negocio. El enfoque con dos contenedores en el mismo pod mejora el rendimiento y la escalabilidad. El embajador y el servicio de negocio comparten la misma red, lo que permite una baja latencia y un alto rendimiento.

Para evitar las operaciones de reintento en cascada que pueden dar lugar a varios esfuerzos, solo Event Grid reintenta las operaciones, en lugar del servicio de negocio. Marca una solicitud con error mediante el envío de un mensaje a una cola de mensajes fallidos.

Los servicios de negocio son idempotentes para asegurarse de que las operaciones de reintento no producen recursos duplicados. Por ejemplo, el servicio de empaquetado utiliza operaciones upsert para agregar datos al almacén de datos.

En el ejemplo se implementa una solución personalizada para poner en correlación las llamadas entre todos los servicios y los saltos de Event Grid.

Este es un ejemplo de código que muestra el patrón Choreography entre todos los servicios de negocio. Muestra el flujo de trabajo de las transacciones de la aplicación de entrega con drones. Para abreviar, se ha quitado el código del control de excepciones y el registro.

[HttpPost]
[Route("/api/[controller]/operation")]
[ProducesResponseType(typeof(void), 200)]
[ProducesResponseType(typeof(void), 400)]
[ProducesResponseType(typeof(void), 500)]

public async Task<IActionResult> Post([FromBody] EventGridEvent[] events)
{

   if (events == null)
   {
       return BadRequest("No Event for Choreography");
   }

   foreach(var e in events)
   {

        List<EventGridEvent> listEvents = new List<EventGridEvent>();
        e.Topic = eventRepository.GetTopic();
        e.EventTime = DateTime.Now;
        switch (e.EventType)
        {
            case Operations.ChoreographyOperation.ScheduleDelivery:
            {
                var packageGen = await packageServiceCaller.UpsertPackageAsync(delivery.PackageInfo).ConfigureAwait(false);
                if (packageGen is null)
                {
                    //BadRequest allows the event to be reprocessed by Event Grid
                    return BadRequest("Package creation failed.");
                }

                //we set the event type to the next choreography step
                e.EventType = Operations.ChoreographyOperation.CreatePackage;
                listEvents.Add(e);
                await eventRepository.SendEventAsync(listEvents);
                return Ok("Created Package Completed");
            }
            case Operations.ChoreographyOperation.CreatePackage:
            {
                var droneId = await droneSchedulerServiceCaller.GetDroneIdAsync(delivery).ConfigureAwait(false);
                if (droneId is null)
                {
                    //BadRequest allows the event to be reprocessed by Event Grid
                    return BadRequest("could not get a drone id");
                }
                e.Subject = droneId;
                e.EventType = Operations.ChoreographyOperation.GetDrone;
                listEvents.Add(e);
                await eventRepository.SendEventAsync(listEvents);
                return Ok("Drone Completed");
            }
            case Operations.ChoreographyOperation.GetDrone:
            {
                var deliverySchedule = await deliveryServiceCaller.ScheduleDeliveryAsync(delivery, e.Subject);
                return Ok("Delivery Completed");
            }
            return BadRequest();
    }
}

Tenga en cuenta estos patrones en el diseño de la organización.