Démystification de la sémantique Exactly-Once de Kafka : Un guide complet
Comprenez la sémantique exactly-once de Kafka avec les producteurs idempotents, les transactions, les consommateurs read_committed et les commits d'offset.
Démystifier la sémantique Exactly-Once de Kafka : Un guide complet
La sémantique exactly-once de Kafka peut protéger un pipeline de traitement de flux contre les enregistrements de sortie en double lorsque les producteurs réessayent, que les brokers basculent ou qu'une application redémarre. La garantie est puissante, mais elle est plus étroite que ce que l'expression laisse entendre : Kafka peut rendre transactionnelles les écritures Kafka et les offsets consommés. Il ne peut pas automatiquement rendre votre base de données externe, votre passerelle de paiement ou votre API HTTP exactly-once.
Utilisez la sémantique exactly-once lorsque les sorties en double seraient coûteuses ou difficiles à nettoyer, comme les ajustements de stock, les événements de solde de compte ou les sujets d'état dérivés consommés par d'autres services.
Garanties de livraison en termes simples
Les applications Kafka parlent généralement de trois modèles de livraison.
- At-most-once : Votre application peut perdre des enregistrements, mais elle ne doit pas traiter le même enregistrement deux fois. Cela peut se produire lorsque les offsets sont commités avant la fin du traitement.
- At-least-once : Votre application ne doit pas perdre d'enregistrements, mais elle peut traiter un enregistrement plus d'une fois après une nouvelle tentative ou un redémarrage.
- Exactly-once : Une boucle de lecture-traitement-écriture Kafka commit ses enregistrements de sortie et ses offsets consommés comme une seule transaction.
Le dernier point est la clé. La sémantique exactly-once est la plus forte lorsque l'application lit depuis Kafka, écrit les résultats dans Kafka et commit les offsets dans la même transaction.
Producteurs idempotents
Un producteur idempotent empêche les écritures en double causées par les nouvelles tentatives du producteur. Kafka attribue un ID au producteur et suit les numéros de séquence pour chaque producteur et partition. Si le broker a déjà accepté un lot et reçoit ensuite la nouvelle tentative, il peut rejeter le doublon au lieu de l'ajouter à nouveau.
Pour les clients Kafka actuels, l'idempotence est activée par défaut lorsque vous ne configurez pas de paramètres de producteur conflictuels. Vous pouvez toujours la définir explicitement :
enable.idempotence=true
acks=all
acks=all signifie que le leader attend tous les réplicas synchronisés avant d'accuser réception de l'écriture. L'idempotence dépend également des paramètres de nouvelle tentative et de requêtes en vol compatibles, alors évitez de remplacer les paramètres de fiabilité du producteur sauf si vous connaissez l'effet dans votre version client.
L'idempotence protège les nouvelles tentatives du producteur, mais elle ne rend pas atomique un flux de traitement complet. Si votre application consomme depuis un sujet et produit vers un autre, vous avez besoin de transactions pour lier la sortie et le commit d'offset ensemble.
Transactions Kafka
Les transactions permettent à un groupe de producteurs de regrouper plusieurs écritures dans une unité atomique. Le producteur a besoin d'un transactional.id stable.
transactional.id=inventory-adjuster-0
enable.idempotence=true
acks=all
Un flux de transaction typique est :
- Initialiser les transactions au démarrage de l'application.
- Commencer une transaction.
- Consommer des enregistrements du sujet d'entrée.
- Produire des enregistrements de sortie.
- Envoyer les offsets consommés à la transaction.
- Commiter la transaction, ou l'annuler en cas d'échec.
Si le processus plante avant le commit, Kafka n'expose pas la sortie non commitée aux consommateurs read_committed. Au redémarrage, l'application peut relire les mêmes enregistrements d'entrée et produire un résultat commité.
Paramètres consommateur importants
Les consommateurs qui lisent la sortie transactionnelle doivent utiliser :
isolation.level=read_committed
enable.auto.commit=false
read_committed cache les enregistrements des transactions annulées. enable.auto.commit=false empêche le consommateur de commiter les offsets en dehors de la transaction.
Le nom du paramètre est important. Le paramètre consommateur de Kafka est enable.auto.commit, pas auto.commit.enable.
Pour une application manuelle consommateur-producteur, le commit d'offset doit faire partie de la transaction du producteur. Dans le client Java, cela signifie utiliser les API de producteur transactionnel, y compris l'envoi des offsets à la transaction avant de la commiter.
Un scénario concret
Imaginez un sujet orders et un sujet de sortie inventory-events. Votre service lit une commande, vérifie le SKU et écrit un événement de déduction de stock.
Sans transactions, un plantage après l'écriture de la sortie mais avant le commit de l'offset d'entrée peut créer une déduction en double après le redémarrage. Avec les transactions, l'événement de sortie et le commit d'offset d'entrée réussissent ou échouent ensemble. Un redémarrage peut relire la commande, mais un seul événement de stock commité devient visible pour les consommateurs read_committed en aval.
Limites à garder à l'esprit
La sémantique exactly-once de Kafka ne couvre pas les effets secondaires en dehors de Kafka, sauf si vous les concevez pour cela. Si le même service écrit également dans PostgreSQL ou appelle une API de facturation, cet effet secondaire externe a besoin de sa propre clé d'idempotence, contrainte unique, stratégie de transaction ou modèle de boîte d'envoi.
Les transactions ajoutent également une surcharge de coordination. Pour une simple ingestion de journaux où les doublons sont acceptables, les producteurs idempotents plus les consommateurs at-least-once peuvent suffire.
Liste de contrôle pratique
Utilisez un transactional.id stable par instance d'application ou tâche. Ne laissez pas deux producteurs actifs utiliser le même ID transactionnel en même temps.
Définissez les consommateurs de sortie transactionnelle sur read_committed. Désactivez les commits d'offset automatiques dans les boucles de traitement transactionnel.
Gardez les transactions courtes. Les transactions volumineuses peuvent augmenter la latence et ralentir la récupération.
Traitez les systèmes externes séparément. Kafka peut protéger l'état de Kafka, mais vos écritures en base de données ont toujours besoin d'une conception idempotente.
Le point à retenir utile : la sémantique exactly-once n'est pas un interrupteur magique. Ce sont un ensemble de choix de producteur, consommateur et transaction qui fonctionnent le mieux pour le traitement de flux Kafka-à-Kafka.