traitement unique à Kafka

Exactement une fois en traitement à Kafka

1. Vue d'ensemble

Dans ce didacticiel, nous verrons commentKafka ensures exactly-once delivery between producer and consumer applications through the newly introduced Transactional API.

En outre, nous utiliserons cette API pour implémenter des producteurs et des consommateurs transactionnels afin d'obtenir une livraison de bout en bout une seule fois dans un exemple WordCount.

2. Livraison de messages à Kafka

En raison de diverses défaillances, les systèmes de messagerie ne peuvent pas garantir la livraison des messages entre les applications de production et de consommation. Selon la manière dont les applications clientes interagissent avec ces systèmes, la sémantique suivante du message est possible:

  • Si un système de messagerie ne duplique jamais un message mais risque de manquer le message occasionnel, nous appelons celaat-most-once

  • Ou, s'il ne manquera jamais un message mais pourrait dupliquer le message occasionnel, nous l'appelonsat-least-once

  • Mais, s'il délivre toujours tous les messages sans duplication, c'estexactly-once

À l’origine, Kafka ne prenait en charge que la remise de messages au plus une fois et au moins une fois.

Cependant,the introduction of Transactions between Kafka brokers and client applications ensures exactly-once delivery in Kafka. Pour mieux le comprendre, examinons rapidement l'API du client transactionnel.

3. Dépendances Maven

Pour travailler avec l'API de transaction, nous aurons besoin deKafka’s Java client dans notre pom:


    org.apache.kafka
    kafka-clients
    2.0.0

4. Une boucle transactionnelleconsume-transform-produce

Pour notre exemple, nous allons consommer les messages d'un sujet d'entrée,sentences.

Ensuite, pour chaque phrase, nous compterons chaque mot et enverrons le nombre de mots individuels à un sujet de sortie,counts.

Dans l'exemple, nous supposerons qu'il existe déjà des données transactionnelles disponibles dans la rubriquesentences.

4.1. Un producteur soucieux des transactions

Alors, ajoutons d'abord un producteur Kafka typique.

Properties producerProps = new Properties();
producerProps.put("bootstrap.servers", "localhost:9092");

De plus, cependant, nous devons spécifier untransactional.id et activeridempotence:

producerProps.put("enable.idempotence", "true");
producerProps.put("transactional.id", "prod-1");

KafkaProducer producer = new KafkaProducer(producerProps);

Parce que nous avons activé l'idempotence, Kafka utilisera cet identifiant de transaction dans le cadre de son algorithme pourdeduplicate any message this producersends, garantissant l'idempotence.

En termes simples, si le producteur envoie accidentellement le même message à Kafka plusieurs fois, ces paramètres lui permettent de le remarquer.

Tout ce que nous devons faire estmake sure the transaction id is distinct for each producer, bien que cohérent à travers les redémarrages.

4.2. Activation du producteur pour les transactions

Une fois que nous sommes prêts, nous devons également appelerinitTransaction pour préparer le producteur à utiliser les transactions:

producer.initTransactions();

Cela enregistre le producteur auprès du courtier comme celui qui peut utiliser des transactions,identifying it by its transactional.id and a sequence number, or epoch. À son tour, le courtier les utilisera pour consigner par avance toutes les actions dans un journal des transactions.

Et par conséquent,the broker will remove any actions from that log that belong to a producer with the same transaction id and earlierepoch, les supposant être issus de transactions obsolètes.

4.3. Un consommateur soucieux des transactions

Lorsque nous consommons, nous pouvons lire tous les messages d'une partition de sujet dans l'ordre. Cependant,we can indicate with isolation.level that we should wait to read transactional messages until the associated transaction has been committed:

Properties consumerProps = new Properties();
consumerProps.put("bootstrap.servers", "localhost:9092");
consumerProps.put("group.id", "my-group-id");
consumerProps.put("enable.auto.commit", "false");
consumerProps.put("isolation.level", "read_committed");
KafkaConsumer consumer = new KafkaConsumer<>(consumerProps);
consumer.subscribe(singleton(“sentences”));

L'utilisation d'une valeur deread_committed garantit que nous ne lirons aucun message transactionnel avant la fin de la transaction.

La valeur par défaut deisolation.level estread_uncommitted.

4.4. Consommer et transformer par transaction

Maintenant que le producteur et le consommateur sont tous deux configurés pour écrire et lire de manière transactionnelle, nous pouvons consommer des enregistrements de notre sujet d'entrée et compter chaque mot de chaque enregistrement:

ConsumerRecords records = consumer.poll(ofSeconds(60));
Map wordCountMap =
  records.records(new TopicPartition("input", 0))
    .stream()
    .flatMap(record -> Stream.of(record.value().split(" ")))
    .map(word -> Tuple.of(word, 1))
    .collect(Collectors.toMap(tuple ->
      tuple.getKey(), t1 -> t1.getValue(), (v1, v2) -> v1 + v2));

Notez que le code ci-dessus n’a rien de transactionnel. Mais,since we used read_committed, it means that no messages that were written to the input topic in the same transaction will be read by this consumer until they are all written.

Maintenant, nous pouvons envoyer le nombre de mots calculé au sujet de sortie.

Voyons comment nous pouvons produire nos résultats, également de manière transactionnelle.

4.5. Envoyer l'API

Pour envoyer nos comptes comme de nouveaux messages, mais dans la même transaction, nous appelonsbeginTransaction:

producer.beginTransaction();

Ensuite, nous pouvons écrire chacun dans notre sujet "comptes" avec la clé étant le mot et le compte étant la valeur:

wordCountMap.forEach((key,value) ->
    producer.send(new ProducerRecord("counts",key,value.toString())));

Notez que puisque le producteur peut partitionner les données par la clé, cela signifie quetransactional messages can span multiple partitions, each being read by separate consumers. Par conséquent, le courtier Kafka stockera une liste de toutes les partitions mises à jour pour une transaction.

Notez également que,within a transaction, a producer can use multiple threads to send records in parallel.

4.6. Commettre des compensations

Et enfin, nous devons engager nos compensations que nous venons de finir de consommer. With transactions, we commit the offsets back to the input topic we read them from, like normal. De plus,we send them to the producer’s transaction.

Nous pouvons faire tout cela en un seul appel, mais nous devons d'abord calculer les décalages pour chaque partition de sujet:

Map offsetsToCommit = new HashMap<>();
for (TopicPartition partition : records.partitions()) {
    List> partitionedRecords = records.records(partition);
    long offset = partitionedRecords.get(partitionedRecords.size() - 1).offset();
    offsetsToCommit.put(partition, new OffsetAndMetadata(offset + 1));
}

Notez que ce que nous nous engageons dans la transaction est le prochain décalage, ce qui signifie que nous devons ajouter 1.

Ensuite, nous pouvons envoyer nos compensations calculées à la transaction:

producer.sendOffsetsToTransaction(offsetsToCommit, "my-group-id");

4.7. Validation ou annulation de la transaction

Et, enfin, nous pouvons valider la transaction, qui écrira atomiquement les offsets dans le stopicconsumer_offsets ainsi que dans la transaction elle-même:

producer.commitTransaction();

Cela vide tout message mis en mémoire tampon dans les partitions respectives. En outre, le courtier Kafka met tous les messages de cette transaction à la disposition des consommateurs.

Bien sûr, si quelque chose ne va pas pendant le traitement, par exemple, si nous détectons une exception, nous pouvons appelerabortTransaction:

try {
  // ... read from input topic
  // ... transform
  // ... write to output topic
  producer.commitTransaction();
} catch ( Exception e ) {
  producer.abortTransaction();
}

Et supprimez tous les messages mis en mémoire tampon et supprimez la transaction du courtier.

If we neither commit nor abort before the broker-configured max.transaction.timeout.ms, the Kafka broker will abort the transaction itself. La valeur par défaut de cette propriété est 900 000 millisecondes ou 15 minutes.

5. Autres bouclesconsume-transform-produce

Ce que nous venons de voir est une boucle de baseconsume-transform-produce qui lit et écrit dans le même cluster Kafka.

Inversement,applications that must read and write to different Kafka clusters must use the older commitSync and commitAsync API. Généralement, les applications stockent les compensations des consommateurs dans leur stockage d’état externe afin de maintenir la transactionnalité.

6. Conclusion

Pour les applications critiques en matière de données, un traitement de bout en bout précis est souvent impératif.

Dans ce didacticiel,we saw how we use Kafka to do exactly this, using transactions et nous avons implémenté un exemple de comptage de mots basé sur les transactions pour illustrer le principe.

N'hésitez pas à consulter tous lescode samples on GitHub.