Підводні камені асинхронного обміну повідомленнями

Мене звати Юрій Івон, я співпрацюю з компанією EPAM як Senior Solution Architect. Хотів би поділитися досвідом розробки систем, що використовують асинхронний обмін повідомленнями, а саме можливими проблемами та способами їх вирішення.

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

Транзакції

Одним із типових сценаріїв асинхронного обміну повідомленнями є сповіщення підписників про зміни в даних.

A diagram of a messageDescription automatically generated

На високому рівні типовий код, який надсилає повідомлення, виглядає так:

  1. Почати транзакцію бази даних.
  2. Змінити дані.
  3. Завершити транзакцію.
  4. Надіслати повідомлення.

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

Якщо втрата повідомлення в цьому сценарії є критичною для вашої системи, існують, по суті, лише два практичні підходи для розв’язання проблеми: патерн Transactional Outbox та розподілені транзакції.

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

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

Таким чином, оптимальним способом забезпечення надійності є патерн Transactional Outbox. Я не збираюся заглиблюватися в специфіку його реалізації, але основна концепція передбачає запис повідомлення в таблицю вихідних повідомлень (outbox) в тій самій базі й в одній транзакції з основними змінами. Як тільки транзакція завершиться, окремий процес або служба отримує повідомлення з цієї таблиці та публікує їх в брокер. Є варіанти, коли служба надсилає повідомлення тільки у випадку, якщо основна програма не змогла його опублікувати, але загальний принцип залишається незмінним.

Має сенс подивитися на те, як зазвичай обробляється таблиця вихідних повідомлень:

  1. Прочитати запис із таблиці вихідних повідомлень.
  2. Відправити відповідне повідомлення в шину.
  3. Видалити запис або позначити його як відправлений.

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

Ми говорили про потенційні проблеми під час публікації, але аналогічна ситуація може виникнути й на стороні процесора повідомлень:

  1. Заблокувати повідомлення для його обробки.
  2. Почати транзакцію бази даних.
  3. Змінити дані.
  4. Завершити транзакцію.
  5. Підтвердити успішну обробку повідомлення.

Що робити, якщо п’ятий крок не вдається виконати через проблему з брокером повідомлень або збій у вашому сервісі?

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

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

Таким чином, найпростіший спосіб уникнути проблем у випадку, якщо Outbox видає дублікати або є проблеми з підтвердженням повідомлень — це зробити логіку процесора ідемпотентною, що вказує нам на інший патерн під назвою Idempotent Consumer. Те, як реалізується цей шаблон, може відрізнятися залежно від типу повідомлень і конкретної бізнес-логіки. Однак основний принцип завжди полягає в тому, щоб мати засоби перевірки, чи було поточне повідомлення вже оброблене. У найпряміший спосіб це може включати зберігання всіх оброблених ідентифікаторів повідомлень. Також цього можна досягти перевіркою існування сутності для команд, які створюють нові сутності, та використанням версіонування для інших. Деякі деталі реалізації версіонування обговоримо далі в розділі «Порядок повідомлень».

Я також хотів би виділити деякі наївні «патерни», яких слід уникати, коли ми маємо справу з обміном повідомленнями в контексті більших бізнес-транзакцій:

  • Надсилання сповіщення про зміни в даних перед завершенням транзакції — вочевидь це може призвести до некосистентності між системами, оскільки транзакція може бути відкочена в той час, коли повідомлення вже було оброблено споживачами. Зазвичай це робиться навіть не навмисно і є швидше результатом додавання коду відправки повідомлень всередину методу, неявно загорнутого в транзакцію бази даних.
  • Видалення повідомлення з черги перед обробкою — деякі брокери дозволяють обійти шаблон «блокування/підтвердження» та негайно видалити поточне повідомлення з черги. При такому підході у нього не буде другого шансу, а це означає, що якщо обробка завершиться невдало, повідомлення буде втрачено назавжди.

Якщо вам потрібно забезпечити узгодженість між публікацією/обробкою повідомлень і пов’язаними з ними змінами в базі даних, розгляньте патерни Transactional Outbox та Idempotent Consumer, а також уникайте «новачкових» помилок, згаданих вище.

Обробка проблемних повідомлень

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

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

Багато розробників знайомі з концепцією Dead Letter Queue (DLQ) і можуть одразу відповісти, що всі повідомлення, які не можуть бути оброблені, повинні надходити туди. Однак як слід поводитися з проблемними повідомленнями та хто несе відповідальність за їх аналіз і повторну обробку? Коротка відповідь полягає в тому, що команда підтримки має бути проінформована про нові недоставлені повідомлення (dead letter messages), а також має бути інструмент, що дозволяє інженерам підтримки переглядати та повторно надсилати повідомлення для обробки.

Основна проблема в тому, що не всі брокери мають зручний інструментарій для подібних цілей:

  • Інструмент управління RabbitMQ дозволяє користувачам переглядати перші N повідомлень у черзі, але це не допомагає, якщо їх вже сотні або більше. Крім того, неможливо перемістити лише вибрані повідомлення назад до вихідної черги.
  • Консоль Amazon SQS (Simple Queue Service) дозволяє користувачам переглядати та видаляти певні повідомлення в черзі, але немає простого способу надіслати їх повторно до вихідної.

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

Немає межі досконалості — ви можете створити кастомний менеджер DLQ, який зробить все, що може забажати інженер підтримки, але оскільки ресурси розробки зазвичай обмежені, краще визначити мінімум, який очікується від таких інструментів.

  • Переглянути всі повідомлення та їх вміст — must have.
  • Повторно надіслати перші N або всі повідомлення до вихідної черги — must have.
  • Видалити перші N або всі повідомлення — must have. Без цього не вийде позбутися проблемних повідомлень.
  • Повторно надіслати обрані повідомлення до вихідної черги — nice to have. Оскільки черга є черга, операції з довільними повідомленнями можуть бути недоступні «з коробки», тому я не вважаю цю функцію обов’язковою.
  • Видалити обрані повідомлення — nice to have.
  • Змінити вміст повідомлення — хоч і може бути зручно мати спосіб виправлення проблемних повідомлень вручну, цю функцію краще повністю вимкнути, оскільки ручне втручання може спричинити неузгодженість даних, яку важко діагностувати.
  • Фільтрувати та сортувати повідомлення за їхніми атрибутами — nice to have, але не обов’язково. Ця функція може спростити аналіз DLQ, але її впровадження, скоріше за все, вимагатиме перенесення проблемних повідомлень у базу даних, а також розробки спеціального інструменту для керування ними.

Таким чином, мінімально необхідним набором функцій для інструменту управління DLQ є перегляд всіх повідомлень, повторна відправка перших N або всіх повідомлень у вихідну чергу і видалення перших N або всіх повідомлень. В ідеалі всі видалення, зроблені вручну, повинні реєструватися в контрольному журналі, що дозволить команді підтримки за необхідності відстежити життєвий цикл повідомлення.

Хоча інструмент керування DLQ має вирішальне значення для обробки помилок, сам по собі він недостатній. Для ефективної обробки помилок у рішенні, що використовує асинхронний обмін повідомленнями, знадобляться всі наступні інструменти:

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

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

Порядок повідомлень

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

  • Повторна обробка повідомлень після перегляду Dead Letter Queue (DLQ) — якщо ваша система змогла обробити будь-які інші повідомлення після того, як хоча б одне з них викликало помилку і потрапило в DLQ, будь-яка спроба повторної обробки означатиме порушення початкового порядку.
  • Кілька паралельних процесорів, які читають одну й ту саму чергу — щоб прискорити обробку повідомлень, ви можете запустити кілька процесорів одночасно, однак, якщо вони будуть читати одну й ту саму чергу, порядок обробки неминуче відрізнятиметься від початкової послідовності публікації.

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

  • Уникати використання DLQ.

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

  • Уникати одночасної роботи декількох процесорів з однією і тією ж чергою.

Весь потік повідомлень може бути логічно розбитий на розділи (partitions), кожен з яких може оброблятися окремим процесором незалежно від інших. Наприклад, повідомлення можуть бути розбиті за клієнтом, за типом повідомлення, за типом сутності тощо. Але майте на увазі, що між повідомленнями, що належать до різних розділів, не повинно бути логічних залежностей. В протилежному випадку вам все одно доведеться розв’язувати проблему порядку. Я пам’ятаю проєкт, де для кожного типу сутностей виділяли окремий топік в Kafka. Однак деякі типи мали parent-child зв’язки між собою, що призводило до складної взаємодії між відповідними процесорами, оскільки «діти» могли потрапити в обробку раніше своїх «батьків».

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

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

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

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

Існує принаймні два широко використовуваних підходи для забезпечення порядку оновлень на основі версій:

Перевірка базової версії — кожне повідомлення містить базову версію сутності та нову версію, яку буде їй надано у разі успішного оновлення. Система перевіряє, чи збігається базова з поточною версією сутності в базі даних. Якщо так, оновлення виконується. Якщо ні, повідомлення надсилається до Dead Letter Queue (DLQ). По суті, це класична перевірка Optimistic Concurrency, але з додатковим полем для інформування споживачів про нову версію. Пізніше в розділі я трохи докладніше розповім про необхідність цього поля залежно від напрямку потоку даних.

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

Наведена нижче діаграма ілюструє загальну ідею обох підходів.

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

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

  • За підходу перевірки базової версії помилка при обробці одного повідомлення призводить до помилок обробки всіх наступних для тієї ж сутності, аж доки проблему не буде усунуто та всі проблемні повідомлення не будуть повторно відправлені на обробку.
  • Монотонне версіонування передбачає відправлення повного вмісту сутності з кожним повідомленням, що може спричинити значні накладні витрати. Щоб їх зменшити, можна логічно розділити весь набір полів на кілька частин і призначити кожній з них окреме поле версії. У випадку змін в даних потрібно буде надсилати лише ті групи полів, в яких ці зміни відбулися. Логіка керування версіями в цьому варіанті залишається незмінною, але тепер застосовується на більш детальному рівні. Підхід з перевіркою базової версії не має такого обмеження.
  • Підхід з перевіркою базової версії може використовувати будь-який генератор, якщо він гарантує унікальні версії для однієї й тієї ж сутності, тому навіть простий GUID підійде. На відміну від цього, монотонне версіонування потребує централізованого генератора зі збереженням поточного стану. Якщо рішення вже використовує реляційну базу даних, це не має бути проблемою, оскільки більшість реляційних баз даних підтримують послідовності (sequences) або пропонують спеціалізований тип стовпця для цієї мети, такий як «rowversion» у SQL Server. Якщо в базі даних немає подібної функціональності, для розв’язання проблеми можна використати зовнішні сервіси, такі як ZooKeeper або Redis. Однак використання таких сервісів додає складності, якщо вони ще не є частиною рішення.

Тут я хотів би наголосити, що звичайні часові мітки слід уникати для цілей версіонування сутностей, оскільки вони недостатньо точні для визначення порядку подій. Різні сервери можуть давати трохи різні часові мітки в той самий момент часу через відхилення годинників (clock skew), і роздільна здатність часових міток може бути недостатньою для надійного визначення порядку подій, що відбуваються дуже близько одна від одної.

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

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

Наведені вище міркування можна узагальнити наступним чином:

  • Завжди, коли у вас є Dead Letter Queue, існує висока ймовірність отримання повідомлень в неправильному порядку.
  • Залежно від природи повідомлень особлива обробка порушень порядку може і не знадобитися, як у випадку з телеметрією.
  • Версіонування — це типове рішення для запобігання неконсистентності даних, спричиненої проблемами з порядком повідомлень. Існує кілька варіантів його реалізації, і вибір залежить від багатьох факторів, розглянутих раніше в цьому розділі.

Керування контрактами

У світі HTTP REST такі інструменти, як Swagger та OpenAPI Specification, відіграють важливу роль у визначенні, документуванні та тестуванні API-контрактів. Вони допомагають розробникам стандартизувати визначення API, забезпечуючи узгодженість і прозорість у комунікації між різними сервісами та системами.

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

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

Щоб розв’язувати ці проблеми та зменшити ймовірність помилок, краще використовувати спеціалізовані інструменти та фреймворки.

Найбільш повним фреймворком для керування асинхронними контрактами, доступним на цей чаc, є AsyncAPI. Він надає спільну «мову» та набір інструментів, що дозволяє розробникам визначати та документувати API у стандартизованому форматі. Це додатково спрощує створення документації, дозволяє генерувати код клієнта та сервера, а також покращує загальну сумісність та легкість обслуговування систем, які базуються на асинхронному обміні повідомленнями.

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

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

Для проєктів, що використовують повідомлення у форматі JSON, Pact.io теж буде корисним інструментом. Він призначений для тестування контрактів, керованих споживачем, що дозволяє командам визначати та перевіряти контракти між сервісами для забезпечення сумісності. Це допомагає підтримувати чітко визначені структури повідомлень і гарантує, що будь-які зміни в API не порушать існуючу функціональність.

Так чи інакше, управління контрактами має бути максимально ефективним.

Тестування

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

Одним із найважливіших аспектів, що потребує уваги на додачу до регулярних тестів, є випадки, пов’язані з аномаліями доставки повідомлень:

  • Що відбувається з повідомленнями, які неможливо обробити.
  • Як обробляються дубльовані повідомлення.
  • Як обробляються повідомлення, отримані в неочікуваному порядку.

Найпростіший спосіб реалізації такого роду тестів — це публікація зразкової серії проблемних повідомлень безпосередньо з тестового скрипта та очікування результатів.

Також важливо перевірити поведінку системи у випадку проблем з підключенням:

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

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

  • Код застосунку — він може містити спеціальні обробники, що симулюють відсутність підключення, якщо застосунку надано відповідні інструкції через конфігурацію або команди.
  • Мережева інфраструктура — якщо ви можете контролювати мережеве середовище навколо ваших сервісів за допомогою firewall або service mesh, це може бути найгнучкішим підходом.
  • Брокер повідомлень — ви можете за потреби зупиняти або перезапускати брокер повідомлень, хоча цей підхід може не підходити для хмарних сервісів.

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

Ще одна складність полягає в остаточній узгодженості (eventual consistency) асинхронних систем, де немає певного моменту, коли ваші умови точно будуть виконані. Відправник може надіслати команду споживачам, але час обробки цієї команди може відрізнятися залежно від споживача. Щоб врахувати це в автоматизованому тестуванні, в перевірку умов необхідно додати повтори та тайм-аути для обробки можливих затримок.

Деякі проєкти не використовують постійне середовище для інтеграційних тестів, а замість цього запускають усі компоненти рішення в контейнерах. Це дозволяє розгортати рішення будь-де незалежно від конкретного хмарного середовища. Такий підхід має свої переваги та недоліки, які виходять за рамки цієї статті, але слід зазначити, що деякі хмарні брокери повідомлень, такі як Azure Service Bus, наразі можуть ще не мати доступного контейнеризованого еквівалента. Тому, застосовуючи цей підхід для інтеграційного тестування, переконайтеся, що обраний вами брокер повідомлень можна запустити у контейнері.

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

Висока доступність та аварійне відновлення

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

Перша проблема високої доступності та аварійного відновлення полягає в тому, що ті, хто додає брокер повідомлень у рішення, можуть не повністю враховувати пов’язані зусилля з налаштування та обслуговування. Досить легко запустити Kafka або будь-який інший брокер локально за допомогою загальнодоступного образа Docker, але налаштування конфігурації для не-PaaS брокера в продакшені та її підтримка є зовсім іншим завданням. Тому завжди враховуйте операційну складність технології при прийнятті подібних рішень.

Хмарні рішення зазвичай мають вбудоване резервування (redundancy), підтримують аварійне перемикання на інший регіон і загалом значно простіші у налаштуванні. Однак я наполегливо рекомендую уважно прочитати документацію про підтримувані можливості та обмеження. Кілька років тому я розробляв архітектуру для системи, яка використовувала Azure Service Bus і мала бути розгорнута у кількох регіонах. Тоді я був здивований, виявивши, що не існує вбудованого механізму для реплікації повідомлень між регіонами. В результаті, якщо якийсь регіон виходив з ладу, всі необроблені повідомлення в ньому втрачалися.

На той момент мені довелося переглянути дизайн, але, схоже, проблему нарешті вирішили та геореплікація для Azure Service Bus вже доступна, хоч і у preview.

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

Важливо розуміти складність реалізації високої доступності та аварійного відновлення для брокера повідомлень і забезпечувати належне планування та підготовку задовго до запуску системи в продакшен.

Компроміс між затримкою та пропускною здатністю

При використанні асинхронних систем комунікації, таких як брокери повідомлень, одним із важливих компромісів є баланс між затримкою (latency) та пропускною здатністю (throughput). Цей баланс є дуже важливим для забезпечення оптимальної роботи інфраструктури обміну повідомленнями.

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

Пропускна здатність (throughput) означає кількість повідомлень, які система може обробити за певний період часу. Висока пропускна здатність є життєво важливою для проєктів, що працюють з великими об’ємами даних, таких як системи збору телеметрії або рішення для онлайн-аналітики.

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

Технологічна прив’язка та відсутність абстракції

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

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

  • Для .NET — Rebus, MassTransit, NServiceBus.
  • Для Java — JMS (Java Message Service), Apache Camel, Spring Integration. Зверніть увагу, що JMS є лише специфікацією API для обміну повідомленнями, тоді як Apache Camel і Spring Integration є повноцінними фреймворками з вбудованими можливостями для маршрутизації повідомлень, трансформації та інших абстракцій вищого рівня поверх базового API для обміну повідомленнями.

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

Висновки

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

  • Коли сервісу потрібно реагувати на події з іншого сервісу, веб-хуки або періодичне опитування (polling) можуть бути прийнятними альтернативами. Але майте на увазі, що часті опитування можуть призвести до проблем із продуктивністю.
  • Якщо вам потрібно просто запустити довготривале завдання та відреагувати на його завершення, може бути достатньо підходу, що використовується в шаблоні REST API для довготривалих завдань.
  • Коли сервісу потрібно надіслати дані до основної системи, і асинхронна комунікація розглядається лише для забезпечення швидкості роботи цього сервісу, добре подумайте. Можливо, продуктивність основної системи не така вже й погана, і ви можете дозволити собі передавати дані синхронно. Що важливіше, асинхронні оновлення основної системи можуть призвести до конфліктів і потреби у складній логіці їх вирішення.
  • Якщо вам потрібна черга вхідних даних для їх послідовної обробки, залежно від масштабу вашої системи, може бути достатньо простої таблиці у звичайній базі даних, яка періодично опитується процесорами даних.

Деякі з цих альтернатив все ще можуть вимагати розв’язання певних проблем, розглянутих у статті, але не всіх одночасно.

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

Ця стаття доступна також англійською на Medium.

Сподобалась стаття? Підписуйтесь на автора, щоб отримувати сповіщення про нові публікації на пошту.

👍ПодобаєтьсяСподобалось21
До обраногоВ обраному22
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Нічого кардинально нового для себе не дізнався, але стаття, як на мене, вийшла хороша. Були моменти, де можна було би подискутувати, але то мабуть суб’єктивні моменти. Автор молодець.

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

не зовсім зрозуміло бо асинхронність же ж by design саме передбачає «відправку повідомлення» як то максимум «(синхронно) підтверджене успішне отримання та прийняття в роботу»

... і якщо там «щось ускладнює» то у вас же ж просто нема асинхронного дизайну от і все?

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

що власне якраз підтверджує що нема дизайну бо власне сьогодні з «синхронного» лишилася хіба оперативна пам’ять (здебільшого) комп’ютера а усе інше вже асінхронне ти просто кидаєш завдання у чергу а «виконавчий бік» його у себе отримує та виконує при цьому зокрема власними силами сортуючи оптимізуючи тощо

... як то зокрема забезпечуючи вирішення

конфліктів і потреби у складній логіці їх вирішення.

як то приклад імхо вже класичної на сьогодні «асинхронного дизайну» як то система логування

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

це ж симуляція черги обробки вхідних даних?

Коли сервісу потрібно реагувати на події з іншого сервісу, веб-хуки або періодичне опитування (polling) можуть бути прийнятними альтернативами. Але майте на увазі, що часті опитування можуть призвести до проблем із продуктивністю.

це про технічні низького рівня імплементації чи про принцип взагалі?

не зовсім зрозуміло бо асинхронність же ж by design саме передбачає «відправку повідомлення» як то максимум «(синхронно) підтверджене успішне отримання та прийняття в роботу»

... і якщо там «щось ускладнює» то у вас же ж просто нема асинхронного дизайну от і все?

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

що власне якраз підтверджує що нема дизайну бо власне сьогодні з «синхронного» лишилася хіба оперативна пам’ять (здебільшого) комп’ютера а усе інше вже асінхронне ти просто кидаєш завдання у чергу а «виконавчий бік» його у себе отримує та виконує при цьому зокрема власними силами сортуючи оптимізуючи тощо

... як то зокрема забезпечуючи вирішення

Можливо тут я недостатньо зрозуміло описав кейс, але неодноразово зустрічав інтеграції такого типу, коли допоміжній системі треба щось записати в майстер-систему в результаті дій користувача. Наприклад, допоміжна система працює з цінами, а бізнес захотів мати змогу змінювати в ній не тільки ціни, а й інші атрибути товарів. Інші атрибути відповідно лежать в якійсь іншій системі. І от є два варіанта — або писати не-цінові атрибути в відповідну систему синхронно, або спочатку зберігати зміни «у себе», і в фоні синхронизувати куди треба. Так от буває, що можна з фоновою синхронізацією не заморачуватися, а записати синхронно.

як то приклад імхо вже класичної на сьогодні «асинхронного дизайну» як то система логування

Логи — простий кейс, бо append-only, нема конфліктів.

це ж симуляція черги обробки вхідних даних?

Вірно, але відразу відпадає потреба мати «yet another animal in the zoo» у вигляді мессадж брокеру.

це про технічні низького рівня імплементації чи про принцип взагалі?

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

Логи — простий кейс, бо append-only, нема конфліктів.

Так на таких append only кейсах в основному і крутиться уся асинхронність.
Тобто є сервіс, є подія. Подія вже відбулась. Вона атомарна і вже не відкатиться.
Інший приклад — ордер оплачено. Сервіс a який відповідає за оплату зробив транзакцію у себе, все що треба змінив. Виплюнув це в умовну кафку і забув. Оплата була. Це ніколи не змінити
Сервіс b відповідальний за меседжі відправив повідомлення потрібним пдресатам. Ти ж не відкатиш email
Сервіс c списав там товар із stock on hand і перевів його в статус reserved
В тепер платіжний сервіс отримує інфу що сорі клієнт через банк відмінив операцію
Що робити?
Відміна це ж і у банку окрема транзакція. То ж і у нас окрема операція. Погнали
А що якщо щось технічне? У сервісу b нема товару щоб списати.
Ну сорян. Оплата пройшла. Ми не можемо однією транзакцією сказати банку йди відміняй оплату. Доречі такий підхід нормальний і для Рітейл монолітів. Часто запаси товарів уходять в мінус а потім окремими операціями вирівнюється. Зате все прозоро
Тут нова операція відміни ініційована сервісом b
Коротше якщо правильний дизайн мікросервісів то у асинхронному світі розподілені транзакції не потрібні. Якщо вони потрібні то не правильний дизайн мікросервісів і обʼєкти що живуть в різних сервісах повинні жити в одному

Світ неідеальний :) Є багато випадків коли двом системам треба змінювати одні й ті самі дані. Звісно кількість таких випадків бажано мінімізувати, але все одно трапляються. І це не тільки про числові атрибути типу залишків на складі і грошей на рахунках, а також, наприклад, про адреси та інші контактні дані, користувачів відповідальних за обробку запиту та інші. Якщо допоміжна система при збереженні даних буде синхронно взаємодіяти з основною, вона без проблем може пройти Optimistic Concurrency Check і повідомити користувачу якщо є якийсь конфлікт оновлення. Якщо робити таке оновлення асинхронно, то задача розв’язання конфліктів значно укладнюється. Саме про це я в тій частині висновків і казав.

Оплата була. Це ніколи не змінити

ну нє всьо так однозначно )) просто створюєш новий меседж на відміну оплати і так само запускаєш у систему всьо ))

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

... або не стаєш бо то вообще не твої проблеми а проблеми відділу обробки оплати а ти свою роботу виконав ти відповідно до «протоколу» (читай «дизайну асінхронного роботи системи») зі свого боку створив відповідне меседж на відміну по оплаті і тебе реально не цікавить результат бо...

(тривожна мовчанка)

... бо то і є _результат_ тобі потрібний бо ти _створив_ відповідний месадж і _зареєстрував_ його у системі належним чином

всьо!

Ти ж не відкатиш email

імєнно! )) просто пишеш і шлеш новий «ви всьо нє так понялі прошу отмєніть»

... опять же ж можна чекати на відповідь (-ді) а можна ні бо ти свою справу зробив

... а стоп чекай а у першому випадку ти чекав відповіді на перший імейл?

... тобто ось хай навіть не імейл а шоб було красіво чат тож ти писав «зустрічаємо Різдво отут» а потім шото пошло не так і треба змінити місце зустрічі бо місце зустрічі змінити не можна (к) (тм) і шо дєлать?

... а ну да ти ж просто пишеш у чат «охрана отмєна зустрічаємо Різдво не отут а отам»

всьо власне це же ж те саме що люди прочитали ще перше повідомлення і зреагували відповідно бо ж якщо вони не прочитали перше повідомлення і не зреагували відповідно то вони же ж і не знають всьодно куди їхать зустрічать Різдво?

В тепер платіжний сервіс отримує інфу що сорі клієнт через банк відмінив операцію

імєнно! ))

Що робити?

всьо пропало шеф всьо пропало (к) (тм)

Зате все прозоро

так і бухгалтери навіть придумали собі систему подвійного запису «дебіт кредіт»

то у асинхронному світі розподілені транзакції не потрібні.

ну не зовсім так просто «транзакція» прив’язується до «бізнес об’єктів» а не до «технологічної імплементації»

для прикладу «бізнес об’єкт» як то місце у кінотеатрі як окремий об’єкт але при цьому «план зайнятих місць» як геть окремий об’єкт який навіть не потребує актуалізації та транзакційності бо ж то лише певний образ на якийсь певний проміжок часу а реальна «транзакція» починається лише коли ти вибираєш місце як конкретний об’єкт і починаєш операцію оформлення квитака на конкретне місце

... як то відправляєш запит асінхронно на початок операції на що відповідно відбувається перевірка чи це місце ще не зайняте і не в обробці і у відповідь «видається токен на транзацію» який «токен» прив’язаний саме до конкретного бізнес об’єкта а не до якіх об’єктів технічної імплементації

і обʼєкти що живуть в різних сервісах повинні жити в одному

аб том же ж і річ що то є об’єкти технічної імплементації а єто другоє (к) (тм)

Тут я говорю про те, що якщо логіка взаємодії в вашій бізнес-транзакції повністю синхронна

імєнно

то у вас же ж просто нема асинхронного дизайну от і все?

Не розумію коментар, сорі. Як те, що в одному випадку немає асинхронного дизайну відміняє те, що при асинхронній взаємодії механізм обробки помилок складніший?

а користувача вже немає

а куди ж він дівся? невже тцк? ))

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

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

... що у свою чергу оформлюється просто додаванням відповідних повідомлень у чергу на обробку ))

... тож повернімося до нашіх баранів

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

отже маємо

логіка взаємодії в вашій бізнес-транзакції повністю синхронна

це загалом ні чого не значить...

тобто кожна операція в ланцюжку чекає на результат

а оце вже ближче тож слід виходити з того що є «бізнес об’єкт» і є «ланцюжок операції (над бізнес об’єктом)»

... з чого виходить що є окрема операція над бізнес об’єктом

тобто слід розглядати весь «ланцюжок операцій» складається з послідовних дій

«бізнес об’єкт вхідного стану № 1»
=>
«операція над бізнес об’єктом»
=>
«бізнес об’єкт вихідного стану № 2»

і в кінці-кінців відповідає клієнту

тож умовно спростивши до лише 1 ланки «ланцюжка» клієнт дає на вхід стан № 1 і отримує на виході стан № 2

відповідно клієнт знає чи була операція успішною чи ні.

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

№ 1 клієнт відправив запит на виконання операції над об’єктом від стану «а» до стану «б»

№ 2 клієнт за якийсь час отримав повідомлення про завершення операції (успішне або не успішне)

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

отже розширемо першу схему де була тільки 1 ланка до будь якої кількості проміжних ланок то що насправді «отримує клієнт»?

... клієнт просто отримує послідовні повідомлення про завершення операції окремих ланок «ланцюжка ланок» і власне все і одне з таких повідомлень просто повідомляє за завершення усіх ланок операції і все

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

а користувача вже немає, тому механізм обробки помилок складніший.

... повертаємося знову до місця де в нас була тільки 1 ланка «ланцюжка»

№ 1 клієнт відправив запит на виконання операції над об’єктом від стану «а» до стану «б»

№ 2 клієнт за якийсь час отримав повідомлення про завершення операції (успішне або не успішне)

тож додається уточнення

«повідомлення про завершення операції було відправлене клієнту»

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

«бізнес об’єкт вхідного стану № 1»
=>
«операція над бізнес об’єктом»
=>
«бізнес об’єкт вихідного стану № 2»

тож ми як одна ланка отримали повідомлення про зробити операцію над об’єктом зробили операцію над об’єктом і відправили «далі по ланцюжку» повідомлення про це

всьо!

Багато букв, але пішов кудись у ліс... На пальцях: кейс 1 — Користувач натиснув на кнопку «Зберегти дані продукту», почекав пару секунд і йому система показала відразу повідомлення «Продукт успішно збережено», ну або сказала «Соррі, не змогла, спробуй ще». Кейс 2 — Користувач натиснув на кнопку «Зберегти дані продукту», вона відпрацювала за секунду, і користувач пішов займатися своїми справами. Але в другому випадку продукт насправді тільки встав в чергу на обробку. І під час розгрібання черги десь на бекенді сталася яка-небудь concurrency check failure. Кого сповіщати? Ну можна користувачу на мейл помилку вислати. І ще доробити фічу, щоб від міг виправити помилку не перебиваючи всі свої зміни по новій.

В якому з кейсів ерор хендлінг простіший? Про що спор, не розумію...

І під час розгрібання черги десь на бекенді сталася яка-небудь concurrency check failure. Кого сповіщати?
В якому з кейсів ерор хендлінг простіший?

чій «ерор хендлінг»? ))

... ну в тім я зразу так і сказав що ти є архітектор ))

Можливо тут я недостатньо зрозуміло описав кейс, але неодноразово зустрічав інтеграції такого типу, коли допоміжній системі треба щось записати в майстер-систему в результаті дій користувача. Наприклад, допоміжна система працює з цінами, а бізнес захотів мати змогу змінювати в ній не тільки ціни, а й інші атрибути товарів. Інші атрибути відповідно лежать в якійсь іншій системі. І от є два варіанта — або писати не-цінові атрибути в відповідну систему синхронно, або спочатку зберігати зміни «у себе», і в фоні синхронизувати куди треба. Так от буває, що можна з фоновою синхронізацією не заморачуватися, а записати синхронно.

гм ща подумаю...

ти знаєш надто слабо визначений кейз тобто буквально х.з. «вгадувати що там є» але скоріш за все там є «синхронне було але тут тіпа придєлать асинхронне но ніхто не зна як бо...»

можна з фоновою синхронізацією не заморачуватися, а записати синхронно.

тож тупий самий варіант кейзу ти просто кидаєш свої зміни атрибутів у загальну чергу на зміну атрибутів і взагалі пофіг де ті атрибути лежать бо кожна цільова система просто вигрібає свої і просто робить собі зміни всьо профітЪ

тобто як буквально який «кейз» таке й «рішення»

треба щось записати в майстер-систему в результаті дій користувача.

і схоже на то що саме той «рішення» і є саме воно ))

Логи — простий кейс, бо append-only, нема конфліктів.

е ніт то є той самій кейз бо ж ти намагаєся на окремій системі на окремі атрибути синхронизувати загалом самого клієнта

треба щось записати в майстер-систему в результаті дій користувача.
а й інші атрибути товарів. Інші атрибути відповідно лежать в якійсь іншій системі.

тож відповідно у тебе атомізація операції має бути на рівні логіки саме клієнта а не на рівні імплементації де там вони лежать ті атрибути а отже

№ 1 дії користувача діють на об’єкт «бізнес-об’єкт»
№ 1.біс най простіша схема ексклюзивного доступу до об’єкта

№ 2 користувач для початку дій отримує токєн доступу до об’єкта
№ 2.біс отримання токєна так само асінхронно відправляє запит чекає на відповідь отримує відповідь

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

№ 4 користувач закінчив всі свої зміни після чого здає токєн доступу до об’єкта

№ 5 інший користувач для початку дій отримує токєн доступу до об’єкта
№ 5.біс отримання токєна так само асінхронно тож цей користувач так само відправив запит на отримання токєна і до цього моменту просто чекав на відповідь на отримання токєна

всьо усьо вапчє асінхронно чисто by design

ти знаєш надто слабо визначений кейз тобто буквально х.з. «вгадувати що там є» але скоріш за все там є «синхронне було але тут тіпа придєлать асинхронне но ніхто не зна як бо...»

Ніт. «ніхто не зна» — це не про мене, сорі.

тож тупий самий варіант кейзу ти просто кидаєш свої зміни атрибутів у загальну чергу на зміну атрибутів і взагалі пофіг де ті атрибути лежать бо кожна цільова система просто вигрібає свої і просто робить собі зміни всьо профітЪ

тобто як буквально який «кейз» таке й «рішення»

А якщо зміни відбуваються на тому самому об’єкті в обох системах одночасно, синхронізувати то все як?

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

е ніт то є той самій кейз бо ж ти намагаєся на окремій системі на окремі атрибути синхронизувати загалом самого клієнта

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

всьо усьо вапчє асінхронно чисто by design

Тільки клієнту доведеться чекати як мінімум на токен для того щоб почати оновлення. То що тоді ми вирішуємо?

Ще раз кейс — є дві системи А і Б, які зберігають різні атрибути одного об’єкту. Бізнес дуже захотів щоб на одному скріні з системи Б можна було оновити атрибути як з Б, так і з А, натиснути кнопку «Save» і бути впевненими що дані зберіглися або отримати помилку в протилежному випадку. Героїчний девелопер вважає що синхронний похід в систему А з Б — це не круто і зайві 3 секунди, тому починає будувати асинхронний апдейт з Б в А через мессаджінг, і т.д. Тому що так буде швидше для користувача, бо для збереження даних не треба нічого чекати від А.

В твоєму прикладі на кроці 2 вже починаємо залежати від А, бо треба ще почекати як токен видадуть.

А якщо зміни відбуваються на тому самому об’єкті в обох системах одночасно, синхронізувати то все як?

отримуєш токєн на монопольні зміни об’єкту робиш зміни вертаєш токєн

... наступний система чекає на своє отримання токєна на монопольні зміни об’єкта отримує токєн робить зміни вертає токєн

всьо!

це за умови «монопольної транзакції на об’єкт як бізнес операції»

зміни відбуваються на тому самому об’єкті в обох системах одночасно, синхронізувати то все як?

скажімо сама бізнес модель сама по собі підтримує модель послідовних змін і нічого їй за те не буде

... скажімо «атрибут зміна адреси об’єкта» що на справді означає «атрибут встановлення нової адреси об’єкта»

... тож ось послідовність змін яка відбувалася

«а» 3-тя уліца строітєлєй дом 25 квартіра 12 4-й етаж
(ніт то само но 5-й етаж)
«б» 3-тя уліца строітєлєй дом 25 квартіра 12 5-й етаж
(ніт давай ну дубровку)
«в» морская 21 квартіра 9 3-й под’єзд 3-й етаж
...

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

... і тут залежить лише від того до чого на справді то є атрибут

як скажімо то є «моя адреса доставки» але то була «моя адреса доставки» для замовлення номер 42 і вона просто перейшла у атрибути уже самого замовлення номер 42 яке уже обробляється як окремий бізнес об’єкт

... а нове замовлення номер 43 було запущене вже після котроїсь зміни атрибуту «моя адреса доставки» і просто отримало нове значення атрибуту діюче на той момент

більше того я навіть можу змінити адресу доставки уже по ходу виконання замовлення на що так само існує така сама процедура запитів на зміну адрес доставки яка насправді є повідомленням

«встановити (нову) адресу доставки»

і має значення лише атомарність самого повідомлення та його змісту як то конкретна адреса щоб не змішувати будинки вулиці лєнінград москва тощо

а те що ці зміни надходять послідовно ну так просто працює нічого такого

Ще раз кейс — є дві системи А і Б, які зберігають різні атрибути одного об’єкту. Бізнес дуже захотів щоб на одному скріні з системи Б можна було оновити атрибути як з Б, так і з А, натиснути кнопку «Save» і бути впевненими що дані зберіглися або отримати помилку в протилежному випадку.
А якщо зміни відбуваються на тому самому об’єкті в обох системах одночасно, синхронізувати то все як?
можливо простіше зробити синхронний виклик.

а ну так ти же ж не вмієш у архітектуру що власне і було

то у вас же ж просто нема асинхронного дизайну от і все?
В контексті чого цей комент?

... тож як «синхронний виклик» буде виглядати з боку самої системи (тут «база»)

— прийшла «система А» запросила конект до бази
— база дала конект «А» (усе відбувається так само асинхронно прикинь)
— прийшла «система Б» запросила конект до бази
— база дала конект «Б»
— «система А» запросила значення атрибутів з бази
— база відправила значення атрибутів
— «система Б» запросила значення атрибутів з бази
— база відправила значення атрибутів

... я вже з цього місця вже покажу «асінхронність» тож слєді за рукой діло на справді було так

— «система А» запросила значення атрибутів з бази
— «система Б» запросила значення атрибутів з бази
— база відправила значення атрибутів (конект «Б»)
— база відправила значення атрибутів (конект «А»)

ну а чого атрібути ж незмінні вони взагалі закешувалися у самій базі і імплементація не гарантує саме постідовне отримання значень з кешу тож запит «А» лише ініціював отримання даних та заповнення їх у кеш а запит «Б» уже стояв у черзі на ті самі дані з кеша і база відправила дані з кеша спочатку тим хто вже стояв у черзі і вже потім тому чий запит ініціював отримання даних

— система «Б» відправила нові значення атрибутів у базу

... як то буквальне UPDATE dbo.Table1 SET addr = «3-тя уліца строітєлєй дом 25 квартіра 12 4-й етаж»

— база зробила зміни і відправила ОК (конект «Б»)
— система «А» відправила нові значення атрибутів у базу

... як то буквальне UPDATE dbo.Table1 SET addr = «3-тя уліца строітєлєй дом 25 квартіра 12 5-й етаж»

— база зробила зміни і відправила ОК (конект «А»)

натиснути кнопку «Save» і бути впевненими що дані зберіглися або отримати помилку в протилежному випадку.

але ж це був буквальний синхронний апдейт в базу хіба ні? і помилки не виникло хіба ні?

В твоєму прикладі на кроці 2 вже починаємо залежати від А, бо треба ще почекати як токен видадуть.

чи мені спочатку треба було якось заблокувати ці дані у базі синхроно бо ж у мене нема жодної інформації про те хто їх іще запитує і має змінити хіба ні?

... тож буквально «почекати доки дані заблокують до змін» синхоно

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

...
— база відправила значення атрибутів (конект «Б») (статус послідовності 42)
— база відправила значення атрибутів (конект «А») (статус послідовності 42)
— система «Б» відправила нові значення атрибутів у базу (статус послідовності 42)
— база зробила зміни і відправила ОК (конект «Б») (статус послідовності 43)
— система «А» відправила нові значення атрибутів у базу (статус послідовності 42)
— база виявила що статус послідовності не відповідає наявному і відправила not OK (конект «А»)

отже клієнт «система А» отримав помилку тому повторює свою операцію

— «система А» запросила значення атрибутів з бази
— база відправила значення атрибутів (конект «А») (статус послідовності 43)
— система «А» відправила нові значення атрибутів у базу (статус послідовності 43)
— база зробила зміни і відправила ОК (конект «А») (статус послідовності 44)

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

... але усе було синхроно ))

Вірно, але відразу відпадає потреба мати «yet another animal in the zoo» у вигляді мессадж брокеру.

див. п.п.

то у вас же ж просто нема асинхронного дизайну от і все?

Можеш, будь ласка, пояснити що значить

то у вас же ж просто нема асинхронного дизайну от і все?

В контексті чого цей комент?

Але асинхронну взаємодію можна реалізувати по-різному, не тільки через обмін мессаджами.

=>

це ж симуляція черги обробки вхідних даних?

=>

Вірно, але відразу відпадає потреба мати «yet another animal in the zoo» у вигляді мессадж брокеру.

тож я переведу з «архітектурського» на людянсько інженерний ))

Але асинхронну взаємодію можна реалізувати по-різному, не тільки через обмін мессаджами.

но через симуляцію обміну мессаджами назвавши це «іншої реалізації по різному» )) профітЪ

но через симуляцію обміну мессаджами назвавши це «іншої реалізації по різному» )) профітЪ

Цей комент каже про те, що ти прирівнюєш поняття «асинхронна взаємодія» і «асинхронний обмін повідомленнями». І схоже весь твій потік демагогії, пересмикувань і намагань «підколоти» викликаний саме цим базовим непорозумінням. Просто уточню: стаття про обмін повідомленнями, а не про асинхронну взаємодію в цілому. Тут я можу тільки порадити Google та ChatGPT для з’ясування відмінностей, можеш ще англійською порівняти asynchronous communication та asynchronous messaging, щоб виключити «складнощі перекладу».

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

Нажаль, вести конструктивну дискусію ти чи не хочеш чи не вмієш. Робити припущення щодо причин не бачу сенсу, як і продовжувати розмову. Але удачі тобі в пошуках самоутвердження!

Якщо бажаєте, приєднуйтесь до чатику з архітектури t.me/swarchua

Дякую за статтю. Корисний аналіз проблеми.

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

З Кафкою все зрозуміло. Там це підримується з коробки. Можливо, є досвід вирішення цієї проблеми при використанні RabbitMQ? Щось порадите?

Для кожного процесора можна зробити окрему чергу, в якості дистріб’ютора між ними — Direct Exchange або Topic Exchange, налаштований таким чином, щоб в залежності від ключа роутингу повідомлення уходило в одну з черг. Клієнти будуть відправляти все в єдиний Exchange, а він вже буде розкидати по чергам відповідно до вказаного клієнтом ключа роутингу. Більш детально можна почитати тут — medium.com/...​mq-exchanges-1cea7c9c8cb5

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

Можливо, ви мали на увазі що для маршрутизації по id це треба менеджити на рівні коду? Тобто писати алгоритм розрахунку черги по відомій айдішці.

Якщо треба реалізувати поведінку з розділенням на черги по хешу, а не по конкретним значенням, то краще використовувати спеціалізоване розширення для RabbitMQ github.com/...​_consistent_hash_exchange

Підписатись на коментарі