Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Семантика доставки: at-least-once и exactly-once

Семантика доставки сообщений: от at-most-once до exactly-once

Гарантии доставки: at-most-once, at-least-once и exactly-once, идемпотентный продюсер, транзакции Kafka и условие exactly-once на стороне консумента.

Прямой ответ

Семантика доставки определяет, сколько раз сообщение может быть обработано при сбоях. Три варианта покрывают все случаи.

Семантика Поведение при сбое Следствие
at-most-once Сообщение может не доставиться Потеря данных
at-least-once Сообщение доставляется повторно Возможны дубли
exactly-once Повторная обработка не меняет результат Дубли исключены

Различие определяется моментом фиксации смещения относительно обработки.

Обработка до фиксации. Сбой после обработки и до фиксации приводит к повторной обработке — at-least-once.

Фиксация до обработки. Сбой после фиксации и до обработки приводит к потере сообщения — at-most-once.

at-least-once выбирается в большинстве случаев, поскольку потеря данных хуже дубля, а дубль устраняется идемпотентностью обработки.

Идемпотентный продюсер исключает дубли при повторе записи:

enable.idempotence=true
acks=all
max.in.flight.requests.per.connection=5

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

Транзакции расширяют гарантию на цепочку «чтение — обработка — запись»:

producer.initTransactions();
try {
    producer.beginTransaction();
    producer.send(new ProducerRecord<>("out", key, value));
    producer.sendOffsetsToTransaction(offsets, groupMetadata);
    producer.commitTransaction();
} catch (Exception e) {
    producer.abortTransaction();
}

Запись результата и фиксация смещения фиксируются одной транзакцией, поэтому частичное применение исключено.

Что exactly-once в Kafka не покрывает

Гарантия транзакций распространяется на операции внутри Kafka. Четыре случая остаются вне её действия.

Запись во внешнюю систему. Результат обработки записывается в базу данных либо вызывается внешний API. Транзакция Kafka не охватывает внешнюю операцию, поэтому сбой между записью наружу и фиксацией транзакции приводит к расхождению.

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

Обработка с побочным эффектом вне транзакции. Отправка письма либо вызов сервиса внутри обработки выполняются независимо от транзакции, поэтому повтор приводит к повторному побочному эффекту.

Собственная логика обработки без идемпотентности. Повторная обработка сообщения меняет результат, если операция не идемпотентна, независимо от гарантий брокера.

Практическое следствие: exactly-once в Kafka означает ровно один эффект внутри Kafka. Для внешних систем гарантия достигается идемпотентностью операции, а не настройкой брокера; о механизме — в статье про идемпотентность операций.

Отдельный аспект — стоимость транзакций. Транзакционная запись требует координации, поэтому пропускная способность ниже, чем при обычной записи. Выбор определяется требованием к отсутствию дублей.

Распространённая альтернатива — at-least-once с идемпотентной обработкой. Идентификатор сообщения используется как ключ операции, поэтому повтор не меняет результат. Такой подход проще транзакций и покрывает внешние системы; о гарантиях доставки — в статье про надёжность доставки в очередях.

Практика: выбор семантики доставки

1. Начинайте с at-least-once и идемпотентной обработки. Подход покрывает внешние системы и не требует транзакционной координации.

2. Используйте идентификатор сообщения как ключ операции. Повторная обработка с тем же идентификатором не меняет результат.

3. Фиксируйте смещение после обработки. Фиксация до обработки приводит к потере сообщения при сбое.

4. Включайте идемпотентность продюсера. Повторная отправка после таймаута не создаёт дубль в топике.

5. Применяйте транзакции для цепочки «чтение — обработка — запись» внутри Kafka. Фиксация результата и смещения выполняется атомарно.

6. Используйте уровень изоляции read_committed. Консумент не видит сообщения незафиксированных транзакций.

7. Проверяйте идемпотентность внешних операций. Запись в базу и вызов сервиса выполняются вне транзакции Kafka.

Рекомендации:

  1. Не считайте exactly-once заменой идемпотентности. Гарантия охватывает операции внутри Kafka, а внешние эффекты требуют отдельной защиты.
  2. Учитывайте стоимость транзакций. Пропускная способность ниже, поэтому транзакции применяются только там, где дубли недопустимы.
  3. Проверяйте поведение при сбое в тестах. Имитация сбоя между обработкой и фиксацией показывает корректность выбранной семантики.
  4. Различайте дубль доставки и дубль эффекта. Повторная доставка допустима, если эффект идемпотентен.
  5. Документируйте выбранную семантику. Требование к отсутствию дублей определяет подход, поэтому решение фиксируется.

Практический сценарий: дубли записей во внешней базе после сбоя

Контекст: после сбоя консумента во внешней базе оказались дубли записей по части сообщений.

  1. Диагностика. Консумент обрабатывал сообщение, записывал результат во внешнюю базу и фиксировал смещение. Сбой происходил между записью и фиксацией, поэтому после перезапуска сообщение обрабатывалось повторно и создавало вторую запись.
  2. Причина. Запись во внешнюю базу не входила в транзакцию Kafka, а обработка не была идемпотентной. Гарантии брокера не распространялись на внешнюю операцию.
  3. Действия. (а) Добавили уникальный идентификатор обработки, формируемый из топика, партиции и смещения; (б) сделали запись во внешнюю базу идемпотентной: вставка с уникальным ограничением по идентификатору, повтор отклоняется как дубль; (в) сохранили фиксацию смещения после успешной записи, чтобы потеря сообщения не возникала; (г) удалили дубли, возникшие до внедрения защиты; (д) добавили тест на сбой между записью и фиксацией с проверкой отсутствия дублей.
  4. Результат. Повторная обработка сообщения не создаёт дубль, поскольку запись идемпотентна по идентификатору.

Вывод: exactly-once в Kafka охватывает операции внутри брокера, а запись во внешнюю систему требует идемпотентности на стороне обработки. Уникальный идентификатор обработки с ограничением в базе устраняет дубли при повторной доставке.

FAQ

Какую семантику выбрать по умолчанию? At-least-once с идемпотентной обработкой. Подход не теряет данные и не создаёт дублей эффекта.

Нужны ли транзакции Kafka для exactly-once? Для цепочки «чтение — обработка — запись» внутри Kafka — да. Для внешних систем транзакции не заменяют идемпотентность.

Что произойдёт при фиксации смещения до обработки? Сбой после фиксации приведёт к потере сообщения, поскольку повторная обработка не выполнится.

Снижает ли транзакционная запись пропускную способность? Да, поскольку требуется координация фиксации. Транзакции применяются там, где дубли недопустимы.

Источники: Apache Kafka semantics, Kafka design semantics, KIP-98 — ссылки в шапке.