Ровно однажды обработка в Кафке

Ровно однажды обработка в Кафке

1. обзор

В этом руководстве мы рассмотрим, какKafka ensures exactly-once delivery between producer and consumer applications through the newly introduced Transactional API.

Кроме того, мы будем использовать этот API для реализации транзакционных производителей и потребителей, чтобы добиться сквозной одноразовой доставки в примере WordCount.

2. Доставка сообщений в Кафке

Из-за различных сбоев системы обмена сообщениями не могут гарантировать доставку сообщений между приложениями производителя и потребителя. В зависимости от того, как клиентские приложения взаимодействуют с такими системами, возможна следующая семантика сообщений:

  • Если система обмена сообщениями никогда не будет дублировать сообщение, но может пропустить случайное сообщение, мы называем этоat-most-once

  • Или, если он никогда не пропустит сообщение, но может дублировать случайное сообщение, мы называем егоat-least-once

  • Но если он всегда доставляет все сообщения без дублирования, то этоexactly-once

Первоначально Kafka поддерживал доставку сообщений только один раз и как минимум один раз.

Однакоthe introduction of Transactions between Kafka brokers and client applications ensures exactly-once delivery in Kafka. Чтобы лучше понять это, давайте быстро рассмотрим API транзакционного клиента.

3. Maven Зависимости

Для работы с API транзакций нам понадобитсяKafka’s Java client в нашем pom:


    org.apache.kafka
    kafka-clients
    2.0.0

4. Транзакционный циклconsume-transform-produce

В нашем примере мы собираемся получать сообщения из входной темыsentences.

Затем для каждого предложения мы подсчитываем каждое слово и отправляем индивидуальное количество слов в тему вывода,counts.

В этом примере мы предположим, что в темеsentences уже есть данные о транзакциях.

4.1. Производитель с учетом транзакций

Итак, давайте сначала добавим типичного продюсера Kafka.

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

Кроме того, нам нужно указатьtransactional.id и включитьidempotence:

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

KafkaProducer producer = new KafkaProducer(producerProps);

Поскольку мы включили идемпотентность, Kafka будет использовать этот идентификатор транзакции как часть своего алгоритма дляdeduplicate any message this producersends, обеспечивая идемпотентность.

Проще говоря, если производитель случайно отправляет одно и то же сообщение в Kafka более одного раза, эти настройки позволяют ему это заметить.

Все, что нам нужно сделать, этоmake sure the transaction id is distinct for each producer, хотя и согласованно при перезапусках.

4.2. Включение производителя для транзакций

Когда мы будем готовы, нам также нужно вызватьinitTransaction to, чтобы подготовить производителя к использованию транзакций:

producer.initTransactions();

Это регистрирует производителя у брокера как того, кто может использовать транзакции,identifying it by its transactional.id and a sequence number, or epoch. В свою очередь, брокер будет использовать их для записи любых действий в журнал транзакций.

И, следовательно,the broker will remove any actions from that log that belong to a producer with the same transaction id and earlierepoch, , если предположить, что они принадлежат несуществующим транзакциям.

4.3. Потребитель, знающий о транзакциях

Когда мы потребляем, мы можем прочитать все сообщения в тематическом разделе по порядку. Хотя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”));

Использование значенияread_committed гарантирует, что мы не прочитаем никаких транзакционных сообщений до завершения транзакции.

Значение по умолчаниюisolation.level равноread_uncommitted..

4.4. Потребление и преобразование транзакцией

Теперь, когда у нас есть производитель и потребитель, настроенные для записи и чтения транзакциями, мы можем использовать записи из нашей входной темы и считать каждое слово в каждой записи:

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

Обратите внимание, что в приведенном выше коде нет ничего транзакционного. Но,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.

Теперь мы можем отправить рассчитанное количество слов в выходную тему.

Давайте посмотрим, как мы можем добиться результатов, в том числе и в транзакциях.

4.5. Отправить API

Чтобы отправить наши счетчики как новые сообщения, но в той же транзакции, мы вызываемbeginTransaction:

producer.beginTransaction();

Затем мы можем записать каждый из них в нашу тему «count», где ключом является слово, а count является значением:

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

Обратите внимание: поскольку производитель может разделить данные по ключу, это означает, чтоtransactional messages can span multiple partitions, each being read by separate consumers. Следовательно, брокер Kafka будет хранить список всех обновленных разделов для транзакции.

Отметим также, чтоwithin a transaction, a producer can use multiple threads to send records in parallel.

4.6. Подтверждение смещений

И, наконец, нам нужно зафиксировать наши смещения, которые мы только что закончили потреблять. With transactions, we commit the offsets back to the input topic we read them from, like normal. Также,we send them to the producer’s transaction.

Мы можем сделать все это за один вызов, но сначала нам нужно рассчитать смещения для каждого тематического раздела:

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

Обратите внимание: то, что мы фиксируем в транзакции, - это предстоящее смещение, то есть нам нужно добавить 1.

Затем мы можем отправить наши расчетные смещения к транзакции:

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

4.7. Фиксация или прерывание транзакции

И, наконец, мы можем зафиксировать транзакцию, которая будет атомарно записывать смещения в стопикconsumer_offsets , а также в саму транзакцию:

producer.commitTransaction();

Это сбрасывает любое буферизованное сообщение в соответствующие разделы. Кроме того, брокер Kafka делает все сообщения в этой транзакции доступными для потребителей.

Конечно, если что-то пойдет не так во время обработки, например, если мы поймаем исключение, мы можем вызватьabortTransaction:

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

И отбросьте все буферизованные сообщения и удалите транзакцию из брокера.

If we neither commit nor abort before the broker-configured max.transaction.timeout.ms, the Kafka broker will abort the transaction itself.  Значение по умолчанию для этого свойства - 900 000 миллисекунд или 15 минут.

5. Другие циклыconsume-transform-produce

Мы только что видели базовый циклconsume-transform-produce, который выполняет чтение и запись в один и тот же кластер Kafka.

Наоборот,applications that must read and write to different Kafka clusters must use the older commitSync and commitAsync API. Как правило, приложения сохраняют смещения потребителей во внешнем хранилище состояний для поддержки транзакций.

6. Заключение

Для приложений, критически важных для данных, сквозная точная обработка часто является обязательной.

В этом руководстве,we saw how we use Kafka to do exactly this, using transactions, и мы реализовали пример подсчета слов на основе транзакций, чтобы проиллюстрировать принцип.

Не стесняйтесь проверить всеcode samples on GitHub.