Genau einmal in Kafka

Genau einmal in Kafka verarbeitet

1. Überblick

In diesem Tutorial sehen wir uns an, wieKafka ensures exactly-once delivery between producer and consumer applications through the newly introduced Transactional API.

Darüber hinaus verwenden wir diese API, um Transaktionsproduzenten und -konsumenten zu implementieren, um in einem WordCount-Beispiel eine durchgängige, genau einmalige Lieferung zu erreichen.

2. Nachrichtenübermittlung in Kafka

Aufgrund verschiedener Fehler können Nachrichtensysteme die Nachrichtenübermittlung zwischen Hersteller- und Verbraucheranwendungen nicht garantieren. Abhängig davon, wie die Clientanwendungen mit solchen Systemen interagieren, ist die folgende Nachrichtensemantik möglich:

  • Wenn ein Nachrichtensystem niemals eine Nachricht dupliziert, aber gelegentlich eine Nachricht verpasst, nennen wir dasat-most-once

  • Oder wenn es nie eine Nachricht verpasst, aber die gelegentliche Nachricht dupliziert, nennen wir esat-least-once

  • Wenn jedoch immer alle Nachrichten ohne Duplizierung zugestellt werden, ist diesexactly-once

Ursprünglich unterstützte Kafka nur die Zustellung von Nachrichten (höchstens einmal und mindestens einmal).

the introduction of Transactions between Kafka brokers and client applications ensures exactly-once delivery in Kafka. Lassen Sie uns zum besseren Verständnis die Transaktionsclient-API schnell überprüfen.

3. Maven-Abhängigkeiten

Um mit der Transaktions-API arbeiten zu können, benötigen wirKafka’s Java client in unserem pom:


    org.apache.kafka
    kafka-clients
    2.0.0

4. A Transaktionsconsume-transform-produce Schleife

In unserem Beispiel werden Nachrichten aus einem Eingabethema,sentences, verwendet.

Dann zählen wir für jeden Satz jedes Wort und senden die einzelnen Wortzahlen an ein Ausgabethema,counts.

In diesem Beispiel wird davon ausgegangen, dass im Themasentencesbereits Transaktionsdaten verfügbar sind.

4.1. Ein transaktionsbewusster Produzent

Fügen wir also zuerst einen typischen Kafka-Produzenten hinzu.

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

Zusätzlich müssen wir jedochtransactional.id angeben undidempotence aktivieren:

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

KafkaProducer producer = new KafkaProducer(producerProps);

Da wir die Idempotenz aktiviert haben, verwendet Kafka diese Transaktions-ID als Teil seines Algorithmus fürdeduplicate any message this producersends, um die Idempotenz sicherzustellen.

Einfach ausgedrückt, wenn der Produzent versehentlich dieselbe Nachricht mehr als einmal an Kafka sendet, können Sie diese Einstellungen verwenden, um sie zu bemerken.

Alles, was wir tun müssen, istmake sure the transaction id is distinct for each producer, obwohl es über Neustarts hinweg konsistent ist.

4.2. Aktivieren des Produzenten für Transaktionen

Sobald wir bereit sind, müssen wir auchinitTransaction aufrufen, um den Produzenten auf die Verwendung von Transaktionen vorzubereiten:

producer.initTransactions();

Dies registriert den Produzenten beim Broker als einen, der Transaktionen verwenden kann,identifying it by its transactional.id and a sequence number, or epoch. Der Broker verwendet diese wiederum, um alle Aktionen in ein Transaktionsprotokoll zu schreiben.

Infolgedessen (the broker will remove any actions from that log that belong to a producer with the same transaction id and earlierepoch, wird davon ausgegangen, dass sie aus nicht mehr existierenden Transaktionen stammen.

4.3. Ein transaktionsbewusster Verbraucher

Wenn wir verbrauchen, können wir alle Nachrichten auf einer Themenpartition der Reihe nach lesen. 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”));

Durch die Verwendung eines Werts vonread_committed wird sichergestellt, dass vor Abschluss der Transaktion keine Transaktionsnachrichten gelesen werden.

Der Standardwert vonisolation.level istread_uncommitted.

4.4. Konsumieren und Transformieren durch Transaktion

Nachdem sowohl Produzent als auch Consumer für das Schreiben und Lesen von Transaktionen konfiguriert sind, können wir Datensätze aus unserem Eingabethema verwenden und jedes Wort in jedem Datensatz zählen:

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));

Beachten Sie, dass der obige Code nichts Transaktionales enthält. Abersince 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.

Jetzt können wir die berechnete Wortzahl an das Ausgabethema senden.

Mal sehen, wie wir unsere Ergebnisse auch transaktional erzielen können.

4.5. API senden

Um unsere Zählungen als neue Nachrichten zu senden, rufen wir in derselben TransaktionbeginTransaction auf:

producer.beginTransaction();

Dann können wir jeden einzelnen in unser Thema "count" schreiben, wobei der Schlüssel das Wort und der count der Wert ist:

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

Da der Produzent die Daten nach dem Schlüssel partitionieren kann, bedeutet dies, dasstransactional messages can span multiple partitions, each being read by separate consumers.. Daher speichert der Kafka-Broker eine Liste aller aktualisierten Partitionen für eine Transaktion.

Beachten Sie auch, dasswithin a transaction, a producer can use multiple threads to send records in parallel.

4.6. Offsets festschreiben

Und schließlich müssen wir unsere Offsets festschreiben, die wir gerade verbraucht haben. With transactions, we commit the offsets back to the input topic we read them from, like normal. Auchwe send them to the producer’s transaction.

All dies können wir in einem einzigen Aufruf erledigen, aber wir müssen zuerst die Offsets für jede Themenpartition berechnen:

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));
}

Beachten Sie, dass wir für die Transaktion den bevorstehenden Offset festlegen, was bedeutet, dass wir 1 hinzufügen müssen.

Dann können wir unsere berechneten Offsets an die Transaktion senden:

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

4.7. Festschreiben oder Abbrechen der Transaktion

Und schließlich können wir die Transaktion festschreiben, die die Offsets atomar sowohl in denconsumer_offsets -Stopp als auch in die Transaktion selbst schreibt:

producer.commitTransaction();

Dadurch werden alle gepufferten Nachrichten an die entsprechenden Partitionen gesendet. Darüber hinaus stellt der Kafka-Broker den Verbrauchern alle Nachrichten in dieser Transaktion zur Verfügung.

Wenn während der Verarbeitung etwas schief geht, z. B. wenn wir eine Ausnahme abfangen, können wir natürlichabortTransaction: aufrufen

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

Löschen Sie alle gepufferten Nachrichten und entfernen Sie die Transaktion vom Broker.

If we neither commit nor abort before the broker-configured max.transaction.timeout.ms, the Kafka broker will abort the transaction itself. Der Standardwert für diese Eigenschaft beträgt 900.000 Millisekunden oder 15 Minuten.

5. Andereconsume-transform-produce Schleifen

Was wir gerade gesehen haben, ist eine einfacheconsume-transform-produce-Schleife, die in denselben Kafka-Cluster liest und schreibt.

Umgekehrt istapplications that must read and write to different Kafka clusters must use the older commitSync and commitAsync API. In der Regel speichern Anwendungen Consumer-Offsets in ihrem externen Zustandsspeicher, um die Transaktionsfähigkeit aufrechtzuerhalten.

6. Fazit

Bei datenkritischen Anwendungen ist häufig eine End-to-End-Verarbeitung genau einmal erforderlich.

In diesem Tutorial habenwe saw how we use Kafka to do exactly this, using transactions und wir ein transaktionsbasiertes Beispiel für die Wortzählung implementiert, um das Prinzip zu veranschaulichen.

Fühlen Sie sich frei, allecode samples on GitHub zu überprüfen.