Масштабованість і надійність у системах, орієнтованих на події: забезпечення надійного споживання подій.

1/4 Запобігання втраті подій за допомогою патерну Outbox.

2/4 Відправка подій з таблиці Outbox.

3/4 Забезпечення надійного споживання подій.

4/4 Масштабування компонентів системи для високої пропускної здатності. ⏳

У перших двох частинах ми обговорили проблеми, що виникають на стороні виробника в системах, орієнтованих на події, які відповідають за створення подій та їх відправку до брокера повідомлень, а також їх вирішення. У цій частині ми зосередимося на іншій стороні рівняння — стороні споживача — і розглянемо процес споживання подій. Ми розглянемо основні проблеми, які можуть виникнути під час цього процесу, та можливі рішення. Як і в процесі виробництва подій, для процесу споживання подій критично важливою є надійність та стійкість до збоїв.

pic

source: https://www.ledjonbehluli.com/posts/domaintointegrationevent_

Порядок денний

  • Дублювання подій
  • Втрата подій / Неуспішна подія
  • Порядок подій
  • Гладке оброблення простоїв брокера
  • Висновок

Дублювання подій

Дублювання подій може відбуватися на стороні споживача з кількох причин.

Найочевидніший сценарій — це коли виробник відправляє ту ж саму подію до брокера повідомлень кілька разів. Це може статися через помилку в сервісі виробника або внаслідок тимчасової перерви в мережі під час комунікації між сервісом виробника та брокером повідомлень. У таких випадках виробник може помилково припустити, що повідомлення не було доставлено (хоча це не так), оскільки він не отримав підтвердження від брокера, що призводить до повторної відправки повідомлення.

Інший сценарій виникає, коли брокер повідомлень доставляє ту ж саму подію до сервісу споживача кілька разів через тимчасову мережеву перерву, що зумовлено обраним методом гарантії доставки між брокером та споживачем. Це зазвичай відбувається при використанні методу принаймні один раз для гарантії доставки. У той час як інші два методи гарантії доставки, не більше одного разу та точно один раз, не несуть ризику дублювання доставки повідомлень.

Для обох вищезгаданих сценаріїв може бути застосоване рішення: Ідемпотентний споживач (Idempotent Consumer).

Спочатку концепцію ідемпотентності можна визначити наступним чином: У системах, орієнтованих на події, ідемпотентність гарантує, що коли сервіс споживача обробляє одне і те ж повідомлення один або кілька разів, система стабільно дає той самий результат. Іншими словами, повторна обробка повідомлення не повинна змінювати поведінку системи. Це критична функція, особливо для обробки сценаріїв відмов (наприклад, коли повідомлення відправляється знову через мережеву перерву) і запобігання дублюванню операцій.

Ідемпотентність на стороні споживача можна досягти різними способами, і одним із ефективних методів є Inbox Pattern (Патерн Вхідної таблиці). У цьому підході кожне повідомлення має унікальний ідентифікатор. Коли сервіс споживача отримує повідомлення від брокера, він спочатку перевіряє, чи існує цей унікальний ідентифікатор у Таблиці вхідних повідомлень (Inbox Table). Якщо ідентифікатор не знайдений, сервіс обробляє повідомлення і вставляє його унікальний ідентифікатор у таблицю вхідних повідомлень. Якщо ідентифікатор вже існує, це означає, що повідомлення вже було оброблено, і сервіс ігнорує його.
Це забезпечує, що навіть якщо повідомлення доставляється кілька разів, воно буде оброблене лише один раз, а наступні доставки будуть проігноровані.

Втрата подій / Неуспішна подія

У попередніх частинах ми розглянули проблему втрати подій на стороні виробника та обговорили, як патерн Outbox може значною мірою пом'якшити цю проблему.

Коли ми говоримо про втрату подій на стороні споживача, ми маємо на увазі ситуацію, коли повідомлення, яке успішно досягло брокера повідомлень, не обробляється належним чином, але сервіс споживача поводиться так, ніби подія була успішно оброблена — фактично пропускаючи подію.

У системах, орієнтованих на події, обробка подій і, після обробки, або видалення їх з брокера повідомлень, або перехід до наступної події є відповідальністю сервісу споживача. Як ця відповідальність виконується, може змінюватися в залежності від структури брокера повідомлень або системи черг повідомлень, яку ви використовуєте.

Наприклад, у Kafka сервіс споживача обробляє повідомлення, а потім підтверджує його, фактично переходячи до наступного offset. Попереднє повідомлення не обов'язково потрібно видаляти. У RabbitMQ, після обробки повідомлення, сервіс споживача може надіслати підтвердження (ack), що фактично дає дозвіл RabbitMQ на видалення повідомлення. Як ще один приклад, при використанні Amazon SQS споживач повинен надіслати запит на видалення до SQS після обробки повідомлення, щоб забезпечити його видалення.

Як ви бачите, кожен з цих трьох інструментів реалізує абсолютно різні механізми споживання. Однак, незважаючи на метод, завжди саме сервіс споживача несе відповідальність за виконання дій для переходу до наступного повідомлення.

Інша критична проблема на стороні споживача — це не виконання дії ‘commit|ack|delete’ до успішної обробки повідомлень. Особливо у сценаріях пакетної обробки, якщо дія ‘commit|ack|delete’ здійснюється для всіх повідомлень до того, як буде гарантовано, що всі повідомлення в відповідному пакеті були успішно оброблені, може статися втрата повідомлень. Пакетна обробка — це добре, але вона має свою ціну в плані розробки для забезпечення того, щоб усі повідомлення були спожиті правильно.

Іншим критичним моментом є ситуація, коли подія, отримана від брокера повідомлень, не може бути оброблена через помилку в сервісі споживача або мережеву проблему. У такому випадку є кілька важливих дій, які ми повинні виконати. Якщо ми не виконаємо ці дії, ми можемо знову зіткнутися з ризиком втрати повідомлення.

Патерн повторних спроб (Retry Pattern)

Сервіс споживача виконує дію в результаті отримання події від брокера повідомлень. Ми не можемо ігнорувати ймовірність того, що під час цієї дії може виникнути непередбачена помилка. Якщо ця непередбачена проблема є тимчасовою, тобто тимчасовою помилкою, ми можемо спробувати обробити подію знову. Завдяки впровадженню патерну повторних спроб, особливо використовуючи метод експоненціального затримки (exponential backoff), ми зможемо витримати тимчасові проблеми з мережею або системою.

Черга мертвих повідомлень (Dead-Letter Queue)

Як ви знаєте, при впровадженні патерну повторних спроб, ми встановлюємо фіксовану кількість спроб. Адже ми не можемо безкінечно повторювати спроби. Якщо ми досягаємо ліміту спроб, але все ще не можемо спожити подію, ми не можемо залишити цю подію без обробки. Можливо, в даних події є помилка або відсутня обов'язкова частина інформації, що спричинила помилку. Подія може бути правильною, але можливо була мережева перерва, і вона тривала довше, ніж очікувалося. У таких випадках ми повинні пропустити подію і продовжити споживати інші. Однак, ми не повинні повністю ігнорувати ці події. Важливо відправити їх до черги мертвих повідомлень (Dead-Letter Queue), щоб обробити або ігнорувати їх пізніше. Ми повинні вирішити, що робити, залежно від причини, чому подія потрапила до черги мертвих повідомлень. Якщо повідомлення має пошкоджений формат, ми можемо його ігнорувати.
Якщо проблема в системі буде вирішена, і повідомлення в черзі мертвих повідомлень (Dead-Letter Queue) є дійсними, ми можемо або надіслати їх назад до початкової черги, або безпосередньо споживати їх з черги мертвих повідомлень.

pic

Патерн повторних спроб (Retry Pattern) та Черга мертвих повідомлень (Dead-Letter Queue)

Упорядкування подій

Іноді порядок спожитих повідомлень може бути важливим. Якщо сервіс споживача працює в кількох екземплярах, забезпечити правильний порядок споживання подій може бути складно. Тут важливими є можливості брокера повідомлень або системи черг повідомлень, яку ви використовуєте. Якщо порядок має значення, було б корисно вибрати відповідний інструмент.

Наприклад, Kafka гарантує, що повідомлення в межах одного партиції будуть спожиті в порядку, завдяки своїй структурі партицій та ключів партицій. Важливо ретельно обирати дані для використання як ключ партиції.

У RabbitMQ гарантувати порядок складніше. Подібно до структури партицій Kafka, ви можете групувати повідомлення за певним ключем і розподіляти їх по різних чергах, підключаючи споживача до кожної черги для досягнення рішення. Однак це вимагає більше зусиль порівняно з Kafka. Крім того, якщо механізми підтвердження на стороні споживача або виробника не використовуються належним чином, і повідомлення не вдалося доставити, воно буде надіслано повторно, що призведе до порушення порядку.

Як альтернативне рішення для систем, де не можна використовувати Kafka, ми можемо додати номер послідовності або мітку часу в усі повідомлення, що дозволить споживачу перевстановлювати порядок повідомлень, коли це необхідно.

Гладке оброблення простою брокера повідомлень

Брокер повідомлень може бути тимчасово недоступний з різних причин. Це може бути через збої в мережі, або ж через вузькі місця в обмежених ресурсах брокера, таких як процесор, пам'ять або диск, що може призвести до значно довшої простою. Ми очікуємо, що сервіси споживачів будуть готові до таких ситуацій.

Сервіс споживача, який активно споживає повідомлення з брокера повідомлень, повинен продовжувати працювати стабільно, навіть якщо брокер тимчасово не працює. Звісно, він не зможе споживати події, але після того як брокер знову стане доступним, ми очікуємо, що він відновить споживання повідомлень з того місця, де він зупинився. Потрібно реалізувати сервіс відповідно.
Патерн повторних спроб (Retry Pattern) та експоненціальне відставання (exponential backoff), про які ми згадували в попередньому розділі, можуть бути реалізовані й тут, дозволяючи вам знову підключитися після того, як брокер стане доступним. Якщо вам потрібно перезапустити сервіс споживача після відновлення роботи брокера для продовження споживання повідомлень, велика ймовірність, що з вашим впровадженням щось не так.

Якщо проста брокера повідомлень триває довго, процеси повторних спроб можуть негативно вплинути на продуктивність системи. У цьому випадку ми можемо впровадити патерн запобіжного вимикача (Circuit Breaker). Запобіжний вимикач може припинити обробку нових повідомлень, поки брокер повідомлень знову не стане доступним.

Висновок

У розподілених системах забезпечення надійної асинхронної комунікації на основі подій є критично важливим. У перших трьох частинах ми обговорили дії, які потрібно виконати для надійної реалізації цієї комунікації як на стороні виробника, так і на стороні споживача. Як ви можете зрозуміти, ми ще не торкнулися теми масштабування, що також варто згадати. Ми розглянемо це питання в наступному розділі, як з точки зору виробника/споживача, так і з точки зору вибору інструменту брокера повідомлень.
Отже, ми завершуємо серію, докладно розглянувши всі ключові концепції, які необхідно знати для досягнення надійної та стійкої системи, орієнтованої на події, від початку до кінця.

Перекладено з: Scalability and Reliability in Event-Driven Systems: Ensuring Reliable Event Consumption.

Leave a Reply

Your email address will not be published. Required fields are marked *