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 шығындары (продюсер/брокер/консюмер растауды «естімеген»).
- Көшбасшыларды/репликаларды қайта сайлау, желілік алшақтықтардан кейін қалпына келтіру.
- Кез келген учаскелердегі таймауттар/ретрайлер (клиент → брокер → консюмер → синк).
Нәтижесі: көлікті «жеткізудің бірегейлігіне» сүйенуге болмайды. Әсерлерді басқарамыз: ДБ-ға жазу, ақшаны есептен шығару, хат жіберу және т.б.
3) Жеткізушілерде Exactly-once және ол шын мәнінде не
3. 1 Kafka
Кірпіш береді:- Idempotent Producer (`enable. idempotence = true ') - ретраялар кезінде продюсер жағындағы дубльдерді болдырмайды.
- Транзакциялар - бірнеше партияға арналған хабарламаларды және тұтыну офсеттерін («рұқсатсыз» read-process-write паттерні) атомарлық түрде жариялайды.
- Compaction - соңғы кілт мәнін сақтайды.
Бірақ «тізбектің соңы» (синк: ДБ/төлем/почта) бәрібір теңсіздікті талап етеді. Әйтпесе, өңдегіш дублі дубль эффектін тудырады.
3. 2 NATS / Rabbit / SQS
Әдепкі - ack/redelivery бар at-least-once. 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' деген табиғи кілт болады. Өңдегіші:- «көрдіңіз бе?» - TTL/ретеншн бар дедуп-стор (Redis/ДБ).
- Егер көрдім - бұрын есептелген нәтижені қайталайды немесе no-op жасайды.
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 жазыңыз»; crash кезінде телнұсқаны Inbox көреді және no-op аяқталады.
4. 5 SAGA/өтемақы
Идемпотенттілік мүмкін болмаған жағдайда (сыртқы провайдер ақшаны есептен шығарды) өтемдік операцияларды (refund/void) және идемпотенттік сыртқы API (сол 'Idempotency-Key' -мен қайталанған 'POST' сол қорытындыны береді) пайдаланамыз.
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 жазбаға дейін: іске қосылды, бірақ синк түсті → жоғалту.
DLQ/ретрайлардың болмауы: шексіз қайталаулар және дауыл.
Табиғи кілттердің орнына кездейсоқ UUID: дедуплициялайтын ештеңе жоқ.
Индекстерсіз прод-кестелермен Inbox/Outbox араластыру: ыстық бұғаттау және p99-қалдықтары.
Сыртқы провайдерлерде идемпотенттік API-сыз бизнес-операциялар.
11) Таңдау чек-парағы
1. Double бағасы (ақша/заң/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: TTL немесе Inbox (PK = message _ id) кестесі бар жылдам жол. Төлемдер үшін - ұзақ (күндер/апталар).
Q: TTL дедуп кілттерін қалай таңдауға болады?
А: Минимум = қайта жеткізудің ең көп уақыты + операциялық қор (әдетте 24-72 сағат). Қаржы үшін - одан да көп.
Q: Егер менде Kafka кілті бойынша compaction болса, кілт керек пе?
А: Иә. Compaction сақтауды азайтады, бірақ синкті іспеттес етпейді.
13) Қорытынды
At-least-once - көліктің базалық, сенімді семантикасы.
Exactly-once бизнес әсері ретінде өңдегіш деңгейінде қол жеткізіледі: Idempotency-Key, Inbox/Outbox, Upsert/нұсқасы, SAGA/өтемақы.
Таңдау - бұл компромисс құны, екеуінің тәуекелі, пайдаланудың қарапайымдылығы. Табиғи кілттерді жобалаңыз, синктерді іспотентті жасаңыз, байқауды қосыңыз және тұрақты түрде game days өткізіңіз - сонда ретрациялар мен істен шығулар дауылында да сіздің үлестеріңіз болжамды және қауіпсіз болады.