Побудова надійної, високодоступної, масштабованої розподіленої системи вимагає дотримання певних методів, принципів і шаблонів. Розробка таких систем передбачає вирішення безлічі проблем. Серед найбільш поширених і фундаментальних проблем є проблема подвійного запису .
«Проблема подвійного запису» — це проблема, яка виникає в розподілених системах, головним чином під час роботи з кількома джерелами даних або базами даних, які потрібно підтримувати в синхронізації. Це стосується труднощів гарантувати, що зміни даних послідовно записуються в різні сховища даних, такі як бази даних або кеш-пам’ять, без виникнення таких проблем, як невідповідність даних, конфлікти або вузькі місця продуктивності.
Архітектура мікросервісів і база даних шаблонів для кожної служби дають вам багато переваг, наприклад незалежне розгортання та масштабування, окремі збої та потенційне підвищення швидкості розробки. Однак операції вимагають змін між кількома мікросервісами, що змушує вас думати про надійне рішення для вирішення цієї проблеми.
Давайте розглянемо сценарій, у якому наш домен передбачає прийняття заявок на кредит, їх оцінку, а потім надсилання сповіщень клієнтам.
У дусі принципу єдиної відповідальності, закону Конвея та підходу до проектування, орієнтованого на домен, після кількох сеансів, пов’язаних зі штурмом подій, весь домен було розділено на три субдомени з визначеними обмеженими контекстами, що мають чіткі межі, моделі доменів і всюдисущу мову.
Перший відповідає за адаптацію та складання нових заявок на кредит. Друга система оцінює ці заявки та приймає рішення на основі наданих даних. Цей процес оцінки, включаючи перевірку KYC/KYB, перевірку на захист від шахрайства та кредитний ризик, може зайняти багато часу, що потребує здатності обробляти тисячі заявок одночасно. Отже, цю функціональність було делеговано виділеному мікросервісу з власною базою даних, що дозволяє незалежне масштабування.
Крім того, цими підсистемами керують дві різні команди, кожна з яких має власні цикли випуску, угоди про рівень обслуговування (SLA) і вимоги до масштабованості.
Нарешті , існує спеціалізована служба сповіщень для надсилання сповіщень клієнтам.
Ось уточнений опис основного випадку використання системи:
На перший погляд, це досить проста і примітивна система, але давайте поглибимося в те, як служба заявки на позику обробляє команду відправити заявку на позику.
Ми можемо розглянути два підходи до взаємодії сервісів:
First-Local-Commit-Then-Publish: у цьому підході служба оновлює свою локальну базу даних (коміти), а потім публікує подію чи повідомлення для інших служб.
First-Publish-Then-Local-Commit: І навпаки, цей метод передбачає публікацію події або повідомлення перед фіксацією змін у локальній базі даних.
Обидва методи мають свої недоліки і лише частково безпечні для зв'язку в розподілених системах.
Це схема послідовності застосування першого підходу.
У цьому сценарії Служба заявки на позику використовує підхід «Спочатку локальне прийняття — потім публікація» , коли вона спочатку фіксує транзакцію, а потім намагається надіслати сповіщення іншій системі. Однак цей процес чутливий до збою, якщо, наприклад, є проблеми з мережею, служба оцінки недоступна або служба заявки на позику стикається з помилкою «Нестача пам’яті» (OOM) і аварійно завершує роботу. У таких випадках повідомлення буде втрачено, а Оцінка залишиться без повідомлення про нову заявку на кредит, якщо не буде вжито додаткових заходів.
І другий.
У сценарії «Спочатку опублікуйте, а потім локально зафіксуйте» Служба подачі заявок на позику стикається з більш значними ризиками. Він може повідомити службу оцінювання про нову програму, але не зберегти це оновлення локально через проблеми, як-от проблеми з базою даних, помилки пам’яті або помилки коду. Такий підхід може призвести до суттєвих невідповідностей у даних, що може спричинити серйозні проблеми, залежно від того, як Служба перегляду кредитів обробляє вхідні заявки.
Тому ми повинні визначити рішення, яке пропонує надійний механізм для публікації подій зовнішнім споживачам. Але перш ніж заглиблюватися в потенційні рішення, ми повинні спочатку уточнити типи гарантій доставки повідомлень, які можна досягти в розподілених системах.
Є чотири типи гарантій, яких ми можемо отримати.
Без гарантій
Немає гарантії, що повідомлення буде доставлено за призначенням. Підхід «Спочатку локально зафіксуйте, потім опублікуйте» саме про це. Споживачі можуть отримувати повідомлення один раз, кілька разів або жодного разу.
Максимально одноразова доставка
Максимально один раз означає, що повідомлення буде доставлено до місця призначення щонайбільше 1 раз. Підхід First-Local-Commit-Then-Publish також можна реалізувати таким чином із політикою повторних спроб із значенням один.
Принаймні одна доставка\Споживачі отримуватимуть і оброблятимуть кожне повідомлення, але можуть отримувати те саме повідомлення кілька разів.
Точно один раз доставка \ Точно один раз доставка означає, що споживач отримає повідомлення фактично один раз.
Технічно це можливо досягти за допомогою транзакцій Кафки та специфічної ідемпотентної реалізації виробника та споживача.
У більшості випадків доставка «принаймні один раз» гарантує вирішення багатьох проблем, забезпечуючи доставку повідомлень принаймні один раз, але споживачі повинні бути ідемпотентними. Однак, враховуючи неминучі збої в мережі, уся логіка споживача має бути ідемпотентною, щоб уникнути обробки дублікатів повідомлень, незалежно від гарантій виробника. Тому ця вимога є не стільки недоліком, скільки відображає реальність.
Існує маса рішень цієї проблеми, які мають свої переваги та недоліки.
Відповідно до Вікіпедії, Two-Phase Commit (2PC) — це протокол розподілених транзакцій, який використовується в інформатиці та системах керування базами даних для забезпечення узгодженості та надійності розподілених транзакцій. Він розроблений для ситуацій, коли кілька ресурсів (наприклад, бази даних) повинні брати участь в одній транзакції, і він гарантує, що або всі вони здійснюють транзакцію, або всі вони переривають її, таким чином зберігаючи узгодженість даних. Звучить саме те, що нам потрібно, але Two-Phase Commit має кілька недоліків:
Найбільш очевидним рішенням для архітектури мікросервісів є застосування шаблону (або навіть іноді антишаблону) — спільної бази даних. Цей підхід дуже інтуїтивно зрозумілий, якщо вам потрібна узгодженість транзакцій у кількох таблицях у різних базах даних, просто використовуйте одну спільну базу даних для цих мікросервісів.
Недоліки цього підходу включають введення єдиної точки відмови, перешкоджання незалежному масштабуванню бази даних і обмеження можливості використання різних рішень бази даних, які найкраще підходять для конкретних вимог і випадків використання. Крім того, для підтримки такої форми розподіленої транзакції знадобляться модифікації кодових баз мікросервісів.
« Транзакційна вихідна скринька » — це шаблон проектування, який використовується в розподілених системах для забезпечення надійного розповсюдження повідомлень навіть в умовах ненадійних систем обміну повідомленнями. Це передбачає збереження подій у визначеній таблиці OutboxEvents у тій самій транзакції, що й сама операція. Цей підхід добре узгоджується з властивостями ACID реляційних баз даних. Навпаки, багато баз даних No-SQL не повністю підтримують властивості ACID, вибираючи натомість принципи теореми CAP і філософії BASE, які надають перевагу доступності та кінцевій узгодженості над суворою узгодженістю.
Транзакційна вихідна скринька забезпечує принаймні один раз гарантію та може бути реалізована кількома підходами:
Хвости журналу транзакцій
Видавець опитувань
Підхід до відстеження журналу транзакцій передбачає використання специфічних для бази даних рішень, таких як CDC (Change Data Capture). Основними недоліками цього підходу є:
Спеціальні рішення для баз даних
Збільшена затримка через особливості реалізацій CDC
Іншим методом є Polling Publisher , який полегшує розвантаження вихідних повідомлень шляхом опитування таблиці вихідних повідомлень. Основним недоліком цього підходу є потенційне збільшення навантаження на базу даних, що може призвести до вищих витрат. Крім того, не всі бази даних No-SQL підтримують ефективне надсилання запитів для певних сегментів документа. Таким чином, вилучення цілих документів може призвести до зниження продуктивності.
Ось невелика схема послідовності, яка пояснює, як це працює.
Основна проблема шаблону Transactional Outbox полягає в його залежності від властивостей ACID бази даних. Це може бути простим у типових базах даних OLTP, але створює проблеми у сфері NoSQL. Щоб вирішити цю проблему, потенційним рішенням є використання журналу додавання (наприклад, Kafka) безпосередньо з ініціювання обробки запиту.
Замість того, щоб безпосередньо обробляти команду «подати заявку на кредит», ми негайно надсилаємо її до внутрішньої теми Kafka, а потім повертаємо клієнту результат «прийнято». Однак, оскільки дуже ймовірно, що команду ще потрібно обробити, ми не можемо негайно повідомити клієнта про результат. Щоб керувати цією остаточною узгодженістю, ми можемо використовувати такі методи, як довге опитування, опитування, ініційоване клієнтом, оптимістичні оновлення інтерфейсу користувача або використання WebSockets або подій, надісланих сервером, для сповіщень. Однак це зовсім окрема тема, тому повернемося до нашої початкової теми.
Ми відправили повідомлення на внутрішню тему Кафки. Служба заявки на позику потім використовує це повідомлення — ту саму команду, яку вона отримала від клієнта — і починає обробку. По-перше, він виконує деяку бізнес-логіку; лише після успішного виконання цієї логіки та збереження результатів він публікує нові повідомлення на загальнодоступну тему Kafka.
Давайте подивимося на трохи псевдокоду.
public async Task HandleAsync(SubmitLoanApplicationCommand command, ...) { //First, process business logic var loanApplication = await _loanApplicationService.HandleCommandAsync(command, ...); //Then, send new events to public Kafka topic producer.Send(new LoanApplicationSubmittedEvent(loanApplication.Id)); //Then, commit offset consumer.Commit(); }
Що робити, якщо обробка бізнес-логіки дає збій? Не хвилюйтеся, оскільки зміщення ще не зафіксовано, повідомлення буде спробовано повторно.
Що робити, якщо надсилання нових подій до Kafka не вдається? Не хвилюйтеся, оскільки бізнес-логіка є ідемпотентною, вона не створить повторну заявку на позику. Замість цього він спробує повторно надіслати повідомлення до загальнодоступної теми Kafka.
Що робити, якщо повідомлення надсилаються Kafka, але фіксація зсуву не вдається? Не хвилюйтеся, оскільки бізнес-логіка є ідемпотентною, вона не створить повторну заявку на позику. Замість цього він повторно надсилатиме повідомлення до загальнодоступної теми Kafka і сподіватиметься, що цього разу фіксація зсуву вдасться.
Основні недоліки цього підходу включають додаткову складність, пов’язану з новим стилем програмування, можливу узгодженість (оскільки клієнт не відразу дізнається результат) і вимогу, щоб уся бізнес-логіка була ідемпотентною.
Що таке пошук подій і як це можна тут застосувати? Джерело подій — це архітектурний шаблон програмного забезпечення, який використовується для моделювання стану системи шляхом фіксації всіх змін у її даних як серії незмінних подій. Ці події представляють факти або зміни стану та служать єдиним джерелом істини для поточного стану системи. Отже, технічно, реалізувавши систему пошуку подій, ми вже маємо всі події в EventStore, і споживачі можуть використовувати цю EventStore як єдине джерело правди про те, що сталося. Немає потреби в спеціальному рішенні бази даних для відстеження всіх змін або занепокоєння щодо впорядкування, єдина проблема полягає в тому, щоб читати, оскільки для отримання фактичного стану об’єкта потрібно відтворити всі події.
У цій статті ми розглянули кілька підходів до побудови надійного обміну повідомленнями в розподілених системах. Є кілька рекомендацій, які ми можемо враховувати під час створення систем із такими характеристиками
Наступного разу ми розглянемо більш практичний приклад реалізації транзакційної вихідної скриньки. див
ти!