Modèle de consommateur idempotent

Concevoir des consommateurs de messages afin que le traitement du même message plusieurs fois ait les mêmes effets que le traitement une seule fois. Les systèmes de messagerie qui garantissent une remise au moins une fois peuvent remettre le même message plusieurs fois. La résistance aux doublons garantit que le retraitement d’un message ne crée pas d’enregistrements en double, ne facture pas deux fois un client et n’entraîne pas d’autres effets indésirables.

Contexte et problème

Les applications distribuées échangent généralement via un répartiteur de messages au lieu d’utiliser 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 signifie également que le répartiteur peut remettre le même message plusieurs fois.

Il n’est pas pratique de garantir une livraison exactement une fois sur un système distribué. Même les répartiteurs qui utilisent les sémantiques « exactly-once » (exactement une fois) ne peuvent garantir que 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 contrôler les effets secondaires que les consommateurs implémentent dans des systèmes externes. La solution durable n’est pas d’éliminer les livraisons en double, mais de faire en sorte que le consommateur les traite correctement. Lorsque vous combinez une livraison au moins une fois avec un consommateur qui ignore les doublons, vous obtenez un traitement effectivement une seule fois.

Les doublons peuvent provenir de plusieurs sources :

  • Nouvelle tentative 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.

  • Échecs consommateur lors du traitement. Un consommateur termine une écriture de base de données, mais se bloque avant d’accuser réception du message. Une autre instance récupère le message et répète l’écriture.

Solution

Créez un consommateur idempotent en conservant une trace des messages qu’il a traités avec succès et en ignorant les messages qu’il a déjà traités, sur la base d’un identifiant stable qui subsiste lors d’une réexpédition. Le consommateur vérifie le magasin d’identificateurs persistants pour déterminer s’il a déjà traité cet identificateur, puis traite le message ou l’ignore en tant que doublon.

Le flux de base se compose des étapes suivantes :

  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 déjà dans le magasin, traitez le message comme un doublon. Accusez réception du message et arrêtez de le traiter, en retournant éventuellement le résultat enregistré précédemment.
  4. Si la clé n’existe pas encore dans le magasin, traitez le message et enregistrez la clé dans une seule opération atomique, puis reconnaissez le message.

Les sections suivantes fournissent des conseils pour rendre les consommateurs idempotents :

Choisir une clé de déduplication stable

La clé de déduplication doit identifier de manière unique et cohérente le message logique dans chaque redélivery. 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.

Par exemple, définissez la propriété Service Bus MessageId sur une valeur qui identifie de manière unique le message logique. N’utilisez CorrelationId pas la clé, car elle fait référence à des groupes de messages connexes, tels qu’une demande et ses réponses. Pour les événements qui suivent la spécification CloudEvents, la sourceid combinaison d’attributs identifie de façon unique un événement et reste stable dans les redeliveries.

Ne vous basez pas sur les identifiants de transport que le broker régénère lors d’une nouvelle remise, ni sur les valeurs dérivées du nombre de tentatives de remise. Ces valeurs changent d’une livraison à l’autre et empêchent la détection des doublons. É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 d’abonnement de publication, chaque consommateur reçoit sa propre copie d’un message et doit suivre indépendamment la fin du traitement. Si les consommateurs partagent un magasin de déduplication, utilisez une clé composée de l’identité du consommateur et de l’identité du message pour les enregistrements. Un stockage basé uniquement sur l’identité du message permettrait au premier consommateur d’empêcher le traitement par tous les autres.

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

Les options de stockage suivantes sont courantes pour les clés traitées :

  • 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 si de nombreux types de messages partagent le même 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 au type de données métier.

Valider la clé traitée et les effets secondaires atomiquement

Un processus de vérification puis de traitement comporte une fenêtre de défaillance. Si un consommateur traite un 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 le consommateur traite à nouveau la nouvelle livraison suivante.

Évitez cette fenêtre de défaillance en écrivant le marqueur de déduplication et les effets métier dans la même transaction. Si un consommateur effectue les deux opérations ensemble ou pas du tout, lors d’une nouvelle livraison, il trouvera soit le marqueur et ignorera le message, soit aucun marqueur parce que la transaction n’est pas allée à son terme et retraitera alors le message en toute sécurité. Cette variante transactionnelle est le modèle Inbox et constitue le pendant côté consommateur du modèle Transactional Outbox côté producteur.

Protéger contre les doublons simultanés

Avec une livraison « au moins une fois » et des consommateurs concurrents, deux instances peuvent recevoir des copies du même message au même moment. Les deux instances peuvent réussir la vérification d’existence avant que l’une ou l’autre ne valide sa transaction, de sorte que la seule vérification n’empêche pas le double traitement.

Appliquez la correction au niveau du magasin de données au lieu de la logique d’application en effectuant les étapes suivantes :

  • Utilisez une contrainte d’unicité sur la clé de déduplication afin que deux transactions puissent tenter d’insérer une clé, mais une seule peut réussir. L’autre transaction ne respecte pas la contrainte et considère le message comme un doublon. Cette approche rend la base de données l’arbitre unique du conflit.

  • Évitez les situations de concurrence de type test-puis-affectation dans les caches. Un modèle qui vérifie une clé, puis le définit dans deux opérations distinctes a une fenêtre qui permet aux nouvelles tentatives simultanées de revendiquer la clé. Utilisez une écriture conditionnelle atomique, telle qu’une insertion qui échoue en cas de conflit ou une opération de définition si absent, afin que l’appropriation de la clé constitue 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. Utilisez l’approche en deux phases suivante pour ces processus :

  1. Enregistrez la clé à l’état en cours, puis effectuez l’action externe.
  2. Mettez à jour l’enregistrement pour terminer et stocker le résultat.

Lors d’une nouvelle livraison, un enregistrement finalisé indique au consommateur de ne pas répéter l’appel. Un enregistrement en cours signale qu’une tentative précédente peut être partiellement terminée ou traitée par un autre consommateur. Le consommateur doit rapprocher les enregistrements périmés ou transmettre les cas non résolus pour intervention avant d’accuser réception de la nouvelle livraison.

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 la comptabilité. Un upsert basé sur un identifiant métier, une opération d’écriture qui définit une valeur absolue plutôt qu’un incrément, ou un HTTP PUT adressé à un identifiant de ressource produisent les mêmes résultats qu’ils soient exécutés une seule fois ou plusieurs fois.

    Vous pouvez parfois effectuer une opération naturellement idempotente à l’aide d’un transfert d’état porté par des événements. Le message contient l’état absolu résultant, tel que le nouveau statut d’une commande, afin que le consommateur l’applique comme une opération d’upsert plutôt que comme une modification relative.

    Conseil / Astuce

    Concevez une idempotence naturelle si possible, et n’utilisez des techniques de déduplication que pour les opérations qui ne peuvent pas être rendues naturellement idempotentes.

  • Utilisez une infrastructure de messagerie au lieu de configurer la déduplication. L’implémentation correcte du stockage de déduplication, de la validation et du nettoyage 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 leurs identificateurs de message et fournit une rétention et un nettoyage configurables pour les données de déduplication. La boîte d’envoi du consommateur MassTransit suit les messages reçus par ses identificateurs de message pour fournir un comportement consommateur exactement une fois.

  • Gérez le cycle de vie des enregistrements de déduplication. Les enregistrements de déduplication s’accumulent, sauf si vous les définissez pour expirer. Conservez chaque enregistrement au moins tant que le répartiteur peut toujours redéliver le message d’origine. La taille de cette fenêtre dépend du nombre maximal de tentatives de remise du broker, de son délai d’expiration de verrouillage ou de visibilité, et de la durée de vie du message.

    Définissez une durée de vie pour les enregistrements de déduplication supérieure à cette fenêtre, afin qu’une nouvelle remise tardive retrouve quand même son marqueur. Supprimer des enregistrements trop tôt rouvre la possibilité de doublons. Tenez compte des messages que les opérateurs renvoient depuis les files d’attente de messages en erreur, car ces renvois peuvent se produire longtemps après l’expiration du délai normal de nouvelle remise.

  • Ne remplacez pas la déduplication du répartiteur pour la logique consommateur idempotente. Certaines plateformes filtrent les doublons au niveau de la couche de transport. Par exemple, la détection des doublons de Service Bus écarte les messages qui répètent un MessageId dans une fenêtre de temps configurée, ce qui supprime les nouvelles tentatives d’envoi en double du producteur.

    Cette fonctionnalité fonctionne côté envoi et dans une fenêtre délimitée, de sorte qu’elle n’empêche pas un consommateur de traiter le même message deux fois après une redélivery. Il vous faut encore une logique du consommateur idempotente. Utilisez les fonctionnalités de la plateforme pour réduire le volume des doublons, et non en remplacement du modèle de consommateur idempotent.

  • 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 Service Bus sessions de messages, ou incluez des données de séquence ou de version afin que le consommateur puisse 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 distribué et la corrélation pour suivre un message entre les services.

  • Propagez l’idempotence vers les appels en aval. Rendre le consommateur de messages 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 de service puisse éliminer ses propres doublons.

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 peut produire des résultats incorrects, tels que des transactions financières en double, la création de ressources en double ou des notifications répétées.

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

Ce modèle peut ne pas convenir lorsque :

  • Les opérations effectuées par le consommateur sont déjà naturellement idempotentes, de sorte que 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 est supérieur à l’impact 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 large qui peut bénéficier à toute opération exécutée plusieurs fois sur une tâche identique. 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.

La même technique de base s’applique dans chaque cas.

  1. Utilisez une clé stable pour identifier l’unité de travail.
  2. Enregistrez ce que vous traitez.
  3. Ignorez ou absorbez les exécutions dupliquées 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 d’unicité, sont transférés vers ces contextes même lorsqu’aucun répartiteur de messages n’est impliqué.

Pour les points de terminaison HTTP, consultez la sémantique des méthodes HTTP et l’idempotence pour obtenir des conseils sur le comportement spécifique à la méthode et la documentation des nouvelles tentatives sans risque.

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
- 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 décrit un consommateur idempotent qui traite les commandes de Service Bus et stocke l’état dans Azure Cosmos DB for NoSQL.

  1. Un producteur définit Service Bus MessageId à un identifiant de commande métier.
  2. Le consommateur reçoit le message en mode PeekLock , ce qui rend le message disponible pour redelivery si le consommateur ne le résout pas avant l’expiration du verrou.
  3. Le conteneur Azure Cosmos DB du consommateur est partitionné selon l’identifiant de commande /orderId et définit le document id sur ce même identifiant de commande, de sorte que chaque copie d’une commande donnée aboutit à la même partition logique et que la commande id elle-même sert de marqueur de déduplication.

Le consommateur effectue les étapes suivantes pour traiter chaque message :

  1. Lisez le message et utilisez-le MessageId comme clé de déduplication.
  2. Tentez de créer 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, terminez 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.
  5. Si le hachage de requête stocké ou les champs métier immuables correspondent, traitez le message comme un doublon, terminez-le et ignorez le traitement ultérieur.
  6. Si le hachage de demande stockée ou les champs métier immuables ne correspondent pas, envoyez le message à une file d’attente de lettres mortes et déclenchez une alerte, plutôt que de supprimer silencieusement le message. Le producteur a peut-être réutilisé l’identificateur pour un contenu différent, ou les détails du message ont peut-être changé depuis le premier traitement de la commande.
  7. Si le traitement échoue pour une raison transitoire, abandonnez le message afin que Service Bus le remette en distribution, ou laissez le verrou expirer afin qu’un autre consommateur puisse le recevoir.

L’opération de création est atomique, donc elle sert à la fois de vérification de déduplication et d’opération d’écriture. Deux consommateurs qui reçoivent des copies du même message ne peuvent pas créer la commande. L’une des tentatives de création réussit, tandis que l’autre renvoie un conflit et rejette sans risque le doublon.

Lorsque le traitement doit écrire plusieurs documents, utilisez un lot transactionnel qui inclut à la fois la clé 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 dans une seule partition logique, vous devez choisir une clé de partition que partagent tous les documents d’un même message. Le lot valide tous les documents ensemble, ou aucun, de sorte qu’un plantage entre le traitement et l’accusé de réception ne puisse pas désynchroniser le marqueur de déduplication et les données métier. Un lot qui tente de créer un document existant renvoie un code d’état 409 (Conflit), ce qui permet d’identifier le doublon.

Pour rendre ce consommateur idempotent également résilient par rapport aux nouvelles tentatives d’envoi en double, activez la détection des doublons dans la file d’attente. Sur une file d’attente Standard ou Premium, la détection en double supprime les envois répétés dans sa fenêtre d’historique. Le consommateur idempotent gère toujours les doublons qui se trouvent hors de cette fenêtre ou qui résultent d’un réacheminement.

Étape suivante