Виникла цікава оптимізаційна архітектурна задачка. Може я дарма над нею задумався. Сама задачка полягає в тому що:
Якщо намалювати діаграму "великів квадартів", що за звичай розглядають на великих екранах за довгими столами, то виглядадтиме це приблизно так. І НЕ ПРАВИЛЬНО. ВОНО НЕ ПРАЦЕЗДАТНЕ. Це стиль думання монолітом.
graph TD
ES1["Зовнішній Сервіс 1"] -- "HTTP POST" --> MyApp["Наш додаток"]
MyApp -- "Збереження даних" --> DB["База Даних"]
DB -- "Читає та збагачує дані " --> MyApp
MyApp -- "Відправляє отримані дані" --> ES3["Зовнішній Сервіс 3 (Ненадійний Endpoint)"]
Виникає питання чому?
На цій діаграмі відстні елементи взаємодії пор http.
Якщо "Наш додаток" почне http взаємодію з "Зовнішній Сервіс 3" не закривши з'єднання з "Зовнішній Сервіс 1", то "Зовнішній Сервіс 1" отримає помилку, що іеіційована http взаємодією з "Зовнішній Сервіс 3".
Якщо "Наш додаток" почне http взаємодію закривши з'єднання з "Зовнішній Сервіс 1", але отримає помилку - то вона запишеться десь в логи і ми будемо решгулярно дивитися в ті логи і казати, що: "у вас сервіс не достпний". А знайти причину не доступності буде досить складно - тому що вона часто криється на мережевому рівні.
А кщо у вас "Зовнішній Сервіс 3" взагалі "ліг" на кілька днів, то треба або зупинити додаток "Наш додаток", або бути готовим читати велику кількість логів з помилками, взагалі то і не портібних. Але вони будуть споживати дисковий простір, пам'ять та процесорний ресурс. Але, якщо ви зупините додаток, то буде не заслужено страждати "Зовнішній Сервіс 1", тому що він стукається до "Наш додаток", а тм нічого немає.
І так складається такий собі ланцюжок неприємностей. Тому на великих екранах за довгими столами вже навчилися малювати жирну лінію і казати що отут буде черга.
graph TD
subgraph "Взаємодія із Зовнішнім Сервісом 1"
ES1["Зовнішній Сервіс 1"] -- "HTTP POST" --> MyApp["Наш додаток отримувач даних"]
MyApp -- "Збереження даних" --> DB["База Даних"]
DB -- "Читає та збагачує дані " --> MyApp
MyApp -- "Відправляє отримані дані" --> MessageQueue["Черга повідомлень для зовнішнього сервіса"]
end
subgraph "Взаємодія із Зовнішнім Сервісом 3"
MessageQueue -- "Отримує повідмолення" --> MyAppConsumer["Наш додаток-споживач повідомлень"]
MyAppConsumer -- "Відправляє повідомлення по http" --> ES3["Зовнішній Сервіс 3 (Ненадійний Endpoint)"]
end
На цій діаграмі моноліт ""Наш додаток" розбито на два компоненти:
По суті це два не залежні компоненти. Вони зв'язані між собою "м'яким зв'язком". Тобто, зупинка компонента "Наш додаток-споживач повідомлень" - ніяким чином не вплине на "Наш додаток отримувач даних". Просто в черзі будуть накопичуватися повідомлення для "Зовнішній Сервіс 3". А от коли "Наш додаток-споживач повідомлень"запустять, то "Зовнішній Сервіс 3" отримає всі повідмолення, що накопичилися за час простою. Вже трошки краще, але всерівно не зрозуміло, коли даємо відповідь "Зовнішній Сервіс 1" по http. Так як "Зовнішній Сервіс 3" є за визначенням не надійним, то при короткострокових проблемах інфраструктури, чи "Зовнішній Сервіс 3" раптом "втомиться" - то всі ці повідомлення вилетять тільки в лог з помилками. Знову ж таки хорошого в цьому мало, тому, що копирасатися в логах і пояснювати власникам "Зовнішній Сервіс 3", чому по таймаут (проблеми мережі) нічого не дішло - теж справа безнадійна, особливо коли немає впливу на інфраструктуру. І по діаграмі не зрозуміло, а що робити коли Зовнішній Сервіс 3" почне відповідати помилками.
Отут і настає привід для вивчення і використання архітектурних шаблонів, а саме для цього і використовують 3. Сценарій 2: Асинхронна обробка з можливістю тимчасових помилок (Банківські платежі).
Перш ніж малювати архітектуру, спробую розказати, як це прауює в IBM MQ. Найпростіший та рекомендований варіант Retry Pattern, це варіант мінімально вимагає додаткової логіки в ACE, покладаючись на вбудовані можливості IBM MQ.
Налаштування IBM MQ (Обов'язково):
INPUT.QUEUE (Моя вхідна черга, наприклад psh.in2):
На черзі треба встановити чергу проблемних повідомлень BACKOUT.QUEUE за допомогою QueMnager UI або за допомогою магії команд mqsi. В даному випадку це буде psh.in.
На черзі треба встановит значення BOTHRESH(3): Встановіть поріг відкату. 3 є гарним стартовим значенням. Це означає, що повідомлення буде спробувано 3 рази.
На QueManager треба встановити параметри BOINTERVAL: Цей параметр надзвичайно важливий для боротьби з короткочасними негараздами. Він визначає затримку в мілісекундах, перш ніж повідомлення, яке було відкочено в INPUT.QUEUE, буде знову доступним для споживання з INPUT.QUEUE. Це дозволяє зовнішньому сервісу відновитися після тимчасових збоїв. АЛЕ ЦЕЙ ПАРАМЕТРИ ВСТАНОВЛЮЄТЬСЯ НА ВЕСЬ QUEUE MANAGER. А ЩЕ В QUEUE MANAGER ВЕРСІЇ 9 ВІН ВІДСТНІЙ В UI. ЙОГО МОЖНА ВСТАНОВИТИ ТІЛЬКИ МАГІЄЮ КОМАНД MQSI.
graph TD
subgraph "Взаємодія із Зовнішнім Сервісом 1"
ES1["Зовнішній Сервіс 1"] -- "HTTP POST" --> MyAPI["Наш API (Приймальний Сервіс)"]
MyAPI -- "Збереження даних" --> DB["База Даних"]
DB -- "Читає та збагачує дані" --> MyAPI
MyAPI -- "Публікує повідомлення" --> MessageQueue["Черга Повідомлень (IBM MQ)"]
MyAPI -- "HTTP 200 OK" --> ES1
end
subgraph "Обробка та Доставка Даних (Наша Система)"
MessageQueue -- "Споживає повідомлення (Асинхронно)" --> RetryWorker["Воркер Retry (IBM ACE Flow)"]
RetryWorker -- "HTTP POST (Може провалитись)" --> ES3["Зовнішній Сервіс 3 (Ненадійний Endpoint)"]
subgraph "Механізм Retry для ES3"
RetryWorker -- "Короткочасний збій/Retry" --> MessageQueue
MessageQueue -- "Якщо перевищено ліміт спроб (BOTHRESH)" --> DLQ["Черга Dead-Letter (IBM MQ BACKOUT.QUEUE)"]
DLQ -- "Моніторинг/Ручне втручання/Переміщення" --> Human["Оператор / Моніторинг"]
end
end
style MyAPI fill:#f9f,stroke:#333,stroke-width:2px
style DB fill:#ccf,stroke:#333,stroke-width:2px
style MessageQueue fill:#bbf,stroke:#333,stroke-width:2px
style RetryWorker fill:#fcf,stroke:#333,stroke-width:2px
style ES3 fill:#f66,stroke:#333,stroke-width:2px
style DLQ fill:#fbb,stroke:#333,stroke-width:2px
Пояснення архітектурної діаграми ("великих квадратів"):
"External Service 1 Interaction" (Взаємодія із Зовнішнім Сервісом 1):
Зовнішній Сервіс 1 (ES1): Відправляє дані на наш API.
Наш API (Приймальний Сервіс): Отримує дані, успішно їх обробляє (зберігає в базу даних) і негайно відповідає ES1 зі HTTP 200 OK. Це робить взаємодію з ES1 синхронною та швидкою, незважаючи на подальшу обробку.
База Даних (DB): Зберігає отримані дані. Це перше місце, де дані стають "безпечними" і зберігаються.
"Data Processing & Delivery (Our System)" (Обробка та Доставка Даних - Наша Система):
Процесор Даних (Воркер): Цей компонент відповідає за вичитування щойно збережених даних з бази даних, їх збагачення та трансформацію. Це може бути окремий мікросервіс, функція бази даних (тригер), або інший воркер, який активується після запису в DB.
Черга Повідомлень (Напр. IBM MQ): Це ключовий елемент архітектури. Замість прямого виклику ненадійного сервісу 3, процесор даних асинхронно відправляє підготовлене повідомлення до черги.
Перевага: Якщо Сервіс 3 недоступний, повідомлення просто чекає в черзі, не блокуючи процесор даних і не змушуючи його повторювати спроби відправки. Черга виступає як буфер.
Воркер Retry (Напр. IBM ACE Flow): Це окремий компонент (у вашому випадку, це ваш ACE Flow), який споживає повідомлення з черги. Він відповідає за фактичну відправку повідомлення на Зовнішній Сервіс 3.
Оскільки він працює асинхронно, він може мати власну внутрішню логіку retry.
Зовнішній Сервіс 3 (Ненадійний Endpoint): Це той самий "вередливий" сервіс, який може мати короткочасні та довготривалі проблеми.
"Retry Mechanism for ES3" (Механізм повторних спроб для Сервісу 3):
Короткочасний збій/Retry: Якщо RetryWorker не може достукатися до ES3 (мережа, таймаут, перший запит не прийнятий), він відкатує транзакцію (викидає виняток, як ми обговорювали). Повідомлення повертається до тієї ж Черги Повідомлень, але з лічильником відкатів, і чекає на BOINTERVAL перед наступною спробою.
Черга Dead-Letter (DLQ - Напр. IBM MQ BACKOUT.QUEUE): Якщо повідомлення перевищує заданий ліміт спроб (ваші BOTHRESH), воно автоматично переміщується до DLQ.
Оператор / Моніторинг: DLQ є сигналом про серйозніші проблеми. Оператори або системи моніторингу повинні стежити за цією чергою, щоб виявляти постійно проблемні повідомлення та втручатися вручну (наприклад, виправити дані, перевірити доступність ES3, перемістити повідомлення для повторної обробки вручну).
Ця архітектура чітко розділяє синхронну відповідь першому сервісу від асинхронної, стійкої доставки даних третьому сервісу, компенсуючи його ненадійність за рахунок буферизації та механізмів retry в черзі повідомлень. Це класичний приклад шаблону "Producer-Consumer" з додатковими шарами стійкості.
graph TD
subgraph "Взаємодія із Зовнішнім Сервісом 1"
ES1["Зовнішній Сервіс 1"] -- "HTTP POST" --> MyAPI_Node["Наш API (Node.js)"]
MyAPI_Node -- "Збереження даних" --> DB_Node[(База Даних)]
MyAPI_Node -- "HTTP 200 OK" --> ES1
end
subgraph "Обробка та Доставка Даних (Наша Система Node.js + RabbitMQ)"
DB_Node -- "Тригер/API-виклик/Воркер" --> DataProducer_Node["Node.js Data Producer"]
DataProducer_Node -- "JSON Трансформація та Збагачення" --> DataProducer_Node
DataProducer_Node -- "Публікує повідомлення" --> RabbitMQ["Брокер повідомлень (RabbitMQ)"]
subgraph "RabbitMQ Retry Mechanism"
RabbitMQ -- "Відправляє до" --> OriginalQueue["Основна Черга (Напр. 'data.to.es3')"]
OriginalQueue -- "Споживає" --> Worker_Node["Node.js Worker (для ES3)"]
Worker_Node -- "HTTP POST (Може провалитись)" --> ES3_Node["Зовнішній Сервіс 3 (Ненадійний Endpoint)"]
Worker_Node -- "Відхилення (NACK)" --> OriginalQueue
OriginalQueue -- "Якщо NACKed/TTL Expired" --> DLX["Dead Letter Exchange (DLX)"]
DLX -- "Маршрутизує до" --> RetryQueue["Retry Черга (З TTL для затримки)"]
RetryQueue -- "TTL минає" --> DLX --> OriginalQueue
RetryQueue -- "Якщо перевищено retry ліміт" --> FailedQueue["Failed/Parked Черга"]
FailedQueue -- "Моніторинг/Ручне втручання" --> Human_Node["Оператор / Моніторинг"]
end
end
style MyAPI_Node fill:#f9f,stroke:#333,stroke-width:2px
style DB_Node fill:#ccf,stroke:#333,stroke-width:2px
style RabbitMQ fill:#A0D468,stroke:#333,stroke-width:2px
style OriginalQueue fill:#E4E6EA,stroke:#333,stroke-width:2px
style Worker_Node fill:#fcf,stroke:#333,stroke-width:2px
style ES3_Node fill:#f66,stroke:#333,stroke-width:2px
style DLX fill:#ADD8E6,stroke:#333,stroke-width:2px
style RetryQueue fill:#FFB6C1,stroke:#333,stroke-width:2px
style FailedQueue fill:#E6B0AA,stroke:#333,stroke-width:2px
Пояснення архітектурної діаграми (Node.js + RabbitMQ):
"Взаємодія із Зовнішнім Сервісом 1":
Наш API (Node.js): Залишається аналогічним, приймає запит, зберігає в базу даних і відповідає HTTP 200 OK. Node.js тут виступає як ефективна платформа для швидких I/O операцій.
База Даних (DB): Зберігає дані.
"Обробка та Доставка Даних (Наша Система Node.js + RabbitMQ)":
Node.js Data Producer: Цей Node.js воркер читає дані з DB (або активується тригером), збагачує та трансформує їх.
Брокер повідомлень (RabbitMQ): Замість IBM MQ, тут використовується RabbitMQ. Producer публікує повідомлення в RabbitMQ.
"RabbitMQ Retry Mechanism" (Механізм Retry в RabbitMQ):
Основна Черга ('data.to.es3'): Повідомлення спочатку надходить сюди.
Node.js Worker (для ES3): Це Node.js додаток (споживач), який читає повідомлення з 'Основної Черги'. Він виконує HTTP POST запит до Зовнішнього Сервісу 3.
Зовнішній Сервіс 3 (Ненадійний Endpoint): Той самий проблемний сервіс.
Механізм Retry в RabbitMQ (основна відмінність):
Відхилення (NACK): Якщо 'Node.js Worker' не може доставити повідомлення до ES3 (через збій, таймаут тощо), він відхиляє повідомлення (NACK) з опцією requeue: false. Це запобігає негайному поверненню повідомлення в оригінальну чергу.
Dead Letter Exchange (DLX) і Dead Lettering: 'Основна Черга' конфігурується з аргументом x-dead-letter-exchange. Коли повідомлення відхиляється, воно автоматично перенаправляється в DLX.
Retry Черга (З TTL для затримки): DLX маршрутизує повідомлення до спеціальної 'Retry Черги'. Ця 'Retry Черга' налаштована з x-message-ttl (Time-To-Live) та x-dead-letter-exchange, що вказує на той самий DLX, з якого повідомлення прийшло.
Як це працює: Повідомлення перебуває в 'Retry Черзі' протягом встановленого TTL. Після закінчення TTL, воно "dead-lettered" назад у DLX, який потім маршрутизує його назад до 'Основної Черги'. Це створює цикл повторних спроб із затримкою.
Якщо перевищено retry ліміт: 'Node.js Worker' (або логіка в 'Retry Черзі' через x-max-length) може вести власний лічильник спроб (наприклад, у заголовках повідомлення). Якщо лічильник досягає певного порогу, повідомлення переміщується до 'Failed/Parked Черги'.
Failed/Parked Черга: Це аналог BACKOUT.QUEUE в IBM MQ. Сюди потрапляють повідомлення, які не вдалося обробити після всіх спроб.
Оператор / Моніторинг: Як і раніше, ця черга потребує моніторингу та, можливо, ручного втручання.
Ключові відмінності порівняно з IBM MQ + ACE:
Розподіл функціоналу Retry: У RabbitMQ механізм затримки та перенаправлення для retry (через TTL, DLX та окрему Retry Queue) є більш явним і налаштовується на рівні RabbitMQ. В IBM MQ це відбувається через BOINTERVAL та BOTHRESH на самій вхідній черзі.
Управління лічильником спроб: У RabbitMQ часто доводиться реалізовувати логіку лічильника спроб (наприклад, у заголовках повідомлення) у самому воркері Node.js. У IBM MQ MQMD.BackoutCount є вбудованим і автоматично підтримується менеджером черг.
Гнучкість Retry-сценаріїв: RabbitMQ, з його DLX/TTL механізмом, надає більшу гнучкість для створення складних retry-графіків (наприклад, експоненціальна затримка) без потреби в окремих воркерах, які постійно повертають повідомлення.
Стек технологій: Відхід від корпоративного стеку IBM до більш "легковажних" рішень Node.js та RabbitMQ.