Пошук уроків, статей та іншого контенту
Порівняє at-most-once, at-least-once та exactly-once і покаже вплив кожної семантики на дизайн системи.
Семантика доставки визначає, що станеться з повідомленням під час збоїв, повторних спроб і перезапусків компонентів.
Вона відповідає не лише на питання «чи буде доставлено повідомлення», а й на питання:
чи може повідомлення загубитися;
чи може воно бути доставлене кілька разів;
де саме система вважає повідомлення обробленим;
як узгоджуються доставка, побічні ефекти та підтвердження обробки.
Важливо розрізняти:
Доставку повідомлення — брокер або відправник передав його споживачу.
Обробку повідомлення — споживач виконав бізнес-операцію.
Побічний ефект — змінив базу даних, списав кошти, відправив email тощо.
Підтвердження — система зафіксувала, що повідомлення можна більше не повторювати.
Одна й та сама семантика доставки може мати різні наслідки залежно від того, де розташовано підтвердження.
Наприклад, якщо споживач спочатку записує дані в базу, а потім підтверджує повідомлення, збій між цими кроками може спричинити повторну обробку.
At-most-once означає: кожне повідомлення буде оброблено не більше одного разу.
Типовий порядок:
споживач отримує повідомлення;
одразу підтверджує його брокеру;
виконує обробку.
Якщо процес завершиться після кроку 2, але до завершення кроку 3, брокер більше не надішле повідомлення. Бізнес-операція не відбудеться.
немає повторної обробки;
не потрібна дедуплікація;
простіша реалізація;
менше навантаження від повторних спроб.
можливі втрати повідомлень;
немає гарантії виконання важливої операції;
повторна доставка після тимчасової помилки не відбувається.
At-most-once підходить, коли втрата окремої події допустима:
метрики, які можна приблизно агрегувати;
телеметрія;
оновлення стану, якщо наступне повідомлення містить повний актуальний стан;
не критичні повідомлення користувацького інтерфейсу.
Вона небезпечна для операцій на кшталт:
списання коштів;
створення замовлення;
видачі доступу;
надсилання юридично значущого повідомлення.
At-least-once означає: якщо повідомлення не було остаточно відкинуто, система намагатиметься доставити його щонайменше один раз.
Повторна доставка є нормальною частиною цієї моделі.
Типовий порядок:
споживач отримує повідомлення;
виконує бізнес-операцію;
підтверджує повідомлення.
Розглянемо збій між кроками 2 і 3:
замовлення успішно створено;
процес споживача аварійно завершився;
підтвердження не надійшло;
брокер повторно доставляє повідомлення;
споживач знову намагається створити замовлення.
Тому at-least-once фактично означає:
Повідомлення не має бути втрачено, але споживач повинен бути готовим обробити його повторно.
менша ймовірність втрати повідомлення;
тимчасові помилки можна подолати повторною спробою;
добре підходить для критичних бізнес-операцій;
можна будувати надійні системи без глобальної розподіленої транзакції.
можливі дублікати;
споживачі мають бути ідемпотентними або мати механізм дедуплікації;
повторні спроби можуть створити додаткове навантаження;
помилкова конфігурація retry може призвести до нескінченного потоку повторів.
Операція є ідемпотентною, якщо повторне її виконання приводить систему до того самого результату.
Наприклад, операція:
Встановити статус замовлення = "paid"може бути ідемпотентною. Повторне встановлення того самого статусу не змінює результат.
Натомість операція:
Збільшити баланс на 100не є ідемпотентною. Два виконання збільшать баланс на 200.
Для at-least-once часто використовують унікальний ідентифікатор повідомлення:
{
"messageId": "payment-8f3c",
"orderId": "order-42",
"amount": 100
}Споживач зберігає оброблені ідентифікатори й не виконує повторний побічний ефект.
class InMemoryDatabase {
constructor() {
this.processedMessages = new Set();
this.orders = new Map();
}
hasProcessed(messageId) {
return this.processedMessages.has(messageId);
}
markAsProcessed(messageId) {
this.processedMessages.add(messageId);
}
saveOrder(orderId, status) {
this.orders.set(orderId, status);
}
}
function processOrder(message, database) {
if (database.hasProcessed(message.messageId)) {
console.log(`Пропускаємо дублікат ${message.messageId}`);
return;
}
// У реальній базі ці операції мають виконуватися в одній транзакції.
database.saveOrder(message.orderId, "created");
database.markAsProcessed(message.messageId);
console.log(`Замовлення ${message.orderId} створено`);
}
function deliverAtLeastOnce(message, consumer) {
// Симулюємо повторну доставку через втрату підтвердження.
consumer(message);
consumer(message);
}
const database = new InMemoryDatabase();
const message = {
messageId: "order-created-42",
orderId: "order-42"
};
deliverAtLeastOnce(message, (receivedMessage) => {
processOrder(receivedMessage, database);
});
console.log(database.orders);Очікуваний результат міститиме лише одне створене замовлення, хоча споживач отримав повідомлення двічі.
У прикладі використано пам’ять процесу, тому він лише демонструє принцип. У реальній системі набір оброблених ідентифікаторів потрібно зберігати у надійному сховищі.
Критично важливо, щоб перевірка дубліката, побічний ефект і запис факту обробки були узгоджені.
Якщо виконати:
запис замовлення;
аварійно завершити процес;
не записати messageId;
то після повторної доставки операція виконається знову.
Зазвичай це вирішують транзакцією бази даних:
BEGIN TRANSACTION
INSERT INTO processed_messages(message_id)
VALUES (...)
-- якщо message_id вже існує, операція завершується без повторного ефекту
INSERT INTO orders(...)
VALUES (...)
COMMITУ схемі має бути унікальне обмеження на message_id. Саме база даних, а не перевірка в коді, повинна захищати від конкурентної обробки одного повідомлення кількома екземплярами.
Exactly-once означає, що повідомлення або відповідна операція буде врахована рівно один раз.
Це найсильніша семантика, але її часто формулюють надто широко.
Є щонайменше три різні значення:
повідомлення доставлено рівно один раз;
обробка всередині певної системи виконана рівно один раз;
бізнес-ефект у всіх зовнішніх системах відбувся рівно один раз.
Це не одне й те саме.
Уявімо послідовність:
споживач отримав повідомлення;
списав кошти у зовнішнього платіжного сервісу;
намагається підтвердити повідомлення;
втрачає мережеве з’єднання.
Після відновлення споживач не знає, чи справді платіжний сервіс виконав операцію. Повторити запит небезпечно: може відбутися подвійне списання. Не повторити — теж небезпечно: можливо, перший запит не дійшов.
Звичайне підтвердження повідомлення не створює атомарності між брокером і зовнішнім сервісом.
Тому практична гарантія exactly-once можлива лише в обмеженому контексті, наприклад коли:
доставка, стан споживача й результат обробки підтримують спільну транзакцію;
брокер і сховище мають протокол узгодженої транзакційної фіксації;
побічний ефект виконується ідемпотентно за унікальним ключем;
система має механізм повторного узгодження невизначеного результату.
Навіть тоді коректніше говорити:
exactly-once у визначеній межі системи
а не обіцяти, що кожен зовнішній ефект у глобальній інфраструктурі гарантовано відбудеться рівно один раз.
Дублікати: не очікуються.
Втрати: можливі.
Складність споживача: низька.
Типовий ризик: критичну операцію не виконано.
Основний сценарій: втрата прийнятніша за повтор.
Дублікати: можливі.
Втрати: мінімізуються, якщо повідомлення не відкинуто явно.
Складність споживача: середня або висока.
Типовий ризик: побічний ефект виконано кілька разів.
Основний сценарій: важливо не втратити повідомлення, а дублікати можна обробити.
Дублікати на гарантованій межі: приховані або усунені.
Втрати на гарантованій межі: не допускаються.
Складність: висока.
Типовий ризик: хибне припущення, що exactly-once поширюється на зовнішні системи.
Основний сценарій: результат має бути узгоджений у транзакційній межі.
Для at-most-once підтвердження відбувається до обробки:
отримати → підтвердити → обробитиДля at-least-once — після обробки:
отримати → обробити → підтвердитиЖоден із цих порядків не усуває всі проблеми:
підтвердження до ефекту допускає втрату;
підтвердження після ефекту допускає повтор.
Дизайн має починатися з визначення, який ризик прийнятніший для конкретної операції.
Для at-least-once повідомлення повинно мати стабільний ідентифікатор. Він не має генеруватися заново під час кожної повторної доставки.
Правильно:
messageId = "payment-123"
повторна доставка → messageId = "payment-123"Неправильно:
перша спроба → randomId = "a1"
повторна спроба → randomId = "b7"У другому випадку споживач не зможе розпізнати дублікат.
Ідемпотентність має охоплювати саме небезпечний побічний ефект.
Недостатньо дедуплікувати повідомлення в пам’яті споживача, якщо:
процес може перезапуститися;
працює кілька екземплярів;
повідомлення обробляються паралельно;
база даних є спільною для кількох споживачів.
Також недостатньо записати messageId окремо від бізнес-зміни. Між цими записами може статися збій.
At-least-once майже завжди передбачає retry, але повторювати потрібно не кожну помилку.
Зазвичай розрізняють:
тимчасові помилки — тайм-аут, короткочасна недоступність сервісу;
постійні помилки — некоректний формат, невідомий тип операції;
невизначені помилки — результат зовнішнього запиту невідомий.
Для тимчасових помилок корисні:
обмежена кількість повторів;
збільшення інтервалу між спробами;
випадкове розсіювання інтервалів, щоб споживачі не створювали одночасний пік навантаження.
Постійно невдале повідомлення не повинно блокувати всю чергу. Його потрібно ізолювати за правилами конкретної системи: наприклад, передати на окремий потік ручного розгляду або відкинути з журналюванням причини.
Розглянемо повідомлення про оплату. Навіть якщо обробка повідомлень у брокері транзакційна, платіжний сервіс може бути окремою системою.
Без ідемпотентного ключа повторний запит може списати кошти двічі.
Надійніший контракт має вигляд:
paymentId = "payment-123"
повторні запити з paymentId = "payment-123"
→ зовнішній сервіс повертає той самий результат
→ нове списання не виконуєтьсяЦе не обов’язково означає глобальну exactly-once доставку. Натомість система робить сам бізнес-оператор ідемпотентним.
Для зовнішніх систем корисно зберігати:
ідентифікатор операції;
ідентифікатор повідомлення;
результат попередньої спроби;
стан операції: pending, succeeded, failed;
час і причину останньої помилки.
Якщо результат запиту невідомий, споживач може не виконувати сліпий повтор, а перевірити стан операції за її ідентифікатором.
Вибір слід робити для кожного типу повідомлень окремо.
втрата повідомлення допустима;
повідомлення лише покращує інформацію, але не є джерелом істини;
повторна обробка дорожча або небезпечніша за втрату;
система має інший механізм періодичного відновлення стану.
втрату повідомлення не можна прийняти;
споживач можна зробити ідемпотентним;
бізнес-операція має стабільний ідентифікатор;
повторні спроби є нормальним способом відновлення після збою.
Для більшості важливих асинхронних бізнес-процесів це практичний вибір.
система справді підтримує потрібну атомарність;
вартість складнішої інфраструктури виправдана;
визначено, де саме діє гарантія;
зовнішні ефекти додатково захищені ідемпотентністю.
Не варто вибирати exactly-once лише як маркетингову назву без опису межі гарантії.
Повідомлення може бути доставлене один раз, але процес може повторитися через збій під час запису результату.
Потрібно явно описати, що саме гарантується:
доставка → споживач
обробка → транзакція в базі
ефект → зовнішній платіжний сервісІдентифікатор повторної спроби має залишатися тим самим ідентифікатором логічної операції.
Після перезапуску процес забуде оброблені повідомлення. Для надійної дедуплікації потрібне стійке сховище.
Якщо бізнес-зміна й маркер обробки фіксуються в різних транзакціях, між ними можливий збій. У такому разі повторна доставка може повторити побічний ефект.
Підтвердження лише повідомляє транспортному рівню, що споживач більше не потребує повторної доставки. Воно не завжди означає, що зовнішній бізнес-ефект підтверджено.
Одна отруйна або некоректна подія може постійно повертатися в обробку й блокувати систему. Retry має мати зрозумілу політику завершення.
At-most-once не допускає повторів, але може втрачати повідомлення.
At-least-once зменшує ризик втрати, але допускає дублікати.
Exactly-once має сенс лише в чітко визначеній транзакційній межі.
Для at-least-once споживач зазвичай роблять ідемпотентним.
Унікальний ідентифікатор має описувати логічну операцію та зберігатися під час повторних спроб.
Перевірка дубліката, бізнес-зміна й запис факту обробки мають бути узгоджені.
Exactly-once усередині брокера не гарантує exactly-once ефект у зовнішній системі.
Семантику потрібно обирати за допустимим ризиком: втрата, дублювання чи складність транзакційної узгодженості.