Garantir l'intégrité des données dans les systèmes de streaming distribués est l'un des aspects les plus complexes de l'architecture moderne. Bien qu'Apache Kafka fournisse par défaut une livraison « au moins une fois » et « au plus une fois », l'atteinte de la sémantique « exactement une fois » est essentielle pour les applications critiques telles que les transactions financières, la gestion des stocks et le traitement des données de santé. Dans ce guide, nous explorerons comment exploiter le cadre transactionnel de Kafka et les producteurs idempotents pour garantir que les messages sont traités exactement une fois, même en cas de pannes réseau ou de plantages d'application.
Comprendre le problème
Dans les systèmes distribués, les partitions réseau, les plantages des consommateurs et les nouvelles tentatives des producteurs peuvent entraîner des messages en double ou une perte de données. Sans mesures de protection appropriées, un consommateur pourrait traiter deux fois le même enregistrement, ce qui conduit à un état incohérent. Par exemple, si une application bancaire reçoit deux messages identiques « débiter 100 $ », le solde du compte de l'utilisateur sera incorrectement réduit de 200 $. La sémantique « exactement une fois » élimine cette ambiguïté en garantissant que chaque enregistrement est appliqué à l'état en aval exactement une fois.
Composants clés : Producteurs idempotents et transactions
Pour atteindre la sémantique « exactement une fois », vous avez besoin de deux mécanismes fondamentaux fonctionnant de concert :
- Producteur idempotent : Garantit que plusieurs instances du même message ne sont pas produites. Les brokers Kafka suivent les identifiants de producteur et les numéros de séquence pour dédoublonner les messages envoyés depuis la même session de producteur.
- Transactions : Permettent de regrouper plusieurs opérations de production et de consommation en une seule unité de travail atomique. Si une partie de la transaction échoue, l'ensemble de la transaction est annulé.
Mise en œuvre d'un producteur idempotent
D'abord, activez l'idempotence dans la configuration de votre producteur Kafka. Cela se fait en définissant enable.idempotence=true. Notez que lorsque l'idempotence est activée, acks doit être défini sur all pour garantir que les messages sont confirmés sur toutes les répliques synchrones avant d'être accusés de réception.
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
public class IdempotentProducerExample {
public static void main(String[] args) {
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// Activer l'idempotence
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
props.put(ProducerConfig.ACKS_CONFIG, "all");
KafkaProducer producer = new KafkaProducer<>(props);
// Envoyer des messages...
producer.close();
}
}
Utilisation des transactions pour un traitement « exactement une fois »
Pour un traitement « exactement une fois » de bout en bout, vous devez combiner un producteur idempotent avec un consommateur qui participe à la même transaction. La clé ici est d'utiliser la configuration transactional.id pour lier le groupe de consommateurs à une session transactionnelle spécifique.
Étape 1 : Configurer le producteur avec un identifiant transactionnel
props.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "my-transactional-id");
producer.initTransactions();
Étape 2 : Débuter une transaction, traiter les données et valider
producer.beginTransaction();
// Consommer les messages
ConsumerRecords records = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord record : records) {
// Traiter l'enregistrement (par ex., mettre à jour un système externe)
process(record);
// Produire le résultat vers un sujet en aval
producer.send(new ProducerRecord<>("output-topic", record.key(), record.value()));
}
// Valider la transaction de manière atomique
producer.commitTransaction();
// L'engagement des décalages est facultatif ici, car Kafka gère les décalages via le journal des transactions
consumer.commitSync();
Considérations pratiques
- Délais d'attente : Définissez des valeurs appropriées pour
transaction.timeout.msetsession.timeout.mspour éviter l'expiration prématurée des transactions. - Coordination du groupe de consommateurs : Le même
transactional.iddoit être utilisé pour toutes les instances de la paire producteur-consommateur dans un scénario de mise à l'échelle horizontale. Kafka garantit qu'une seule instance est active à la fois par identifiant transactionnel. - Systèmes en aval : La sémantique « exactement une fois » dans Kafka ne garantit que le message est écrit une seule fois dans Kafka. Si votre système en aval (par ex., une base de données) n'est pas idempotent, vous devez également implémenter une logique de dédoublonnage au niveau du consommateur.
Conclusion
La mise en œuvre de la sémantique « exactement une fois » dans Apache Kafka nécessite une combinaison soigneuse de producteurs idempotents, d'API transactionnelles et d'une configuration appropriée. En exploitant la prise en charge transactionnelle intégrée de Kafka, vous pouvez construire des applications de streaming robustes et tolérantes aux pannes qui maintiennent la cohérence des données même dans des environnements hautement distribués. Bien que la configuration soit plus complexe qu'une simple livraison « au moins une fois », la garantie d'intégrité des données en fait un investissement judicieux pour les systèmes critiques.