Exactly-Once Semantics
Comment garantir qu'un évènement est traité exactement une fois — pas zéro, pas deux — dans un pipeline streaming distribué qui peut redémarrer à tout moment.
Problème
Dans un pipeline streaming distribué, un worker peut crasher en plein traitement, un broker Kafka peut redistribuer la partition, une fenêtre peut s'émettre deux fois après recovery. Le résultat naturel est at-least-once : chaque évènement est traité au moins une fois, possiblement plusieurs. Pour la plupart des cas (KPI dashboard, anomaly detection), c'est suffisant car les agrégats sont idempotents (un count peut être recalculé). Mais pour des cas où chaque évènement modifie un état externe non-idempotent (envoyer une notification, écrire un règlement bancaire, faire un appel d'API à l'administration fiscale), le double traitement est un bug applicatif majeur. Comment éliminer cette duplication ?
Forces
- Coût en latence : exactly-once exige des coordinations supplémentaires (checkpoint, two-phase commit).
- Coût en throughput : Kafka EOS divise le throughput par 1.5-3 vs at-least-once.
- Frontière du sink : EOS interne au moteur est plus simple ; EOS de bout en bout exige idempotence ou 2PC du sink.
- FLP impossibility : exactly-once parfait est impossible théoriquement dans un système asynchrone ; en pratique on combine plusieurs mécanismes.
- Confusion marketing : Kafka 0.11 a annoncé « exactly-once » avec une définition précise (atomicité de write Kafka) qui n'est pas toujours celle attendue.
Solution
Trois mécanismes combinés. (1) Idempotent producer : chaque producer Kafka envoie un sequence number par partition ; le broker dédoublonne automatiquement (Kafka 0.11+, configuration enable.idempotence=true). (2) Transactions Kafka : un producer peut wrapper plusieurs writes dans une transaction atomique commit/abort ; les consumers en mode read_committed ne voient que les transactions committed. (3) Chandy-Lamport snapshots : Flink utilise un algorithme de checkpoint distribué (papier 1985) — injecte des barriers dans le flux, chaque opérateur sauvegarde son state à la traversée d'une barrier, et le state global cohérent est reconstructible. Sur recovery, Flink rejoue à partir du dernier checkpoint cohérent. (4) Sink 2PC : pour un sink externe (PostgreSQL, S3), un connecteur 2PC garantit que le commit Kafka et le commit sink sont atomiquement liés.
Structure
Kafka source ──► Flink job ────► Kafka sink ──► External sink (DB)
│
│ Every N seconds:
│ inject CHECKPOINT barrier into stream
│
Operator A ──► [state A]─[barrier]─► Operator B ──► [state B]
│ │
│ snapshot state A │ snapshot state B
│ to durable storage │ to durable storage
▼ ▼
Checkpoint metadata committed (atomic)
On failure:
Flink restarts from last checkpoint
Source replays from offset stored in checkpoint
All operators restore state from snapshot
Sink: if 2PC, only committed transactions visible
→ No double effect from work between barrier and crash Implémentation EDI
Exactly-once est requis pour plusieurs flux EDI critiques. (1) Rapports fiscaux DRR (ViDA, juillet 2030) : chaque facture B2B intra-UE doit être déclarée exactement une fois à l'administration ; le pipeline Flink → connecteur API fiscal doit éliminer toute duplication même après recovery. (2) Génération automatique d'INVOIC depuis un flux d'ORDERS : double génération = double facturation = litige client. (3) Notification de réception au partenaire : double notification = double comptabilisation chez le partenaire. Implémentation type : Kafka source en mode read_committed → Flink avec EOS checkpoint toutes les 30 secondes → sink connecteur transactional (TwoPhaseCommitSinkFunction pour PostgreSQL fiscal store + Kafka sink pour propagation aval). Toujours combiner avec idempotence applicative au sink — c'est la défense en profondeur : si EOS échoue, idempotence rattrape.
Anti-patterns
- Croire que EOS interne au moteur = EOS de bout en bout — sans sink transactionnel, on perd la garantie au commit.
- Désactiver les checkpoints pour gagner en throughput — perte de toute garantie EOS.
- Sink HTTP non-idempotent sans déduplication par clé — double notification garantie sur recovery.
- Checkpoint interval trop long — gros volume de réplay sur recovery.
- Confondre EOS Kafka (atomic write batch) et EOS applicatif (effet métier unique) — Kafka garantit l'atomicité, pas que l'effet métier ne se produise qu'une fois.
Patterns liés
- Idempotence — la défense ultime au sink.
- At-Least-Once — la sémantique par défaut sans EOS.
- Two-Phase Commit — utilisé pour la coordination sink.
- Transactional Outbox — alternative côté producteur.
Sources
- Carbone P. et al. — State Management in Apache Flink: Consistent Stateful Distributed Stream Processing, VLDB 2017. vldb.org
- Confluent — Exactly-Once Semantics Are Possible: Here's How Kafka Does It, 2017. confluent.io
- Chandy K., Lamport L. — Distributed Snapshots: Determining Global States of Distributed Systems, ACM TOCS 1985.
- Kreps J. — Exactly Once Delivery and Transactional Messaging in Apache Kafka, KIP-98 design doc. cwiki.apache.org
- Akidau, Chernyak, Lax — Streaming Systems, O'Reilly 2018, chap. 5.