Вопрос 10 · Раздел 15

В чём разница между at-most-once, at-least-once и exactly-once

Outbox pattern: (1) INSERT into outbox_table + business_data в одной транзакции БД. (2) Отдельный процесс читает outbox_table и отправляет в Kafka. (3) После успешной отправки —...

Версии по языкам: English Russian Ukrainian

Уровень Junior

Определения

At-most-once (не более одного раза):

  • Сообщение доставится 0 или 1 раз
  • Может потеряться при ошибках
  • ~1x baseline latency

At-least-once (как минимум один раз):

  • Сообщение доставится 1 или больше раз
  • Может продублироваться при retry
  • Данные не потеряются

Exactly-once (ровно один раз):

  • Сообщение доставится ровно 1 раз
  • Нет потерь, нет дубликатов
  • 2-3x latency

Пример из жизни

At-most-once:
  Отправил SMS → не проверил дошло ли
  → Может не дойти

At-least-once:
  Отправил SMS → не получил подтверждение → отправил снова
  → Может дойти дважды

Exactly-once:
  Отправил SMS → получил уникальный ID → проверил доставку
  → Дошло ровно один раз

Когда что использовать

Семантика Когда использовать
At-most-once Метрики, логи, мониторинг
At-least-once Большинство бизнес-систем
Exactly-once Финансы, биллинг, банковские операции

Уровень Middle

Как это работает в Kafka

At-most-once:

// Producer — не ждёт подтверждения
props.put("acks", "0");

// Consumer — коммитит сразу после получения
props.put("enable.auto.commit", "true");

At-least-once:

// Producer — ждёт подтверждения от всех
props.put("acks", "all");
props.put("enable.idempotence", "true");
props.put("retries", Integer.MAX_VALUE);

// Consumer — коммитит после обработки
props.put("enable.auto.commit", "false");
consumer.commitSync();  // после process()

Exactly-once:

// Producer
props.put("enable.idempotence", "true");
props.put("transactional.id", "my-tx-id");

// Consumer
props.put("isolation.level", "read_committed");

Сравнительная таблица

Параметр At-most-once At-least-once Exactly-once
Потеря данных Да Нет Нет
Дубликаты Нет Да Нет
Производительность Максимальная Высокая Средняя
Сложность Минимальная Средняя Высокая
Use case Метрики Бизнес-события Финансы

Idempotency — ключ к at-least-once

// Идемпотентная обработка
public void process(Message msg) {
    if (!alreadyProcessed(msg.getId())) {
        doProcess(msg);
        markAsProcessed(msg.getId());
    }
}

Типичные ошибки

  1. At-least-once без обработки дубликатов:
    Дубликаты → двойные списания → баги в бизнес-логике
    
  2. Exactly-once без понимания ограничений:
    Kafka → PostgreSQL → exactly-once не работает
    Kafka не управляет транзакцией PostgreSQL
    
  3. Коммит до обработки:
    consumer.commitSync();  // ❌
    process(records);       // Если упал — данные потеряны
    

Уровень Senior

“EndOfWorld” проблема

Exactly-once в Kafka работает “из коробки” только для Kafka-to-Kafka:

Kafka → Processing → Kafka  ✅ Works
Kafka → Processing → PostgreSQL  ❌ Does not work

Почему?

Kafka может гарантировать atomic commit только для:
- Записи в Kafka topic
- Коммита offsets в __consumer_offsets

Kafka не может управлять транзакцией в PostgreSQL!

Outbox pattern: (1) INSERT into outbox_table + business_data в одной транзакции БД. (2) Отдельный процесс читает outbox_table и отправляет в Kafka. (3) После успешной отправки — DELETE из outbox_table.

Решения для внешних систем

1. Idempotent writes:

// PostgreSQL — UPSERT по уникальному ключу
INSERT INTO orders (id, data)
VALUES (?, ?)
ON CONFLICT (id) DO NOTHING;

2. Outbox Pattern:

1. Бизнес-операция → запись в outbox (в той же транзакции)
2. CDC (Debezium) → читает outbox
3. Отправляет в Kafka
4. Consumer → обрабатывает → пишет в target

3. Two-Phase Commit (XA — не рекомендуется):

Prepare phase → все участники готовы
Commit phase → все коммитят
Проблемы: блокировки, сложность, производительность

Internal Implementation Details

Idempotent Producer:

PID (Producer ID) + Sequence Number
Брокер отклоняет дубликаты по последовательности
Работает на уровне партиции

Transactions:

Transaction Coordinator → управляет state
__transaction_state topic → хранит state
Commit/Abort → атомарные операции

Performance Analysis

Latency comparison (относительная):
At-most-once:     1x
At-least-once:    1.2x
Exactly-once:     2-3x

Throughput comparison (относительная):
At-most-once:     100%
At-least-once:    85-95%
Exactly-once:     50-70%

When to Use What

At-most-once:

  • Метрики мониторинга
  • Логи приложений
  • Real-time аналитика (приемлемая потеря)

At-least-once:

  • Большинство бизнес-систем
  • Обработка заказов
  • Уведомления
  • Обновление кэша

Exactly-once:

  • Финансовые транзакции
  • Биллинг
  • Банковские переводы
  • Только Kafka-to-Kafka сценарии

Best Practices

✅ Выбирайте at-least-once по умолчанию
✅ Стремитесь к идемпотентным консьюмерам
✅ Exactly-once только для Kafka-to-Kafka
✅ Outbox pattern для внешних систем
✅ Idempotent writes в БД

❌ At-most-once для важных данных
❌ Exactly-once для Kafka → Database
❌ Без обработки дубликатов
❌ Игнорирование performance trade-offs

Архитектурные решения

  1. At-least-once + idempotent consumer — оптимальный баланс
  2. Exactly-once требует настройки параметров транзакций на брокере (transaction.state.log.replication.factor >= 2). В Kafka 1.0+ transaction support включён по умолчанию.
  3. Outbox pattern — универсальное решение для end-to-end
  4. Idempotent writes — дешевле и надёжнее транзакций

Резюме для Senior

  • Выбирайте at-least-once по умолчанию
  • Стремитесь сделать консьюмеры идемпотентными
  • Exactly-once работает только Kafka-to-Kafka
  • Для внешних систем используйте Outbox или idempotent writes
  • Понимайте performance trade-offs каждой семантики

🎯 Шпаргалка для интервью

Обязательно знать:

  • At-most-once: 1x latency, возможна потеря, для метрик/логов
  • At-least-once: 1.2x latency, дубликаты возможны, для бизнес-систем
  • Exactly-once: 2-3x latency, 50-70% throughput, только для финансов
  • Exactly-once работает из коробки только для Kafka-to-Kafka
  • Для Kafka → БД: Outbox pattern или idempotent writes (UPSERT)
  • At-least-once + idempotent consumer — оптимальный баланс для большинства систем
  • Коммит ДО обработки = data loss, коммит ПОСЛЕ = at-least-once

Частые уточняющие вопросы:

  • Почему exactly-once не работает с внешними системами? — Kafka не может управлять транзакцией PostgreSQL.
  • Что такое Outbox pattern? — INSERT в outbox_table в той же транзакции → Debezium CDC → Kafka.
  • Какая семантика по умолчанию? — At-least-once, strive for idempotent consumers.
  • Какой overhead у exactly-once? — 2-3x latency, 50-70% throughput.

Красные флаги (НЕ говорить):

  • «Exactly-once работает для Kafka → HTTP API» — HTTP не поддерживает транзакции Kafka
  • «At-most-once для заказов» — потеря заказов недопустима
  • «Дубликаты не проблема» — двойные списания = баг в бизнес-логике
  • «Коммит перед обработкой — стандартная практика» — это data loss

Связанные темы:

  • [[9. Какие гарантии доставки сообщений предоставляет Kafka]]
  • [[11. Как настроить exactly-once семантику]]
  • [[23. Что такое idempotent producer]]
  • [[25. Что такое DLQ (Dead Letter Queue)]]