Exactly-Once Semantics dans Kafka
Les garanties de livraison sont cruciales dans les systèmes distribués. Ce guide explique les trois niveaux de garantie et comment atteindre l'exactly-once avec Kafka.
Les Trois Garanties de Livraison
At-Most-Once (Au plus une fois)
Le message est envoyé une fois, sans retry. S'il est perdu, il est perdu.
Producer ──► Broker ──► Consumer
│
└─► ❌ Si échec, pas de retry
Message potentiellement perdu
Configuration :
props.put("acks", "0");
props.put("retries", 0);
Usage : Logs non critiques, métriques où la perte est acceptable.
At-Least-Once (Au moins une fois)
Le message est envoyé jusqu'à confirmation. Peut créer des doublons.
Producer ──► Broker ──► Consumer
│ │
│ timeout │
│◄─────────┘
│
└──► Broker (retry) ← Message déjà écrit = DOUBLON
Configuration :
props.put("acks", "all");
props.put("retries", Integer.MAX_VALUE);
props.put("enable.idempotence", "false");
Usage : La plupart des cas où les doublons peuvent être gérés côté consumer.
Exactly-Once (Exactement une fois)
Le message est traité exactement une fois, même en cas de retry ou de crash.
Producer ──► Broker ──► Consumer
│ │
│ timeout │
│◄─────────┘
│
└──► Broker (retry avec PID+sequence)
│
└─► Détecte le doublon, ignore
Configuration :
props.put("acks", "all");
props.put("enable.idempotence", "true");
Usage : Transactions financières, comptages, opérations critiques.
Idempotence : La Base de l'Exactly-Once
Comment ça Marche
Kafka assigne à chaque producer un Producer ID (PID) et un sequence number par partition :
┌──────────────────────────────────────┐
│ Message Header │
├──────────────────────────────────────┤
│ Producer ID (PID): 12345 │
│ Epoch: 0 │
│ Sequence Number: 42 │
│ Partition: 0 │
├──────────────────────────────────────┤
│ Key: order-123 │
│ Value: {"amount": 100} │
└──────────────────────────────────────┘
Le broker maintient le dernier sequence number par (PID, Partition). Si un message arrive avec un sequence déjà vu → rejeté comme doublon.
Activer l'Idempotence
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
// Activer l'idempotence
props.put("enable.idempotence", "true");
// Ces configs sont automatiquement définies avec idempotence:
// acks=all
// retries=Integer.MAX_VALUE
// max.in.flight.requests.per.connection=5
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
Limites de l'Idempotence
L'idempotence garantit l'exactly-once pour une seule partition :
Partition 0: msg1, msg2, msg3 ✅ Pas de doublon
Partition 1: msg4, msg5, msg6 ✅ Pas de doublon
MAIS cross-partition non garanti sans transactions
Transactions : Exactly-Once Multi-Partition
Le Problème
Sans transactions, une opération multi-partition peut être partiellement complétée :
1. Écrire dans Partition 0 ✅
2. Écrire dans Partition 1 ✅
3. Écrire dans Partition 2 ❌ Crash!
Résultat: État incohérent
La Solution : Transactions
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("enable.idempotence", "true");
props.put("transactional.id", "my-transactional-producer"); // Requis!
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
// Initialisation (une seule fois)
producer.initTransactions();
try {
// Démarrer la transaction
producer.beginTransaction();
// Envoyer plusieurs messages
producer.send(new ProducerRecord<>("topic-a", "key1", "value1"));
producer.send(new ProducerRecord<>("topic-b", "key2", "value2"));
producer.send(new ProducerRecord<>("topic-c", "key3", "value3"));
// Commit atomique
producer.commitTransaction();
} catch (Exception e) {
// Abort si erreur
producer.abortTransaction();
}
Isolation des Transactions
Les consumers peuvent choisir leur niveau d'isolation :
// Consumer config
props.put("isolation.level", "read_committed"); // Voir seulement les transactions committées
// OU
props.put("isolation.level", "read_uncommitted"); // Voir tous les messages (défaut)
Timeline:
─────────────────────────────────────────────►
│ msg1 │ msg2 │ msg3 │ COMMIT │ msg4 │
read_uncommitted: voit msg1, msg2, msg3, msg4
read_committed: voit msg1, msg2, msg3 seulement après COMMIT
Pattern Read-Process-Write
Le cas d'usage classique : lire d'un topic, traiter, écrire dans un autre topic.
Sans Exactly-Once (Risque de doublon)
1. Consumer lit message
2. Traitement
3. Producer écrit résultat
4. Consumer commit offset ← Crash ici = message retraité
Avec Exactly-Once
// Producer transactionnel
props.put("transactional.id", "process-orders");
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
producer.initTransactions();
// Consumer
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(consumerProps);
consumer.subscribe(Collections.singletonList("input-topic"));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
if (!records.isEmpty()) {
producer.beginTransaction();
try {
for (ConsumerRecord<String, String> record : records) {
// Traitement
String result = process(record.value());
// Écrire le résultat
producer.send(new ProducerRecord<>("output-topic", record.key(), result));
}
// Commit offset DANS la transaction
Map<TopicPartition, OffsetAndMetadata> offsets = new HashMap<>();
for (TopicPartition partition : records.partitions()) {
List<ConsumerRecord<String, String>> partitionRecords = records.records(partition);
long lastOffset = partitionRecords.get(partitionRecords.size() - 1).offset();
offsets.put(partition, new OffsetAndMetadata(lastOffset + 1));
}
producer.sendOffsetsToTransaction(offsets, consumer.groupMetadata());
// Commit atomique: messages + offsets
producer.commitTransaction();
} catch (Exception e) {
producer.abortTransaction();
}
}
}
Kafka Streams : Exactly-Once Simplifié
Kafka Streams gère automatiquement l'exactly-once :
Properties props = new Properties();
props.put(StreamsConfig.APPLICATION_ID_CONFIG, "my-streams-app");
props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
// Activer exactly-once
props.put(StreamsConfig.PROCESSING_GUARANTEE_CONFIG, StreamsConfig.EXACTLY_ONCE_V2);
StreamsBuilder builder = new StreamsBuilder();
builder.stream("input-topic")
.mapValues(value -> process(value))
.to("output-topic");
KafkaStreams streams = new KafkaStreams(builder.build(), props);
streams.start();
Avec EXACTLY_ONCE_V2, Kafka Streams :
- Utilise des transactions automatiquement
- Gère les offsets dans les transactions
- Gère les crashes et recovery
Comparaison des Garanties
| Aspect | At-Most-Once | At-Least-Once | Exactly-Once |
|---|---|---|---|
| Perte de message | Possible | Non | Non |
| Doublons | Non | Possible | Non |
| Performance | ⚡ Maximum | 🔶 Bonne | 🔴 Plus lente |
| Complexité | Simple | Simple | Plus complexe |
| Coût broker | Faible | Moyen | Plus élevé |
Quand Utiliser Chaque Garantie
At-Most-Once
- Logs applicatifs
- Métriques non critiques
- Données remplaçables
At-Least-Once
- La plupart des cas d'usage
- Consumer idempotent (peut gérer les doublons)
- Event sourcing avec déduplication
Exactly-Once
- Transactions financières
- Comptages et agrégations
- Données de facturation
- Systèmes de paiement
Impact sur les Performances
L'exactly-once a un coût :
Latence (P99):
At-least-once: ~10ms
Exactly-once: ~50ms (5x plus lent)
Throughput:
At-least-once: 100,000 msg/s
Exactly-once: 20,000 msg/s (5x moins)
Conseil : N'utilisez l'exactly-once que quand c'est vraiment nécessaire. Pour la plupart des cas, un consumer idempotent avec at-least-once suffit.
Résumé
| Garantie | Configuration | Usage |
|---|---|---|
| At-most-once | acks=0, retries=0 |
Logs non critiques |
| At-least-once | acks=all, retries=MAX |
Cas standard |
| Exactly-once | enable.idempotence=true |
Single partition |
| EOS transactions | transactional.id |
Multi-partition |
| Kafka Streams | EXACTLY_ONCE_V2 |
Stream processing |
Fin de la Série Fondamentaux
Vous avez maintenant les bases pour comprendre et utiliser Kafka efficacement :
- Qu'est-ce que Kafka
- Architecture
- Producers & Consumers
- Consumer Groups
- Réplication
- Exactly-Once (cet article)
→ Prochaine étape : Articles avancés Kafka