Modèle de consommateur idempotent

Concevoir des consommateurs de messages afin que le traitement du même message plusieurs fois ait le même effet que le traitement d’une seule fois. Les systèmes de messagerie qui garantissent une remise au moins une fois peuvent remettre le même message plusieurs fois. Sans résilience contre les doublons, le retraitement d’un message peut créer des enregistrements en double, facturer deux fois un client ou avoir d’autres effets indésirables.

Contexte et problème

Les applications distribuées échangent généralement des tâches par l’intermédiaire d’un courtier de messages plutôt que par des appels synchrones directs. La plupart des répartiteurs, notamment Azure Service Bus, Azure Event Hubs, Apache Kafka et RabbitMQ, fournissent au moins une livraison une fois. Cette garantie garantit qu’un message atteint un consommateur même en cas d’échec, mais cela signifie également que le répartiteur peut remettre le même message plusieurs fois.

Les doublons proviennent de plusieurs sources :

  • Nouvelles tentatives du producteur

    Un producteur envoie un message, ne reçoit pas d’accusé de réception en raison d’une erreur réseau temporaire ou d’un délai d’expiration, et envoie à nouveau le message. Le broker contient maintenant deux copies même si l’envoi a réussi du premier coup.

  • Nouvelle livraison après un accusé de réception manquant

    Un consommateur reçoit et traite un message, mais n’en accuse pas réception, parce qu’il tombe en panne, que le verrou expire ou que l’accusé de réception est perdu. Le répartiteur suppose que le message n’a pas été traité et le remet à nouveau.

  • Défaillances du consommateur pendant le traitement

    Un consommateur termine l’écriture d’une base de données, mais se bloque avant qu’il ne reconnaisse le message. Lorsqu’une autre instance récupère le message renvoyé, elle répète l’opération d’écriture.

La remise en une seule fois dans un système distribué est impossible à garantir en pratique. Même les répartiteurs qui prétendent que les sémantiques « exactly-once » (exactement une fois) garantissent uniquement les opérations qu’ils contrôlent directement, telles que la remise de messages aux consommateurs ou l’écriture de données dans le répartiteur. Ils ne peuvent pas garantir les effets secondaires externes que les consommateurs effectuent dans d’autres systèmes. La solution durable n’est pas d’éliminer la livraison en double. C’est de rendre le consommateur tolérant. Lorsque vous combinez une livraison au moins une fois avec un consommateur qui ignore les doublons, vous obtenez un traitement effectivement une seule fois.

Solution

Faites en sorte que le consommateur soit idempotent en conservant une trace des messages déjà traités et en ignorant tout message qu’il a déjà vu. Le consommateur fonde cette décision sur un identificateur stable qui reste inchangé lors d’une nouvelle remise, vérifie un stockage persistant pour déterminer si cet identificateur a déjà été traité, puis traite le message ou le rejette comme doublon.

Les étapes suivantes décrivent le flux principal :

  1. Lisez le message et extrayez sa clé de déduplication.
  2. Vérifiez le stockage de déduplication pour cette clé.
  3. Si la clé existe, traitez le message comme un doublon. Reconnaissez-le et arrêtez-le, en retournant éventuellement le résultat enregistré précédemment.
  4. Si la clé n’existe pas, traitez le message et enregistrez la clé dans une seule opération atomique, puis reconnaissez le message.

Choisir une clé de déduplication stable

La clé doit identifier de façon unique et cohérente le message logique à chaque nouvelle remise. Utilisez un identificateur de message attribué par le producteur ou une clé d’idempotency au niveau de l’entreprise qui identifie l’opération logique spécifique, et non un contexte de corrélation partagé que plusieurs messages peuvent porter. Dans Azure Service Bus, la MessageId propriété sert cet objectif, car elle identifie de manière unique le message et sa charge utile. N’utilisez CorrelationId pas la clé, car elle regroupe les messages associés, tels qu’une demande et ses réponses. Pour les événements qui suivent la spécification CloudEvents, la combinaison des attributs source et id identifie un événement de manière unique et reste stable lors des réémissions.

Ne vous appuyez pas sur des identifiants au niveau du transport que le broker régénère lors d’une nouvelle remise, ni sur des valeurs dérivées des tentatives de remise, car ces valeurs changent d’un doublon à l’autre et empêchent la détection. Évitez également de dériver la clé à partir de champs volatiles tels que les horodatages de réception.

Lorsque plusieurs consommateurs indépendants traitent le même canal, comme plusieurs abonnés dans une conception de publication-abonnement, chaque consommateur traite légitimement sa propre copie d’un message et doit suivre indépendamment l’achèvement du traitement des messages. Si ces consommateurs partagent un magasin de déduplication, indexez l’enregistrement à l’aide d’une clé composite de l’identité du consommateur et de l’identité du message. Un stockage basé uniquement sur l’identité du message permet au premier consommateur d’empêcher le traitement par tous les autres consommateurs.

Décider où stocker les clés traitées

Vous avez deux options courantes :

  • Table de déduplication dédiée. Le consommateur gère une table distincte, parfois appelée inbox, contenant une ligne par clé traitée. Cette approche conserve les préoccupations de déduplication distinctes des données métier et fonctionne bien lorsque de nombreux types de messages partagent un mécanisme.

  • Entité métier elle-même. Le consommateur stocke la clé sur l’enregistrement que le message crée ou met à jour. Cette approche évite une table distincte, mais couple la déduplication à la forme des données métier.

Enregistrer le marqueur et les effets secondaires de façon atomique

Le processus « vérifier puis traiter » présente une fenêtre de défaillance. Si le consommateur traite le message, puis enregistre la clé dans une étape distincte, une panne entre les deux opérations laisse les effets secondaires appliqués, mais la clé non enregistrée, de sorte que la livraison suivante traite à nouveau le message.

Résolvez cette fenêtre d’échec en écrivant le marqueur de déduplication et les effets secondaires métier dans la même transaction. Lorsque les deux opérations sont validées ensemble, ou pas du tout, une nouvelle tentative de livraison trouve soit le marqueur de validation et n’effectue pas de retraitement, soit aucun marqueur parce que la transaction a été annulée, et retraite alors le message en toute sécurité. Cette variante transactionnelle est le modèle de boîte de réception, et il s’agit du compagnon côté consommation du modèle de boîte d'envoi transactionnelle côté produit.

Protéger contre les doublons simultanés

Dans un mode de livraison « au moins une fois » avec plusieurs consommateurs concurrents, deux instances peuvent recevoir des copies du même message au même moment. Les deux peuvent réussir la vérification d’existence avant qu’aucun des deux n’ait validé, donc la vérification à elle seule n’empêche pas le double traitement.

Appliquez la correction au niveau du magasin de données au lieu de la logique de l’application :

  • Utilisez une contrainte unique sur la clé de déduplication. Les deux transactions tentent d’insérer la clé, mais une seule réussit. L’autre ne respecte pas la contrainte et traite le message comme un doublon. Cette approche rend la base de données l’arbitre unique de la course.

  • Évitez les situations de concurrence de type test-puis-affectation dans les caches. Un modèle qui vérifie une clé, puis la définit en deux opérations distinctes, présente une fenêtre permettant à des tentatives concurrentes de nouvelle exécution de toutes deux s’approprier la clé. Utilisez une écriture conditionnelle atomique, telle qu’une insertion qui échoue en conflit ou une opération "définir si absent", afin que la revendication de la clé soit une seule étape atomique.

Gérer les effets secondaires qui ne peuvent pas participer à la transaction

Certains processus ne peuvent pas participer à la transaction de base de données du consommateur, comme l’appel à une API tierce ou l’écriture de données dans un stockage externe. Pour ces processus, utilisez une approche en deux phases :

  1. Enregistrez la clé dans l’état en cours avant d’effectuer l’action externe.
  2. Effectuez le processus.
  3. Mettez à jour l’enregistrement pour terminer et stocker le résultat.

Lors d’une nouvelle remise, un enregistrement marqué comme terminé vous permet d’éviter de répéter l’appel. Un enregistrement en cours signale qu’une tentative précédente a peut-être été partiellement terminée ou qu’elle est en cours d’exécution par un autre consommateur.

Problèmes et considérations

Tenez compte des points suivants lorsque vous décidez comment implémenter ce modèle :

  • Préférez les opérations naturellement idempotentes. Certaines opérations sont intrinsèquement idempotentes et n’ont pas besoin de déduplication de comptabilité. Une opération d’upsert basée sur un identifiant métier, une écriture qui définit une valeur absolue plutôt qu’un incrément, ou une requête HTTP PUT vers un identifiant de ressource donne le même résultat, qu’elle soit exécutée une seule ou plusieurs fois.

    Vous pouvez parfois rendre une opération naturellement idempotente grâce à un transfert d’état porté par les événements, dans lequel le message contient l’état absolu résultant, par exemple le nouveau statut d’une commande, afin que le consommateur l’applique sous forme d’upsert plutôt que comme une modification relative.

    Conseil / Astuce

    Concevez d’abord pour une idempotence naturelle, et n’ajoutez des techniques de déduplication que pour les opérations qu’il n’est pas possible de rendre naturellement idempotentes.

  • Gérez le cycle de vie des enregistrements de déduplication. Les enregistrements de déduplication s’accumulent, sauf si vous les expirez. Conservez chaque enregistrement au moins tant que le répartiteur peut redéliver le message d’origine. Cette fenêtre dépend du nombre maximal de tentatives de remise du répartiteur, de sa durée de verrouillage ou d’expiration de visibilité, ainsi que de la durée de vie du message. Définissez une durée de vie pour les enregistrements de déduplication qui dépasse cette fenêtre afin qu’une redistribution tardive puisse toujours trouver son marqueur. Supprimer des enregistrements trop tôt rouvre la possibilité de doublons. Tenez compte des messages qu’un opérateur resoumet depuis une file d’attente de lettres mortes, car une resoumission peut se produire longtemps après la période normale de nouvelle remise.

  • Utilisez un framework de messagerie au lieu d’implémenter vous-même la déduplication. La mise en œuvre correcte du magasin de déduplication, de la validation atomique et du nettoyage des enregistrements est sujette aux erreurs. Les infrastructures basées sur les messages fournissent ce modèle en tant que fonctionnalité intégrée.

    Par exemple, NServiceBus déduplique les messages entrants par leur identificateur de message et fournit une rétention et un nettoyage configurables pour les données de déduplication. La boîte de réception du consommateur MassTransit suit les messages reçus par leur identificateur de message pour fournir un comportement consommateur exactement une fois.

  • La déduplication du répartiteur réduit mais ne supprime pas la nécessité d’une logique consommateur idempotente. Certaines plateformes filtrent les doublons au niveau de la couche de transport. Azure Service Bus détection des doublons écarte les messages qui comportent un MessageId répété dans une fenêtre de temps configurée, ce qui supprime les doublons provoqués par les tentatives répétées d’envoi par le producteur. Cette fonctionnalité fonctionne côté envoi et dans une fenêtre délimitée. Cela n’empêche pas un consommateur de traiter deux fois le même message après une nouvelle livraison ; vous avez donc toujours besoin d’une logique idempotente côté consommateur. Considérez les fonctionnalités de la plateforme comme une première couche de défense qui réduit le volume de doublons, et non comme un substitut au modèle.

  • Tenez compte de l’ordre des messages. La déduplication supprime les doublons, mais ne garantit pas l’ordre. Si le consommateur dépend du traitement de la commande, combinez ce modèle avec un mécanisme de classement, tel que Azure Service Bus sessions de messages, ou incluez des données de séquence ou de version qui permettent au consommateur de rejeter les messages obsolètes.

  • Instrument d’observabilité. Émettez la clé de déduplication et un identificateur de corrélation dans les journaux structurés et suivez une métrique pour les doublons détectés. Un taux de doublons croissant peut indiquer une mauvaise configuration côté producteur, une fenêtre d’accusé de réception ou de verrouillage trop petite, ou des consommateurs défaillants. Utilisez le suivi et la corrélation de bout en bout pour suivre un message entre les services.

  • Propagez l’idempotence vers les appels en aval. Le fait de rendre un consommateur idempotent ne protège pas les services qu’il appelle. Lorsqu’un consommateur appelle des services en aval lors du traitement, propagez la clé d’idempotence afin que chaque niveau puisse dédupliquer ses propres opérations.

Quand utiliser ce modèle

Utilisez ce modèle dans les situations suivantes :

  • Vous consommez des messages provenant d’un courtier de messages qui garantit une livraison au moins une fois, ce qui est le mode par défaut de la plupart des courtiers de messages.

  • Le retraitement d’un message produit des résultats incorrects, tels que les transactions financières en double, la création de ressources en double ou les notifications répétées.

  • Plusieurs consommateurs concurrents traitent le même canal, ce qui rend probable la livraison en double simultanée.

Ce modèle peut ne pas convenir lorsque :

  • Chaque opération effectuée par le consommateur est déjà naturellement idempotente, donc le retraitement est inoffensif et la déduplication de la comptabilité ajoute des coûts sans avantage.

  • La charge de travail peut tolérer les effets d’un traitement en double occasionnel, et le coût d’un stockage de déduplication l’emporte sur les conséquences d’un doublon.

Traitement idempotent au-delà de la messagerie

Ce modèle applique le principe d’idempotence aux consommateurs de messages, mais le traitement idempotent est un principe de fiabilité plus général. Toute opération pouvant s’exécuter plusieurs fois sur une tâche identique en bénéficie. Ce principe inclut les transformations d’extraction, de transformation et de chargement (ETL) qui retraitent des données rejouées, le traitement en flux qui reprend à partir d’un point de contrôle, des tâches planifiées qui se chevauchent ou redémarrent, ainsi que des points de terminaison webhook ou HTTP qui reçoivent des livraisons en double.

Dans chaque cas, la même technique de base s’applique :

  1. Identifiez l’unité de travail avec une clé stable.
  2. Enregistrez ce que vous avez déjà traité.
  3. Ignorez ou absorbez les doublons afin que la répétition du travail ne modifie pas le résultat.

Les mécanismes de cet article, tels que les clés stables, les marqueurs atomiques et les contraintes uniques, sont transférés vers ces contextes même lorsqu’aucun répartiteur de messages n’est impliqué.

Conception de la charge de travail

Évaluez comment utiliser le modèle consommateur Idempotent dans la conception d'une charge de travail pour répondre aux objectifs et principes abordés dans les piliers du Azure Well-Architected Framework. Le tableau suivant fournit des conseils sur la façon dont ce modèle prend en charge les objectifs de chaque pilier.

Pilier Comment ce modèle soutient les objectifs des piliers.
Les décisions de conception de fiabilité aident votre charge de travail à devenir résiliente au dysfonctionnement et à s’assurer qu’elle se rétablit dans un état entièrement opérationnel après une défaillance. Ce modèle permet à une charge de travail d’utiliser une livraison au moins une fois et des réessais sûrs sans corrompre les données, ce qui transforme la livraison en double, qui constituait un risque pour l’intégrité, en une condition tolérée.

- RE :07 Autopréservation
- Gérer les erreurs temporaires

Si ce modèle introduit des compromis au sein d’un pilier, considérez-les contre les objectifs des autres piliers.

Exemple

L’exemple suivant montre un consommateur idempotent qui traite les commandes de Azure Service Bus et conserve l’état dans Azure Cosmos DB for NoSQL.

Un producteur définit Service Bus MessageId à un identifiant de commande métier. Le consommateur reçoit des messages en mode PeekLock , ce qui redélise un message si le consommateur ne le termine pas pendant la durée de verrouillage. Le conteneur Azure Cosmos DB du consommateur se partitionne selon l’identifiant de commande (/orderId) et attribue au document id le même identifiant de commande. Ainsi, chaque copie d’une commande donnée aboutit à la même partition logique, et l’enregistrement de la commande lui-même sert de marqueur de déduplication.

Le consommateur traite chaque message comme suit :

  1. Lisez le message et utilisez-le MessageId comme clé de déduplication.
  2. Créez le document de commande avec id et la clé de partition tous deux définis sur l’identifiant de commande.
  3. Si la création réussit, complétez le message afin que Service Bus le supprime de la file d’attente.
  4. Si la création échoue avec un état HTTP 409 (conflit), car un document avec celui-ci id existe déjà, lisez le document existant et comparez-le au message actuel. Si un hachage de requête stocké ou des champs métier immuables correspondent, traitez le message comme un doublon, terminez-le et ignorez le traitement. S’ils ne correspondent pas, le producteur a peut-être réutilisé l’identifiant pour un contenu différent, ou les détails de la commande ont peut-être changé depuis son traitement initial ; dans ce cas, placez le message dans la file des messages morts ou déclenchez une alerte au lieu de l’ignorer silencieusement.
  5. Si le traitement échoue pour une raison transitoire, abandonnez le message afin que Service Bus le remette, ou laissez le verrou expirer afin qu’un autre récepteur le reçoive.

L’opération de création est atomique. Elle sert donc de vérification de déduplication et d’écriture. Deux consommateurs qui reçoivent des copies du même message ne peuvent pas créer la commande. L’une des deux opérations de création réussit, tandis que l’autre renvoie un conflit et rejette son doublon en toute sécurité.

Lorsque le traitement doit écrire plusieurs documents, utilisez un lot transactionnel qui inclut à la fois le document de déduplication et les documents métier au sein de la même clé de partition. Étant donné qu’un lot transactionnel s’exécute au sein d’une seule partition logique, choisissez une clé de partition que partagent tous les documents d’un même message. Le lot valide soit tous les documents, soit aucun, de sorte qu’une panne entre le traitement et l’accusé de réception ne peut pas laisser le marqueur de déduplication et les données métier désynchronisés. Un lot qui tente de créer un document qui existe déjà renvoie un code d’état 409 (Conflit), ce qui permet d’identifier le doublon.

Pour rendre ce consommateur résilient par rapport aux nouvelles tentatives d’envoi en double, activez également la détection des doublons dans la file d’attente. La détection des doublons empêche les envois répétés au sein de sa fenêtre d’historique, et le consommateur idempotent gère les doublons qui surviennent en dehors de cette fenêtre ou qui résultent d’une nouvelle remise.

Étape suivante