В чём разница между at-most-once, at-least-once и exactly-once
Outbox pattern: (1) INSERT into outbox_table + business_data в одной транзакции БД. (2) Отдельный процесс читает outbox_table и отправляет в Kafka. (3) После успешной отправки —...
Уровень 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());
}
}
Типичные ошибки
- At-least-once без обработки дубликатов:
Дубликаты → двойные списания → баги в бизнес-логике - Exactly-once без понимания ограничений:
Kafka → PostgreSQL → exactly-once не работает Kafka не управляет транзакцией PostgreSQL - Коммит до обработки:
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
Архитектурные решения
- At-least-once + idempotent consumer — оптимальный баланс
- Exactly-once требует настройки параметров транзакций на брокере (transaction.state.log.replication.factor >= 2). В Kafka 1.0+ transaction support включён по умолчанию.
- Outbox pattern — универсальное решение для end-to-end
- 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)]]