Прямой ответ
Семантика доставки определяет, сколько раз сообщение может быть обработано при сбоях. Три варианта покрывают все случаи.
| Семантика | Поведение при сбое | Следствие |
|---|---|---|
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.
Рекомендации:
- Не считайте exactly-once заменой идемпотентности. Гарантия охватывает операции внутри Kafka, а внешние эффекты требуют отдельной защиты.
- Учитывайте стоимость транзакций. Пропускная способность ниже, поэтому транзакции применяются только там, где дубли недопустимы.
- Проверяйте поведение при сбое в тестах. Имитация сбоя между обработкой и фиксацией показывает корректность выбранной семантики.
- Различайте дубль доставки и дубль эффекта. Повторная доставка допустима, если эффект идемпотентен.
- Документируйте выбранную семантику. Требование к отсутствию дублей определяет подход, поэтому решение фиксируется.
Практический сценарий: дубли записей во внешней базе после сбоя
Контекст: после сбоя консумента во внешней базе оказались дубли записей по части сообщений.
- Диагностика. Консумент обрабатывал сообщение, записывал результат во внешнюю базу и фиксировал смещение. Сбой происходил между записью и фиксацией, поэтому после перезапуска сообщение обрабатывалось повторно и создавало вторую запись.
- Причина. Запись во внешнюю базу не входила в транзакцию Kafka, а обработка не была идемпотентной. Гарантии брокера не распространялись на внешнюю операцию.
- Действия. (а) Добавили уникальный идентификатор обработки, формируемый из топика, партиции и смещения; (б) сделали запись во внешнюю базу идемпотентной: вставка с уникальным ограничением по идентификатору, повтор отклоняется как дубль; (в) сохранили фиксацию смещения после успешной записи, чтобы потеря сообщения не возникала; (г) удалили дубли, возникшие до внедрения защиты; (д) добавили тест на сбой между записью и фиксацией с проверкой отсутствия дублей.
- Результат. Повторная обработка сообщения не создаёт дубль, поскольку запись идемпотентна по идентификатору.
Вывод: exactly-once в Kafka охватывает операции внутри брокера, а запись во внешнюю систему требует идемпотентности на стороне обработки. Уникальный идентификатор обработки с ограничением в базе устраняет дубли при повторной доставке.
FAQ
Какую семантику выбрать по умолчанию? At-least-once с идемпотентной обработкой. Подход не теряет данные и не создаёт дублей эффекта.
Нужны ли транзакции Kafka для exactly-once? Для цепочки «чтение — обработка — запись» внутри Kafka — да. Для внешних систем транзакции не заменяют идемпотентность.
Что произойдёт при фиксации смещения до обработки? Сбой после фиксации приведёт к потере сообщения, поскольку повторная обработка не выполнится.
Снижает ли транзакционная запись пропускную способность? Да, поскольку требуется координация фиксации. Транзакции применяются там, где дубли недопустимы.
Источники: Apache Kafka semantics, Kafka design semantics, KIP-98 — ссылки в шапке.