Питання 10 · Розділ 15

У чому різниця між at-most-once, at-least-once і exactly-once

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

Мовні версії: 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, прагніть до 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)]]