Patrón de consumidor idempotente

Diseñe consumidores de mensajes para que el procesamiento del mismo mensaje más de una vez tenga los mismos efectos que procesarlo una vez. Los sistemas de mensajería que garantizan la entrega al menos una vez pueden entregar el mismo mensaje varias veces. La resistencia contra duplicados garantiza que el reprocesamiento de un mensaje no cree registros duplicados, cargue doblemente un cliente o tenga otros efectos no deseados.

Contexto y problema

Las aplicaciones distribuidas suelen intercambiar trabajo a través de un agente de mensajes en lugar de usar llamadas sincrónicas directas. La mayoría de los agentes, incluidos Azure Service Bus, Azure Event Hubs, Apache Kafka y RabbitMQ, proporcionan una entrega al menos una vez. Esta garantía garantiza que un mensaje llegue a un consumidor incluso cuando se produzcan errores, pero también significa que el agente puede entregar el mismo mensaje más de una vez.

No es práctico garantizar la entrega exactamente una vez en un sistema distribuido. Incluso los agentes que usan semántica de exactamente una vez solo pueden garantizar las operaciones que controlan directamente, como entregar mensajes a los consumidores o volver a escribir datos en el agente. No pueden controlar los efectos secundarios que los consumidores implementan en sistemas externos. La solución duradera no es eliminar la entrega duplicada, pero para que el consumidor lo procese correctamente. Al combinar al menos una entrega una vez con un consumidor que omite los duplicados, se logra un procesamiento de una vez de forma eficaz .

Los duplicados pueden surgir de varios orígenes:

  • Reintentos del productor. Un productor envía un mensaje, no recibe una confirmación debido a un error de red transitorio o tiempo de espera y envía el mensaje de nuevo. El agente ahora tiene dos copias, aunque el envío se realizó correctamente a la primera.

  • Reentrega tras la falta de acuse de recibo. Un consumidor recibe y procesa un mensaje, pero no confirma su recepción porque el consumidor se bloquea, el bloqueo caduca o el acuse de recibo se pierde. El agente supone que el mensaje no se procesó y lo entrega de nuevo.

  • Fallos del consumidor durante el procesamiento. Un consumidor completa una escritura en la base de datos, pero falla antes de confirmar la recepción del mensaje. Otra instancia recupera el mensaje y vuelve a realizar la escritura.

Solución

Cree un consumidor idempotente haciendo que este mantenga un registro de los mensajes que procesa correctamente y omita aquellos que ya ha procesado, basándose en un identificador estable que sobrevive a la reentrega. El consumidor comprueba el almacén de identificadores persistente para determinar si ya ha procesado ese identificador y, a continuación, procesa el mensaje o lo descarta como duplicado.

El flujo principal consta de los pasos siguientes:

  1. Lea el mensaje y extraiga su clave de desduplicación.
  2. Compruebe el almacén de desduplicación para esa clave.
  3. Si la clave ya existe en el almacén, trate el mensaje como duplicado. Confirme el mensaje y deje de procesarlo, devolviendo opcionalmente el resultado registrado anteriormente.
  4. Si la clave aún no existe en el almacén, procese el mensaje y registre la clave en una sola operación atómica y confirme el mensaje.

En las secciones siguientes se proporcionan instrucciones para hacer que los consumidores sean idempotentes:

Elección de una clave de desduplicación estable

La clave de desduplicación debe identificar de forma única y coherente el mensaje lógico en cada entrega. Utilice un identificador de mensaje asignado por el productor o una clave de idempotencia de nivel de negocio que identifique la operación lógica específica, no un contexto de correlación compartido que varios mensajes pueden llevar.

Por ejemplo, establezca la propiedad Service Bus MessageId en un valor que identifique de forma única el mensaje lógico. No use CorrelationId como clave, ya que hace referencia a grupos de mensajes relacionados, como una solicitud y sus respuestas. Para los eventos que siguen la especificación CloudEvents, la combinación de atributos source y id identifica de forma única un evento y permanece estable en las reentregas.

No se base en identificadores de nivel de transporte que el intermediario regenera al volver a entregar el mensaje ni en valores derivados de los intentos de entrega. Esos valores cambian entre entregas y derrotan la detección de duplicados. Evite también derivar la clave de campos volátiles, como las marcas de tiempo de recepción.

Cuando más de un consumidor independiente procesa el mismo canal, como varios suscriptores en un diseño de publicación y suscripción, cada consumidor recibe su propia copia de un mensaje y necesita realizar un seguimiento independiente de la finalización del procesamiento. Si los consumidores comparten un almacén de desduplicación, use como clave para los registros una clave compuesta por la identidad del consumidor y la identidad del mensaje. Un almacén basado únicamente en la identidad del mensaje permitiría que el primer consumidor impidiera que todos los demás lo procesaran.

Decidir dónde almacenar las claves procesadas

Las siguientes opciones de almacenamiento son comunes para las claves procesadas:

  • Una tabla de desduplicación dedicada. El consumidor mantiene una tabla independiente, a veces denominada bandeja de entrada, que contiene una fila por clave procesada. Este enfoque mantiene los problemas de desduplicación separados de los datos empresariales y funciona bien si muchos tipos de mensajes comparten el mismo mecanismo.

  • La propia entidad empresarial. El consumidor almacena la clave en el registro que el mensaje crea o actualiza. Este enfoque evita una tabla independiente, pero vincula la desduplicación al tipo de datos de negocio.

Confirmar atómicamente la clave procesada y los efectos colaterales

Un flujo de comprobación y posterior procesamiento tiene una ventana de fallo. Si un consumidor procesa un mensaje y, a continuación, registra la clave en un paso independiente, un fallo entre las dos operaciones deja aplicados los efectos secundarios pero la clave sin registrar, por lo que el consumidor vuelve a procesar la siguiente reentrega.

Evite esta ventana de error escribiendo el marcador de desduplicación y los efectos secundarios empresariales en la misma transacción. Si un consumidor confirma ambas operaciones a la vez, o ninguna, cuando el mensaje se vuelve a entregar o bien encuentra el marcador y lo omite, o bien no encuentra ningún marcador porque la transacción no se completó y reprocesa el mensaje de forma segura. Esta variante transaccional es el patrón Bandeja de entrada y es la contraparte en el lado del consumidor del patrón Bandeja de salida transaccional del productor.

Protección contra duplicados simultáneos

Con una entrega de al menos una vez con consumidores en competencia concurrentes, dos instancias pueden recibir copias del mismo mensaje simultáneamente. Ambas instancias pueden superar la comprobación de existencia antes de que ninguna de las dos confirme la transacción, por lo que la comprobación por sí sola no impide el procesamiento duplicado.

Aplique la corrección en el almacén de datos en lugar de en la lógica de la aplicación mediante los pasos siguientes:

  • Use una restricción de unicidad en la clave de desduplicación de modo que dos transacciones puedan intentar insertar una clave, pero solo una puede realizarse correctamente. La otra transacción produce un error en la restricción y trata el mensaje como duplicado. Este enfoque hace que la base de datos sea el único árbitro del conflicto.

  • Evite las condiciones de carrera de tipo check-then-set en las cachés. Un patrón que comprueba una clave y luego la asigna en dos operaciones separadas deja una ventana que permite que los reintentos concurrentes se apropien de la clave. Utilice una escritura condicional atómica, como una inserción que falle en caso de conflicto o una operación de establecer si no existe, para hacer que la adquisición de la clave sea un único paso atómico.

Gestionar los efectos secundarios que no pueden integrarse en la transacción

Algunos procesos no pueden participar en la transacción de base de datos del consumidor, como llamar a una API de terceros o escribir en un almacén externo. Use el siguiente enfoque en dos fases para estos procesos:

  1. Registre la clave con un estado en curso y, a continuación, realice la acción externa.
  2. Actualice el registro a completado y almacene el resultado.

Cuando se vuelve a entregar, un registro marcado como completado indica al consumidor que omita repetir la llamada. Un registro en proceso indica que un intento anterior podría haberse completado parcialmente o que otro consumidor lo está procesando. El consumidor debe conciliar los registros desactualizados o redirigir los casos no resueltos para su intervención antes de reconocer la reentrega.

Problemas y consideraciones

Tenga en cuenta los siguientes puntos a medida que decida cómo implementar este patrón:

  • Prefiere operaciones naturalmente idempotentes. Algunas operaciones son intrínsecamente idempotentes y no necesitan contabilidad de desduplicación. Una operación de inserción o actualización basada en un identificador empresarial, una escritura que establece un valor absoluto en lugar de un incremento, o una solicitud HTTP PUT a un identificador de recurso producen los mismos resultados si se ejecutan una vez o muchas veces.

    A veces, se puede hacer que una operación sea idempotente de manera natural mediante la transferencia de estado en eventos. El mensaje contiene el estado absoluto resultante, como el nuevo estado de un pedido, por lo que el consumidor lo aplica como una operación de inserción o actualización en lugar de un cambio relativo.

    Tip

    Diseñe con idempotencia natural siempre que sea posible y use técnicas de desduplicación solo para las operaciones que no pueden ser idempotentes de forma natural.

  • Use un marco de mensajería en lugar de configurar la desduplicación. La implementación correcta del almacenamiento de desduplicación, la confirmación y la limpieza son propensas a errores. Los marcos basados en mensajes proporcionan este patrón como una característica integrada.

    Por ejemplo, NServiceBus desduplica los mensajes entrantes por sus identificadores de mensaje y proporciona retención y limpieza configurables para los datos de desduplicación. La bandeja de salida del consumidor MassTransit realiza un seguimiento de los mensajes recibidos por sus identificadores de mensajes para proporcionar un comportamiento de consumidor exactamente una vez.

  • Administrar el ciclo de vida de los registros de desduplicación. Los registros de desduplicación se acumulan a menos que los configure para que caduquen. Conserve cada registro al menos siempre que el agente pueda volver a entregar el mensaje original. El tamaño de esta ventana depende del número máximo de intentos de entrega del bróker, del tiempo de expiración del bloqueo o de visibilidad, y del tiempo de vida del mensaje.

    Establezca un tiempo de vida para los registros de desduplicación que supere esta ventana, de modo que una reentrega tardía aún encuentre su marcador correspondiente. La eliminación de registros demasiado pronto vuelve a abrir la ventana para los duplicados. Tenga en cuenta los mensajes que los operadores reenvían desde las colas de mensajes fallidos, ya que estos reenvíos pueden producirse mucho después de que se cierre la ventana normal de nueva entrega.

  • No sustituya la desduplicación del bróker por la lógica del consumidor idempotente. Algunas plataformas filtran duplicados en la capa de transporte. Por ejemplo, la detección de duplicados de Service Bus descarta los mensajes que repiten un MessageId dentro de un intervalo de tiempo configurado, lo que suprime los reintentos duplicados de envío del productor.

    Esta característica funciona en el lado de envío y dentro de una ventana limitada, por lo que no impide que un consumidor procese el mismo mensaje dos veces después de una entrega. Aún necesita lógica idempotente del consumidor. Use las características de la plataforma para reducir el volumen duplicado, no como reemplazo del patrón de consumidor idempotente.

  • Ten en cuenta el orden de los mensajes. La deduplicación elimina duplicados, pero no garantiza el orden. Si el consumidor depende del orden de procesamiento, combine este patrón con un mecanismo de ordenación, como Service Bus sesiones de mensajes, o incluya datos de secuencia o versión para que el consumidor pueda rechazar mensajes obsoletos.

  • Instrumento para la observabilidad. Emita la clave de desduplicación y un identificador de correlación en los registros estructurados y realice un seguimiento de una métrica para los duplicados detectados. Una tasa creciente de duplicados puede indicar una mala configuración del productor, un tamaño insuficiente de la ventana de confirmación o de bloqueo, o consumidores con problemas. Use el seguimiento distribuido y la correlación para seguir un mensaje entre servicios.

  • Propague la idempoencia a las llamadas de nivel inferior. Hacer que el consumidor de mensajes sea idempotente no protege los servicios a los que llama. Cuando un consumidor invoca servicios posteriores durante el procesamiento, debe propagar la clave de idempotencia para que cada nivel de servicio pueda desduplicar su propio trabajo.

Cuándo usar este patrón

Use este patrón cuando:

  • Consumes mensajes de un agente que ofrece entrega al menos una vez, que es el valor predeterminado para la mayoría de los agentes.

  • El reprocesamiento de un mensaje puede producir resultados incorrectos, como transacciones financieras duplicadas, creación de recursos duplicados o notificaciones repetidas.

  • Varios consumidores competidores procesan el mismo canal, lo que hace que la entrega duplicada simultánea sea más probable.

Este patrón podría no ser adecuado cuando:

  • Las operaciones que realiza el consumidor ya son idempotentes de forma natural, por lo que volver a procesarlas es inofensivo y la gestión de la deduplicación añade coste sin aportar ningún beneficio.

  • La carga de trabajo puede tolerar los efectos de algún procesamiento duplicado puntual, y el coste de un almacén de desduplicación es mayor que el impacto de un procesamiento duplicado.

Procesamiento idempotente más allá de la mensajería

Este patrón aplica la idempotencia a los consumidores de mensajes, pero el procesamiento idempotente es un principio más amplio de fiabilidad que puede beneficiar a cualquier operación que se ejecute más de una vez sobre una misma tarea. Este principio incluye transformaciones de extracción, transformación y carga (ETL) que reprocesan datos repetidos, procesamiento en flujo que se reanuda desde un punto de control, trabajos programados que se solapan o se reinician, y webhooks o puntos de conexión HTTP que reciben entregas duplicadas.

La misma técnica básica se aplica en cada caso.

  1. Use una clave estable para identificar la unidad de trabajo.
  2. Registra lo que procesas.
  3. Omita o absorba ejecuciones duplicadas para que la repetición del trabajo no cambie el resultado.

Los mecanismos de este artículo, como claves estables, marcadores atómicos y restricciones de unicidad, se transfieren a estos contextos incluso cuando no hay ningún agente de mensajes implicado.

Diseño de cargas de trabajo

Evalúe cómo usar el patrón Idempotent Consumer en el diseño de una carga de trabajo para abordar los objetivos y principios descritos en los pilares de Azure Well-Architected Framework. En la tabla siguiente se proporciona una guía sobre cómo este patrón apoya los objetivos de cada pilar.

Fundamento Cómo apoya este patrón los objetivos de los pilares
Las decisiones de diseño de fiabilidad ayudan a que su carga de trabajo sea resiliente a fallos y garantizan que se recupere a un estado de pleno funcionamiento después de que se produzca un fallo. Este patrón permite que una carga de trabajo use al menos una entrega y reintentos seguros sin dañar los datos, lo que transforma la entrega duplicada de un riesgo de corrección en una condición tolerada.

- RE:07 Autopreservación
- errores transitorios

Si este patrón introduce concesiones dentro de un pilar, considérelas en relación con los objetivos de los otros pilares.

Example

En el ejemplo siguiente se describe un consumidor idempotente que procesa los pedidos de Service Bus y conserva el estado en Azure Cosmos DB para NoSQL.

  1. Un productor establece el Service Bus MessageId como un identificador de pedido a nivel empresarial.
  2. El consumidor recibe el mensaje en modo PeekLock , que hace que el mensaje esté disponible para volver a entregarlo si el consumidor no lo liquida antes de que expire el bloqueo.
  3. El contenedor de Azure Cosmos DB del consumidor se particiona según el identificador de pedido /orderId y establece el id del documento con ese mismo identificador de pedido, de modo que cada copia de un pedido determinado se resuelve en la misma partición lógica y el propio pedido id sirve como marcador de deduplicación.

El consumidor realiza los pasos siguientes para procesar cada mensaje:

  1. Lea el mensaje y úselo MessageId como clave de desduplicación.
  2. Intente crear el documento de pedido con id y la clave de partición establecida en el identificador de pedido.
  3. Si la creación se realiza correctamente, complete el mensaje para que Service Bus lo quite de la cola.
  4. Si la creación falla con el estado HTTP 409 (Conflict) porque ya existe un documento con ese id, lea el documento existente y compárelo con el mensaje actual.
  5. Si coinciden el hash de la solicitud almacenado o los campos de negocio inmutables, considere el mensaje como duplicado, márquelo como completado y omita cualquier procesamiento posterior.
  6. Si el hash de solicitud almacenado o los campos de negocio inmutables no coinciden, envíe el mensaje a una cola de mensajes fallidos y genere una alerta, en lugar de descartar el mensaje silenciosamente. Es posible que el productor haya reutilizado el identificador de contenido diferente o que los detalles del mensaje hayan cambiado desde que se procesó el pedido por primera vez.
  7. Si se produce un error en el procesamiento por un motivo transitorio, abandone el mensaje para que Service Bus vuelva a entregarlo o deje que el bloqueo expire para que otro consumidor pueda recibirlo.

La operación de creación es atómica, por lo que actúa como comprobación de desduplicación y la operación de escritura. Dos consumidores que reciben copias del mismo mensaje no pueden crear el pedido. Un intento de creación tiene éxito, y el otro genera un conflicto y descarta de forma segura su duplicado.

Cuando el procesamiento debe escribir más de un documento, use un lote transaccional que incluya tanto la clave de desduplicación como los documentos empresariales dentro de la misma clave de partición. Dado que un lote transaccional se ejecuta dentro de una sola partición lógica, elija una clave de partición que todos los documentos de un mismo mensaje compartan. El lote confirma todos los documentos de una vez o ninguno, por lo que un fallo entre el procesamiento y el acuse de recibo no puede dejar desincronizados el marcador de desduplicación y los datos de negocio. Un lote que intenta crear un documento que ya existe devuelve un estado 409 (Conflict), que identifica el duplicado.

Para que este consumidor idempotente también sea resiliente ante reintentos de envío duplicados, habilite la detección de duplicados en la cola. En una cola Estándar o Prémium, la detección de duplicados suprime los envíos repetidos dentro de su ventana de historial. El consumidor idempotente sigue gestionando los mensajes duplicados que quedan fuera de esa ventana o que son resultado de una reentrega.

Paso siguiente

  • Las opciones de mensajería asincrónica en Azure describen las opciones de infraestructura de mensajería que determinan las garantías de entrega y los requisitos de control de duplicados.

  • El patrón de bandeja de salida transaccional, que publica mensajes de forma fiable al confirmarlos en la misma transacción que los datos de negocio, es la parte publicadora del patrón de consumidor idempotente.

  • El patrón Retry permite a las aplicaciones controlar los errores transitorios mediante las operaciones de reintento, lo que hace que el procesamiento idempotente sea necesario porque los reintentos pueden provocar la entrega duplicada.

  • El diseño resistente de Azure Event Hubs y Azure Functions aplica este patrón a las funciones desencadenadas por Event Hubs, incluidas técnicas de eliminación de duplicados para flujos de eventos.