Sémantique Exactly-once
Qu'est-ce qu'exactly-once en fait
Par « exactly-once », on entend souvent deux choses différentes :- Livraison : le message sera livré au consommateur exactement une fois.
- Traitement : l'effet secondaire final (enregistrement dans la base de données, changement de bilan, émission d'un autre événement) se produira exactement une fois, même s'il y a eu plus de livraisons ou de tentatives.
Dans les systèmes distribués, il est plus fiable de parler de la sémantique du traitement. La livraison d'une seule fois est difficile à fournir (doublons et répétitions sont inévitables), mais vous pouvez faire en sorte que l'état total soit équivalent à un seul traitement.
Quand EOS est nécessaire et quand pas
EOS est nécessaire si :- Transactions monétaires et bilans : le double débit n'est pas acceptable.
- Comptage des licences/quotas, compteurs de facturation.
- Appels externes irréversibles (par exemple, activation unique d'une clé).
- Les effets sont réversibles ou compensables (sagas, retours).
- Les doublons temporaires sont autorisés dans les vitrines/logs.
- Il est moins cher de fournir un sink idempotent que de traîner des transactions sur l'ensemble du chemin.
Modèle : end-to-end vs. hop-by-hop
Hop-by-hop EOS : chaque section (source → processeur → récepteur) garantit qu'elle applique son action exactement une fois.
End-to-end EOS : l'ensemble de la chaîne garantit que, du « fait » à l' « effet side », le résultat est équivalent à un seul traitement.
En pratique, le end-to-end est obtenu en combinant les transactions et/ou l'idempotence sur chaque hop.
Blocs de base
1. Opérations idempotentes
Répéter la même requête sur la clé de l'opération donne le même résultat.
Ключи: `idempotency_key`/`event_id`/`operation_id`.
Implémentation : Table des opérations « vues » avec TTL ≥ Retence du journal d'entrée.
2. Transactions « je lis-traite-écris » (read-process-write)
Dans une unité atomique, les effets secondaires et les progrès de la lecture (offsets/position) sont enregistrés. Cela élimine les « fantômes » lorsque vous tombez entre les étapes.
3. Versioning/SEQUENCE
La version/compteur est stockée pour l'unité ; les modifications ne s'appliquent que si 'expected _ version' correspond. Les répétitions du même événement n'augmentent pas la version → l'effet une fois.
4. Déduplication
Index par '(consumer_id, event_id)' ou par 'business _ id'naturel de l'opération.
Modèles de mise en œuvre
1) Journal transactionnel + sink transactionnel avec fixation offset
Idéal pour le stream-processing.
Nous lisons dans le journal (seulement les entrées confirmées).
Nous faisons le traitement.
- a) on enregistre l'effet dans sink (OBD/table),
- b) fixons "lu avant l'offset N' (dans la même OBD).
- Commit. Avec le restart, tout est magique (et l'offset est déplacé) ou rien.
Propriétés : l'exécution répétée ne nuit pas ; « une fois exactement » par effet, même si le message a été lu deux fois.
2) Outbox + idempotent consumer
Pour les services transactionnels-producteurs.
En une seule transaction OBD : nous modifions l'enregistrement du domaine et écrivons l'événement dans la boîte de réception.
Le republicateur envoie l'événement au bus avec le même 'event _ id'.
Les consumers utilisent les événements idempotent (dédup par 'event _ id').
Propriétés : le producteur veille à ce que le fait ne soit pas perdu ; les consumers garantissent exactement un effet.
3) EOS dans les systèmes de type Kafka/Flink (conceptuellement)
Producteur idempotent : protège contre les prises lors des retraits d'expédition.
Transactions du producteur : groupe d'entrées dans les topiques + décalage du consumère commuté atomiquement ; les lecteurs utilisent l'isolation "read _ committed'.
Le côté processeur stocke l'état (state store) et l'échange avec la transaction.
Propriétés : redémarrer le store/task n'entraîne pas de double effet ; doublons « non visibles » downstream.
4) Idempotent « siki » (sinks) via upsert/merge
Sink accepte 'opération _ id '/' event _ id' et exécute 'UPSERT... WHERE NOT EXISTS`.
L'effet secondaire (par exemple, la cotisation) se produit atomiquement avec la vérification « n'a pas déjà été appliqué ».
Propriétés : Méthode EOS bon marché à la frontière avec le stockage, sans transactions distribuées.
Détails clés de la mise en œuvre
ID d'opération
Doivent être déterministes pour les répétitions (ne générez pas un nouvel UUID lors de la rétraction).
Avoir une zone de visibilité stable (par consumer/par unité/par système).
Tableau de déduplication
Колонки: `consumer_id`, `operation_id`, `applied_at`, `ttl_expires_at`.
Index par '(consumer_id, operation_id)'.
La TTL ≥ la fenêtre de répétition maximale (rétention du journal + retards potentiels).
Une concurrence optimiste
Dans le modèle write, stockez la version de l'unité.
Lorsque vous appliquez un événement/commande, utilisez 'WHERE version = : expected' ; le double n'augmentera pas la version.
Commande/ordre
EOS n'est pas égal à « exactement le même ordre ». Assurez la cohérence à travers la clé de lot (tous les événements de l'agrégat → un lot) et/ou la comparaison « sequence ».
Appels externes idempotent
Pour les méthodes non sécurisées (par exemple les webhooks HTTP dans un service tiers), ajoutez 'Idempotency-Key' et demandez à votre partenaire de le prendre en charge.
Pièges fréquents
EOS à un seul endroit : si sink est idempotente, mais que vous émettez des événements secondaires sans idempotente, obtenez « exactement plusieurs fois » downstream.
Deux commits : d'abord dans la base de données, puis dans le commit offset dans le courtier - la chute entre les deux crée des effets en double.
CDC crus vers l'extérieur : un changement dans le schéma OBD brise l'idempotence des consommateurs.
Clés instables : 'operation _ id' dépend du temps/random et change avec le rétro.
Coût et compromis
Latence : transactions/lectures isolées → croissance p95/p99.
Overhead storage : tables de dédupit, stores d'état, logs de transaction.
Complexité de l'exploitation : délais de transaction, rééquilibrage des flux, sessions « éteintes ».
Diagnostic : plus d'états ("dans la cabane", "vu comme read_committed", "reculé").
Choisissez EOS point par point : pour les agrégats critiques et les effets ; couvrir le reste par l'idempotence et les compensations.
Test exactly-once
1. Fault-injection : la chute du processus entre les étapes « a enregistré l'effet » et « a enregistré l'offset ».
2. Doublons : faites passer le même message 2 à 5 fois, assurez-vous d'avoir un seul effet.
3. Restarts et rebalance : arrêt/redémarrage des workers, vérification de l'absence de double traitement.
4. Flappy réseau : Temporisation au milieu de la transaction, répétition de commit.
5. Tests de charge : augmentation des files d'attente → s'il n'y a pas de dégradation à « toujours dans la transaction ».
Mini-modèles (pseudo)
Sink idempotent avec fixation offset
pseudo begin tx if not exists(select 1 from dedup where consumer_id=:c and op_id=:id)
then apply_effect(...) -- upsert / merge / add_one_time_action insert into dedup(c, id, applied_at) values(:c,:id, now)
end if update offsets set pos=:pos where consumer_id=:c commit
Commande avec version de l'unité
pseudo begin tx update account set balance = balance +:delta,
version = version + 1 where id=:account_id and version=:expected_version;
if row_count=0 then error CONCURRENT_MODIFICATION commit
Sécurité et conformité
PII/PCI dans les tables de déduplication : stockez un minimum, utilisez des jetons au lieu de données « brutes ».
Audit : Loger 'opération _ id', 'trace _ id', résultat (APPLIED/ALREADY_APPLIED).
Stratégie de stockage : TTL sur les tables de déduplication, archivage des offsets/logs.
Anti-modèles
« Livraison exactly-once » : tentative d'éliminer les prises au niveau du protocole de transport sans effet d'idempotence.
Transactions distribuées à l'échelle mondiale pour tout : XA/2PC à travers tous les services - fragile et lent.
Mélange de sous-produits non impotentes (par exemple, e-mail envoyé avant le commit offset).
Absence de clés d'opération : confiance dans l'unicité de la charge utile.
Chèque-liste de production
- Chaque effet critique a une clé idempotente.
- La position offset/lecture est fixée dans une seule transaction avec effet.
- Les tables de dédoublement sont indexées ; La TTL ≥ la rétraction de la loge.
- Pour les agrégats, une concurrence optimiste (version/sequence) est incluse.
- Les threads/topics sont lus en mode « seulement magique » (si disponible).
- Les tests de duplication et de chute sont présents dans CI/CD.
- Dashboards : proportion de répétitions, de transactions infructueuses, de temps de blocage, de pénalités.
- Documentation à l'intention des intégrateurs sur 'Idempotency-Keu '/répétitions/taimautams.
FAQ
Peut-on fournir un EOS sans transaction ?
Souvent oui - par l'idempotence sink's (upsert/merge) et la versionation des agrégats. Les transactions simplifient les garanties mais augmentent la valeur.
Tout le monde a besoin de « exactly-once » ?
Non. Il est cher. Appliquer ponctuellement là où aucune compensation n'est possible/route.
Comment lier des e-mails/webhooks à EOS ?
Tamponner la notification avant commit, envoyer après la fixation de l'effet ; stocker 'notification _ id'et rendre l'envoi idempotent.
Qu'est-ce qui est plus important - la livraison ou le traitement ?
Traitement. Les livraisons peuvent être répétées ; l'état final doit être correct et unique.
Total
Exactly-once parle de l'exactitude de l'effet, pas de l'absence de prises dans le câblage. Elle est obtenue par la combinaison de l'idempotence, de la fixation atomique de l'effet et du progrès de la lecture, du parti intelligent et de la discipline du versioning. Appliquez EOS lorsque le coût de l'erreur n'est pas acceptable et vérifiez sa réalité avec des tests de chute et de prise - pas par la foi dans le transport.