GH GambleHub

Exactly-once vs At-least-once

1) Навіщо взагалі обговорювати семантики

Семантика доставки визначає як часто одержувач побачить повідомлення при збоях і ретраях:
  • At-most-once - без повторів, але можлива втрата (рідко прийнятно).
  • At-least-once - не втрачаємо, але можливі дублікати (дефолт більшості брокерів/черг).
  • Exactly-once - кожне повідомлення обробляється рівно один раз з точки зору спостережуваного ефекту.

Ключова істина: в розподіленому світі без глобальних транзакцій і синхронної узгодженості «чисте» end-to-end exactly-once недосяжне. Ми будуємо ефективно exactly-once: допускаємо повтори на транспорті, але робимо обробку ідемпотентною так, щоб спостережуваний ефект був «ніби один раз».


2) Модель відмов і де виникають дублікати

Повтори з'являються через:
  • Втрати ack/commit (продюсер/брокер/консюмер «не почули» підтвердження).
  • Перевиборів лідерів/реплік, відновлень після мережевих розривів.
  • Таймаутів/ретраїв на будь-яких ділянках (kliyent→broker→konsyumer→sink).

Наслідок: не можна покладатися на «унікальність доставки» транспорту. Керуємо ефектами: запис в БД, списання грошей, відправка листа і т.п.


3) Exactly-once в постачальниках і що це насправді

3. 1 Kafka

Дає цеглинки:
  • Idempotent Producer (`enable. idempotence = true') - запобігає дублі на стороні продюсера при ретраях.
  • Транзакції - атомарно публікують повідомлення в кілька партій і коммитят офсети споживання (патерн read-process-write без «перепусток»).
  • Compaction - зберігає останнє значення за ключем.

Але «кінець ланцюга» (синк: БД/платіж/пошта) все одно вимагає ідемпотентності. Інакше дубль обробника викличе дубль ефекту.

3. 2 NATS / Rabbit / SQS

За замовчуванням - at-least-once з ack/redelivery. Exactly-once досягається на рівні додатку: ключі, дедуп-стор, upsert.

Висновок: Exactly-once транспортом ≠ exactly-once ефектом. Останній робиться в обробнику.


4) Як побудувати ефективно exactly-once поверх at-least-once

4. 1 Ідемпотентний ключ (idempotency key)

Кожна команда/подія несе природний ключ: `payment_id`, `order_id#step`, `saga_id#n`. Обробник:
  • Перевіряє «вже бачив?» - дедуп-стор (Redis/БД) з TTL/ретеншном.
  • Якщо бачив - повторює раніше обчислений результат або робить no-op.
Redis-ескіз:
lua
-- SET key if not exists; expires in 24h local ok = redis.call("SET", KEYS[1], ARGV[1], "NX", "EX", 86400)
if ok then return "PROCESS" else return "SKIP" end

4. 2 Upsert в базі (ідемпотентний синк)

Записи робляться через UPSERT/ON CONFLICT з перевіркою версії/суми.

PostgreSQL:
sql
INSERT INTO payments(id, status, amount, updated_at)
VALUES ($1, $2, $3, now())
ON CONFLICT (id) DO UPDATE
SET status = EXCLUDED.status,
updated_at = now()
WHERE payments.status <> EXCLUDED.status;

4. 3 Транзакційний Outbox/Inbox

Outbox: бізнес-транзакція і запис «події до публікації» відбуваються в одній транзакції БД. Фоновий публікатор читає outbox і відправляє в брокер → немає розбіжностей між станом і подією.
Inbox: для вхідних команд зберігаємо'message _ id'і результат до виконання; повторна обробка бачить запис і не повторює побічні ефекти.

4. 4 Консистентна обробка ланцюжка (read→process→write)

Kafka: транзакція «прочитав офсет → записав результати → коміт» в один атомарний блок.
Без транзакцій: «спочатку запиши результат/Inbox, потім ack»; при креші дублікат побачить Inbox і завершиться no-op.

4. 5 SAGA/компенсації

Коли ідемпотентність неможлива (зовнішній провайдер списав гроші), використовуємо компенсуючі операції (refund/void) і ідемпотентні зовнішні API (повторний'POST'з тим же'Idempotency-Key'дає той же підсумок).


5) Коли достатньо at-least-once

Оновлення кешів/матеріалізованих представлень з компакцією по ключу.
Лічильники/метрики, де повторна інкрементація прийнятна (або зберігаємо дельти з версією).
Нотифікації, де вторинне письмо не критично (краще все одно ставити ключ).

Правило: якщо дубль не змінює бізнес-сенс або легко виявимо → at-least-once + частковий захист.


6) Продуктивність і вартість

Exactly-once (навіть «ефективно») коштує дорожче: доп. записи (Inbox/Outbox), зберігання ключів, транзакцій, складніше діагностика.
At-least-once дешевше/простіше, краще по throughput/p99.
Оцінюйте: ціна дубля × ймовірність дубля vs вартість захисту.


7) Приклади конфігурацій і коду

7. 1 Kafka продюсер (ідемпотентність + транзакції)

properties enable.idempotence=true acks=all retries=INT_MAX max.in.flight.requests.per.connection=5 transactional.id=orders-writer-1
java producer.initTransactions();
producer.beginTransaction();
producer.send(recordA);
producer.send(recordB);
// также можно atomically commit consumer offsets producer.commitTransaction();

7. 2 Консюмер з Inbox (псевдокод)

pseudo if (inbox.exists(msg.id)) return inbox.result(msg.id)
begin tx if!inbox.insert(msg.id) then return inbox.result(msg.id)
result = handle(msg)
sink.upsert(result)     # идемпотентный синк inbox.set_result(msg.id, result)
commit ack(msg)

7. 3 HTTP Idempotency-Key (зовнішні API)


POST /payments
Idempotency-Key: 7f1c-42-...
Body: { "payment_id": "p-123", "amount": 10.00 }

Повторний POST з тим же ключем → той же результат/статус.


8) Спостережуваність і метрики

'duplicate _ attempts _ total'- скільки разів ловили дубль (по Inbox/Redis).
'idempotency _ hit _ rate'- частка повторів, «врятованих» ідемпотентністю.
'txn _ abort _ rate'( Kafka/БД) - частка відкатів.
'outbox _ backlog'- відставання публікації.
'exactly _ once _ path _ latency {p95, p99}'vs'at _ least _ once _ path _ latency'- накладні витрати.
Аудит логів: связка `message_id`, `idempotency_key`, `saga_id`, `attempt`.


9) Тест-плейбуки (Game Days)

Повтор відправки: ретраї продюсера при штучних таймаутах.
Креш між «сінк і ack»: переконатися, що Inbox/Upsert запобігають дубль.
Пере-доставка: збільшити redelivery в брокері; перевірити дедуп.
Ідемпотентність зовнішніх API: повторні POST з тим же ключем - однакова відповідь.
Зміна лідера/розрив мережі: перевірити транзакції Kafka/поведінку консюмерів.


10) Анти-патерни

Покладатися на транспорт: «у нас Kafka з exactly-once, значить можна без ключів» - ні.
No-op ack до запису: ackнули, але синк впав → втрата.
Відсутність DLQ/ретраїв з джиттером: нескінченні повтори і шторм.
Випадкові UUID замість природних ключів: нема чим дедуплікувати.
Змішування Inbox/Outbox з прод-таблицями без індексів: гарячі блокування і p99-хвости.
Бізнес-операції без ідемпотентного API у зовнішніх провайдерів.


11) Чек-лист вибору

1. Ціна дубля (гроші/юридика/UX) vs ціна захисту (латентність/складність/вартість).
2. Чи є природний ключ події/операції? Якщо ні - придумайте стабільний.
3. Синк підтримує Upsert/версіонування? Інакше - Inbox + компенсації.
4. Чи потрібні глобальні транзакції? Якщо ні - сегментуйте на SAGA.
5. Потрібно реплей/довгий ретеншн? Kafka + Outbox. Потрібні швидкі RPC/низька затримка? NATS + Idempotency-Key.
6. Мульти-тенантність і квоти: ізоляція ключів/просторів.
7. Спостережуваність: метрики idempotency і backlog включені.


12) FAQ

Q: Чи можна досягти «математичного» exactly-once end-to-end?
A: Тільки у вузьких сценаріях з одним консистентним сховищем і транзакціями на всьому шляху. У загальному випадку - ні; використовуйте ефективно exactly-once через ідемпотентність.

Q: Що швидше?
A: At-least-once. Exactly-once додає транзакції/зберігання ключів → вище p99 і вартість.

Q: Де зберігати ключі ідемпотентності?
A: Швидкий стор (Redis) з TTL, або таблиця Inbox (PK = message _ id). Для платежів - довше (дні/тижні).

Q: Як вибирати TTL дедуп-ключів?
A: Мінімум = максимальний час повторної доставки + операційний запас (зазвичай 24-72 год). Для фінансів - більше.

Q: Чи потрібен ключ, якщо у мене compaction по ключу в Kafka?
A: Так. Compaction зменшить зберігання, але не зробить ваш синк ідемпотентним.


13) Підсумки

At-least-once - базова, надійна семантика транспорту.
Exactly-once як бізнес-ефект досягається на рівні обробника: Idempotency-Key, Inbox/Outbox, Upsert/версії, SAGA/компенсації.
Вибір - це компроміс вартість ↔ ризик дубля ↔ простота експлуатації. Проектуйте природні ключі, робіть синкі ідемпотентними, додавайте спостережуваність і регулярно проводите game days - тоді ваші пайплайни будуть передбачуваними і безпечними навіть при штормі ретраїв і відмов.

Contact

Зв’яжіться з нами

Звертайтеся з будь-яких питань або за підтримкою.Ми завжди готові допомогти!

Розпочати інтеграцію

Email — обов’язковий. Telegram або WhatsApp — за бажанням.

Ваше ім’я необов’язково
Email необов’язково
Тема необов’язково
Повідомлення необов’язково
Telegram необов’язково
@
Якщо ви вкажете Telegram — ми відповімо й там, додатково до Email.
WhatsApp необов’язково
Формат: +код країни та номер (наприклад, +380XXXXXXXXX).

Натискаючи кнопку, ви погоджуєтесь на обробку даних.