L’idempotence dans les systèmes distribués : une approche qui fonctionne réellement

Évitez les effets de bord dupliqués

Sommaire

L’idempotence dans les systèmes distribués est la propriété qui vous sauve lorsque le réseau ment, la file d’attente réessaie, le client panique et l’opérateur relance l’exécution. Dans les systèmes de production, la livraison en double est normale. Les effets de bord en double constituent le bug.

HTTP définit une méthode idempotente comme celle où plusieurs requêtes identiques ont le même effet prévu sur le serveur qu’une seule requête. C’est pourquoi PUT, DELETE et les méthodes sûres sont idempotentes dans la sémantique du protocole et peuvent être réessayées automatiquement après une défaillance de communication.

flux de message d’intégration : idempotence

Cette définition est utile, mais elle est insuffisante. Dans les architectures réelles, l’idempotence n’est pas une réponse de trivia HTTP. C’est une garantie métier. Si un client clique sur « payer » une fois, vous ne pouvez pas facturer deux fois parce qu’un dépassement de délai s’est produit entre l’engagement et la réponse. Si un travailleur met à jour l’inventaire et plante avant d’accuser réception du message, vous ne pouvez pas décrémenter le stock deux fois parce que le courtier a relivré le message. Tel est le standard.

L’erreur que je vois répéter encore et encore est de traiter l’idempotence comme une fonctionnalité de transport plutôt que comme une propriété du système. La déduplication des files d’attente, les verbes HTTP et les reprises client aident, mais aucun d’eux ne sauve une conception qui laisse la même intention métier créer un deuxième effet de bord. Si vous souhaitez une vue d’ensemble sur la façon dont ces décisions d’intégration s’inscrivent dans les limites des services et les compromis de persistance, commencez par Architecture d’application en production : modèles d’intégration, conception de code et accès aux données.

D’où viennent les doublons en production

Les doublons n’apparaissent pas parce que les équipes sont négligentes. Ils apparaissent parce que les systèmes distribués réessaient, réordonnent et rejouent.

Un client peut envoyer une requête de création, le serveur peut l’engager, et la réponse peut quand même disparaître sur le fil. C’est exactement pourquoi HTTP distingue les méthodes idempotentes et pourquoi les API de paiement telles que Stripe et PayPal exposent des mécanismes d’idempotence explicites pour les méthodes non sûres comme POST.

Les courtiers de messages rendent le problème encore plus évident. La livraison au moins une fois signifie qu’un consommateur peut être invoqué répétitivement pour le même message, et un gestionnaire peut mettre à jour la base de données avec succès mais échouer avant l’accusé de réception, causant au courtier de relivrer le même message.

Les webhooks ne sont pas différents. GitHub déclare que les livraisons de webhooks peuvent arriver dans le désordre, les livraisons échouées ne sont pas automatiquement relivrées, et chaque livraison comporte un GUID X-GitHub-Delivery unique que vous devez utiliser pour vous protéger contre la relecture. Pour une vue d’architecture pratique des points de terminaison de chat en tant que limites d’interaction, consultez Les plateformes de chat comme interfaces système dans les systèmes modernes.

Même les systèmes qui annoncent des garanties plus fortes vous laissent encore du travail à faire. Kafka peut empêcher les entrées en double dans les journaux Kafka avec des producteurs idempotents et peut fournir une livraison exactement une fois pour les flux lecture-traitement-écriture qui restent dans Kafka avec des transactions et des consommateurs read_committed. Mais les propres documents de conception de Kafka sont clairs : les systèmes externes nécessitent encore une coordination avec les offsets et les sorties. La livraison exactement une fois de Google Cloud Pub/Sub est limitée aux abonnements de tir, au sein d’une région cloud, et exige toujours que les clients suivent la progression du traitement jusqu’à ce que l’accusé de réception réussisse.

Mon résumé partisan est simple. Supposez que le transport va réessayer. Supposez que les opérateurs vont rejouer. Supposez que les webhooks arriveront en retard. Concevez le chemin d’écriture de sorte qu’une intention répétée ne puisse pas créer un deuxième effet métier. La conception des erreurs est étroitement liée : la façon dont les erreurs sont enveloppées, traduites et classées comme réessayables ou non est partie intégrante de la même discipline des limites — Architecture de gestion des erreurs Go : limites et modèles couvre la classification des erreurs réessayables, la traduction des limites et les modèles sentinelles qui permettent à la logique de reprise de prendre des décisions judicieuses. Lorsque les reprises continuent de heurter une dépendance malsaine, un disjoncteur à la limite d’intégration échoue rapidement avant que les tempêtes de reprise n’amplifient le travail en double.

Le contrat API auquel je fais réellement confiance

Comment les clés d’idempotence empêchent les requêtes API en double

Le seul contrat API auquel je fais confiance pour les opérations de mutation est l’intention fournie par l’appelant plus la persistance côté serveur.

AWS recommande un identifiant de requête fourni par l’appelant et avertit que le service doit enregistrer atomiquement le jeton d’idempotence ensemble avec le travail de mutation. Stripe stocke le premier code d’état et le corps de réponse pour une clé, compare les paramètres ultérieurs avec la requête originale et retourne le même résultat pour les reprises. PayPal utilise PayPal-Request-Id sur les API POST pris en charge et retourne le dernier statut pour la requête précédente avec le même en-tête.

Cela mène à un contrat pratique :

  1. Le client génère une clé d’idempotence pour une opération métier.
  2. Le serveur scope cette clé par locataire et nom d’opération.
  3. Le serveur stocke un hachage de requête afin que la même clé ne puisse pas être réutilisée pour un payload différent.
  4. Le serveur enregistre l’état tel que en attente, terminé ou échoué.
  5. Les reprises avec la même clé retournent soit le résultat stocké, soit un pointeur stable vers celui-ci.
  6. Les reprises avec la même clé et un payload différent échouent bruyamment.

Il existe une ébauche d’en-tête Idempotency-Key de l’IETF, mais au 09/05/2026, elle est toujours listée dans le Datatracker de l’IETF comme un Internet-Draft expiré plutôt qu’une RFC publiée. En pratique, le nom de l’en-tête est toujours largement utile comme convention de facto, mais vous devriez documenter le contrat dans votre propre API au lieu de prétendre que la norme est terminée.

Que doit représenter la clé ? L’intention. Pas une tentative HTTP. Pas une connexion TCP. Pas un compteur de reprise. Si l’utilisateur signifie « créer la commande 123 une fois », chaque reprise pour cette même commande doit réutiliser la même clé. Si l’utilisateur signifie « passer une deuxième commande », cela doit utiliser une clé différente.

Un ID de requête est pour le traçage. Une clé d’idempotence est pour la correction. Si vous mélangez ceux-ci, vos tableaux de bord semblent propres tandis que votre argent bouge deux fois.

Pourquoi PUT n’est pas suffisant

Non, HTTP PUT n’est pas suffisant pour rendre une opération idempotente.

Oui, la RFC 9110 donne à PUT des sémantiques idempotentes. Mais si votre gestionnaire PUT émet un nouvel événement en aval, envoie un email à chaque reprise ou facture à nouveau un fournisseur externe, alors votre implémentation a violé le contrat métier même si le nom de votre route semble respectable.

Le choix du verbe aide les clients à comprendre l’intention. Il n’implémente pas l’intention pour vous.

Utilisez PUT lorsque le modèle de ressource correspond véritablement à une opération de remplacement complet ou de type upsert. Utilisez POST lorsque vous créez des commandes ou des actions. Mais pour toute mutation qui pourrait être réessayée à travers les limites du réseau, documentez un contrat d’idempotence explicite. Si vos actions de mutation sont déclenchées depuis des flux de travail de chat, le même contrat s’applique dans Modèles d’intégration Slack pour les alertes et les flux de travail et Modèle d’intégration Discord pour les alertes et les boucles de contrôle. Les effets de bord cachés sont là où l’architecture va mourir.

Combien de temps une clé d’idempotence doit-elle être stockée

Plus longtemps que votre équipe de transport ne le souhaite.

Stripe dit que les clés peuvent être supprimées après au moins 24 heures. PayPal dit que la rétention est spécifique à l’API et donne des exemples qui peuvent durer jusqu’à 45 jours. Amazon SQS FIFO déduplique uniquement dans une fenêtre de 5 minutes. GitHub conserve les livraisons récentes pendant 3 jours pour la relivraison manuelle. Ces chiffres sont wildly différents parce que la période de rétention correcte est une décision métier, pas un protocole par défaut.

Si vous ne gardez les clés que pour cinq minutes parce que votre file d’attente le fait, vous ne concevez pas l’idempotence. Vous copiez une limitation de transport dans votre couche métier.

Conservez les enregistrements d’idempotence pour au moins le maximum de ces fenêtres :

  • horizon de reprise client
  • horizon de redriving de file d’attente
  • horizon de relecture des webhooks
  • horizon de relecture de l’opérateur
  • horizon de règlement ou de compensation pour les opérations de mouvement d’argent

Pour les paiements, les réservations et le provisionnement, cela signifie souvent des heures ou des jours, pas des minutes.

AWS souligne également deux anti-modèles avec lesquels je suis entièrement d’accord. N’utilisez pas les horodatages comme clé, car les décalages d’horloge et les collisions les rendent peu fiables. Ne stockez pas aveuglément les payloads de requête entiers comme enregistrement de déduplication pour chaque requête, car cela nuit aux performances et à l’évolutivité. Stockez un hachage de requête normalisé plus l’état de réponse minimum dont vous avez besoin pour rejouer en toute sécurité. Si vous devez reproduire la première réponse byte pour byte, stockez le corps de réponse canonique comme le fait Stripe.

Les modèles de base de données qui rendent l’idempotence réelle

L’idempotence devient réelle lorsque la couche de persistance peut gagner une course exactement une fois.

PostgreSQL vous donne deux primitives critiques ici. Les contraintes uniques imposent l’unicité sur une ou plusieurs colonnes, et INSERT ... ON CONFLICT vous permet de définir une action alternative au lieu d’échouer sur une violation d’unicité. PostgreSQL documente également que ON CONFLICT DO UPDATE garantit un résultat d’insertion ou de mise à jour atomique sous concurrence.

Cela signifie que votre couche d’idempotence devrait généralement commencer avec une table comme celle-ci :

create table api_idempotency (
    tenant_id text not null,
    operation text not null,
    idempotency_key text not null,
    request_hash text not null,
    state text not null,
    status_code integer,
    response_body jsonb,
    resource_type text,
    resource_id text,
    created_at timestamptz not null default now(),
    expires_at timestamptz not null,
    primary key (tenant_id, operation, idempotency_key)
);

Et le flux de traitement devrait ressembler à ceci :

begin transaction

try insert (tenant_id, operation, idempotency_key, request_hash, state='pending')
on conflict do nothing

load row for (tenant_id, operation, idempotency_key) for update

if row.request_hash != incoming_request_hash
    fail with conflict or validation error

if row.state = 'completed'
    return stored response

if row.state = 'pending' and row was created by another live request
    either wait briefly, or fail fast with a retryable response

perform local business mutation

store stable result in idempotency row
set state = 'completed'

commit
return result

La partie importante n’est pas la syntaxe. La partie importante est l’atomicité. L’enregistrement de la clé et l’exécution de la mutation doivent réussir ou échouer ensemble. AWS le dit explicitement pour l’idempotence API, et la même règle s’applique aux services basés sur SQL.

Ne faites pas une séquence naïve de vérification puis action comme « sélectionner la clé ; si manquante alors insérer la commande ». Sous concurrence, deux requêtes peuvent passer la vérification et toutes deux créer l’effet de bord. Une contrainte unique n’est pas optionnelle. C’est le mécanisme qui transforme votre architecture d’un folklore optimiste en quelque chose que vous pouvez prouver sous charge.

Voici la règle que j’utilise dans les revues. Si la décision de déduplication n’est pas protégée par la même limite transactionnelle que la mutation, vous n’avez pas d’idempotence. Vous avez de l’espoir.

Les messages, événements et webhooks ont besoin de leurs propres limites

Comment les consommateurs gèrent les événements et messages en double

Pour les consommateurs de messages, le modèle classique est toujours le bon. Enregistrez les ID de messages traités dans la même transaction de base de données que la mise à jour métier. Chris Richardson décrit directement l’approche de la table PROCESSED_MESSAGES, en utilisant une clé primaire sur l’abonné et l’ID de message afin que les doublons échouent proprement et puissent être ignorés.

De nombreuses équipes appellent ce magasin explicite processed_messages une table d’inbox. L’étiquette importe moins que la règle. Le récepteur doit persister la preuve qu’il a déjà traité le message avant qu’une reprise ne puisse faire rien en toute sécurité.

Une forme minimale ressemble à ceci :

create table processed_messages (
    subscriber_id text not null,
    message_id text not null,
    processed_at timestamptz not null default now(),
    primary key (subscriber_id, message_id)
);

Et le flux du consommateur est aussi strict que le flux HTTP :

begin transaction

insert into processed_messages (subscriber_id, message_id)
values (?, ?)
on conflict do nothing

if no row inserted
    rollback
    ack and ignore duplicate

apply business mutation

commit
ack message

Ce modèle est ennuyeux. Bien. L’idempotence devrait être ennuyeuse.

C’est aussi généralement meilleur que d’essayer de s’appuyer sur des termes marketing de courtier. Le support exactement une fois de Kafka est excellent lorsque vous restez dans le modèle transactionnel de Kafka, mais les documents de Kafka avertissent encore que les destinations externes ont besoin de coopération. SQS FIFO réduit les envois en double uniquement dans sa fenêtre de déduplication de 5 minutes. Exactly-once de Pub/Sub s’attend toujours à ce que l’abonné suive la progression et évite le travail en double lorsque les accusés de réception échouent.

Exactly-once est généralement une optimisation locale. Les effets de bord idempotents sont la garantie du système.

Associez la déduplication avec le modèle outbox

Si votre service met à jour l’état local et publie également un événement, la consommation idempotente seule n’est pas suffisante. Vous avez également besoin d’un moyen sûr de sortir l’événement après l’engagement de la transaction locale.

C’est pourquoi le modèle outbox transactionnel est important. Chris Richardson décrit l’idée de base comme l’écriture de l’événement dans une table outbox dans la même transaction que la mise à jour métier, puis la publication asynchrone. Debezium dit que le modèle outbox évite les incohérences entre l’état interne d’un service et les événements consommés par d’autres services. NServiceBus va plus loin et montre comment le traitement outbox déduplique les messages entrants et évite les enregistrements zombies et les messages fantômes.

Voici l’architecture que je recommande pour les services qui possèdent des données et publient des événements d’intégration :

  1. Validez et persistez la commande sous une clé d’idempotence.
  2. Écrivez l’état métier et l’événement outbox dans une seule transaction locale.
  3. Laissez le CDC ou un dispatcher outbox publier l’événement.
  4. Rendez les consommateurs en aval également idempotents.

Outbox ne supprime pas le besoin de consommateurs idempotents. Il supprime le besoin de prétendre qu’un engagement de base de données et une publication de courtier peuvent être une seule transaction distribuée magique lorsqu’ils ne le peuvent généralement pas.

Les webhooks sont juste des messages avec un meilleur branding

Traitez les webhooks entrants exactement comme des messages d’un bord réseau non fiable.

GitHub documente que les livraisons peuvent arriver dans le désordre, recommande d’utiliser X-Hub-Signature-256 pour vérifier l’authenticité et fournit X-GitHub-Delivery comme identifiant de livraison unique. Il note également que les relivraisons réutilisent le même ID de livraison.

Donc l’architecture est simple :

  • vérifiez la signature en premier
  • utilisez le GUID de livraison comme clé de déduplication
  • persistez la réception avant les effets de bord
  • rendez les gestionnaires conscients de l’ordre plutôt que de supposer l’ordre d’arrivée
  • enfilez le travail lourd et retournez rapidement

Si votre gestionnaire de webhook écrit directement dans les tables métier avant d’enregistrer la réception, il n’est pas prêt pour la production. Il est simplement plus rapide à faire des erreurs en double.

Les sagas et moteurs de workflow ont toujours besoin d’idempotence

Les sagas et moteurs de workflow durables ne suppriment pas le problème. Ils le rendent visible.

Temporal recommande d’écrire des Activités pour être idempotentes car les Activités peuvent être réessayées après des échecs ou des dépassements de délai. Ses documents mentionnent même le cas limite où un travailleur complète un effet de bord externe avec succès mais plante avant de signaler la complétion, ce qui cause à l’Activité de s’exécuter à nouveau. Temporal suggère également d’utiliser une combinaison de l’ID d’exécution du Workflow et de l’ID d’Activité comme clé d’idempotence stable lors de l’appel de services en aval. Si vous appliquez cela dans l’orchestration de services, Microservices Go pour l’orchestration IA/ML couvre les compromis de workflow plus larges.

C’est exactement le bon modèle mental. Un moteur de workflow peut préserver l’historique d’exécution et coordonner les reprises. Il ne peut pas rétroactivement annuler la facturation d’une carte ou annuler l’envoi d’un email à moins que votre application ne lui donne des étapes idempotentes et des compensations idempotentes.

La même chose s’applique aux sagas. Les propres conseils de saga de Temporal décrivent des actions de compensation qui s’exécutent lorsqu’une étape échoue. Ces compensations doivent être idempotentes aussi. Si « rembourser le paiement » s’exécute deux fois, vous pouvez avoir résolu le bug original en en créant un nouveau.

Ma règle ici est brutale et simple. Chaque Activité, chaque gestionnaire de commande et chaque compensation qui touche le monde extérieur devrait soit être naturellement idempotent, soit porter une clé d’idempotence réelle vers le système en aval.

Comment tester l’idempotence avant la production

La plupart des équipes testent les chemins heureux puis font semblant d’être surprises lorsque les reprises se produisent. Cela ne suffit pas. Pour les équipes Go, Tester le code Go concurrent avec testing/synctest couvre comment écrire des tests rapides et déterministes pour les boucles de reprise et le comportement de délai de contexte sans dormir à travers des délais artificiels.

Vous devriez avoir des tests automatisés pour au moins ces cas :

  • le serveur engage la mutation mais la réponse n’atteint jamais le client
  • deux requêtes identiques sont en concurrence avec la même clé d’idempotence
  • la même clé est réutilisée avec un payload différent
  • un consommateur engage son travail de base de données et plante avant l’accusé de réception
  • un webhook est rejoué avec le même ID de livraison
  • un dispatcher outbox publie le même événement plus d’une fois
  • une Activité de workflow complète l’appel externe et plante avant que la complétion ne soit signalée
  • un enregistrement d’idempotence expire et une vraie reprise tardive arrive

AWS recommande explicitement des suites de tests complètes qui incluent des requêtes réussies, des requêtes échouées et des requêtes en double. Ce conseil est banal et absolument correct.

J’ajouterais un exercice d’échec de plus. Vérifiez que la réponse rejouée est sémantiquement équivalente au premier résultat. AWS discute des reprises arrivant tard et plaide pour des réponses qui préservent le sens original même après que l’état sous-jacent a changé. C’est la différence entre « aucun effet de bord supplémentaire n’est survenu » et « l’appelant a toujours un contrat cohérent ».

Des règles partisans qui sauvent les systèmes réels

Voici les règles que j’imposerais dans une revue d’architecture.

Premièrement, les clés d’idempotence appartiennent à l’intention métier, pas aux tentatives de transport.

Deuxièmement, scopez chaque clé par locataire et opération. Les espaces de clés globaux sont la façon dont des requêtes non liées entrent en collision.

Troisièmement, persistez la décision de déduplication atomiquement avec la mutation. Si ce n’est pas vrai, la conception est fausse.

Quatrièmement, rejetez les reprises de même-clé et de payload différent. Stripe et AWS font cela pour une bonne raison.

Cinquièmement, conservez les clés pour l’horizon de relecture complet du processus métier, pas pour la fenêtre de file d’attente la plus courte.

Sixièmement, associez les producteurs avec un outbox et les consommateurs avec le suivi des ID de message. Un côté sans l’autre est la moitié d’une conception.

Septièmement, propagez la même identité d’opération en aval lorsque l’action métier est la même. AWS recommande explicitement de passer le jeton d’idempotence le long de la chaîne de traitement.

Huitièmement, ne supposez jamais que le marketing exactement une fois supprime le besoin d’effets de bord idempotents.

Si cela semble strict, tant mieux. L’idempotence est là où l’architecture optimiste rencontre la réalité de la production. Vous n’avez pas besoin de complexité partout. Mais partout où des effets de bord en double nuiraient à l’argent, à l’état ou à la confiance, l’idempotence devrait être une partie de premier plan du contrat.

Ces mêmes règles s’appliquent directement aux agents d’IA en arrière-plan. Les agents de sondage qui revendiquent des tâches, émettent des notifications ou déclenchent des appels d’outils ont besoin de clés de déduplication et de protocoles de revendication idempotents autant que les API de paiement. Pour savoir comment le modèle de revendication et de déduplication fonctionne à l’intérieur des assistants IA de production, consultez Agents de sondage dans les assistants IA : 11 modèles d’implémentation.

Liens utiles

S'abonner

Recevez de nouveaux articles sur les systèmes, l'infrastructure et l'ingénierie IA.