Реалізація CQRS-патерну в інфраструктурі

Коли читання та запис конкурують за одні й ті самі ресурси, сервіс просідає під навантаженням. Ми проєктуємо та впроваджуємо контур CQRS під ключ: розводимо потоки, прибираємо взаємний вплив і передаємо команді готові схеми та регламенти. Навантаження на запис перестає визначати швидкість відгуку.
Часті запитання

CQRS в інфраструктурі: де ламається розділення читання і запису

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

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

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

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

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

Ідемпотентність стає частиною контракту команди: ключ дедуплікації на вході, унікальний індекс за ідентифікатором, перевірка очікуваної версії під час запису.

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

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

Що дає CQRS-контур: запис, читання і клас усунених відмов

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

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

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

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

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

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

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

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

Варіанти розділення читання і запису: порівняння підходів

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

Вибір диктує вимога до свіжості даних: десь достатньо «за секунду», десь рахунок іде на частки секунди.

Варіант Коли підходить Обмеження
Окремі ролі в одній базі Аналітика та адмінка заважають транзакційному запису Спільний ресурс: важкий звіт усе ще гальмує запис
Репліка для читання Навантаження переважно на читання, відставання в секунди допустиме Затримка реплікації: читання відразу після запису потребує маршруту на майстер
Матеріалізовані подання Важкі агрегати, старіння даних прийнятне Перерахунок упирається в блокування; набір подань жорстко прив'язаний до запитів
Журнал подій та асинхронні проєкції Кілька моделей читання, потрібна історія та перезбирання Узгодженість зрештою: дублі, повторна обробка, відставання проєкції

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

Порядок впровадження: від аудиту схеми даних до передачі регламентів

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

Маршрут впровадження

  1. Аудит схеми даних. Знімаємо карту сутностей: що тільки читається, що постійно пишеться, де операції конкурують за блокування рядків. На виході — перелік сценаріїв із профілем навантаження за кожним.
  2. Схема потоків команд. Фіксуємо межі агрегатів і те, яка команда породжує яку подію. Тут же вирішуємо, які зміни допустимо застосовувати асинхронно, а які мають бути строго узгодженими.
  3. Проєктування моделей читання. Під кожен екран і звіт — своє подання з потрібними індексами, без універсальної таблиці «на все». Окремо обговорюємо допустиме відставання проєкції для кожного сценарію.
  4. План впровадження. Йдемо по одному обмеженому контексту, зберігаючи попередній шлях запису як запасний. Поки новий контур не підтверджено, відкат можливий перемиканням без втрати даних.
  5. Налаштування синхронізації. Визначаємо чергу, порядок доставки подій, ідемпотентність обробників і поведінку при повторній доставці. Для збійних повідомлень — окрема гілка з розбором, щоб одна подія не блокувала потік.
  6. Перевірка на навантаженні. Проганяємо запис і читання окремо: дивимося відставання проєкцій під піком, поведінку при недоступності споживача і час прогріву подань після перебудови.
  7. Передача регламентів. Віддаємо інструкції щодо перегравання подій, відновлення проєкції з нуля та розбору розбіжностей, а також набір метрик стану контуру для чергової зміни.

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

Комплект на виході: схеми потоків, політики доступу, регламенти

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

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

Комплект живе в репозиторії замовника й оновлюється разом із кодом. Перевіряємо його на прийманні просто: якщо за ним не можна перезібрати проєкцію або ввести в роботу нового інженера — завдання не закрито.

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

«Сервіс починався з одного вузла: і запис, і читання йшли в одну й ту саму таблицю. На піку транзакції запису впиралися в блокування — час відгуку запису зростав із секунди до десятка, а частка відмов читання сягала 8%. Найгірше було з відмовами: втрата вузла означала ручне відновлення і до півгодини недоступності.

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

Що змінилося за метриками. Час відгуку запису на піку скоротився в 6–8 разів і перестав залежати від обсягу читань. Частка відмов читання впала нижче десятих часток відсотка: запит більше не чекає блокувань, а падіння окремої репліки не валить видачу. Відновлення після збою вузла — автоматичне, за пару хвилин, без втрати підтверджених записів.

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

Чим CQRS відрізняється від репліки для читання?

Репліка для читання — це та сама модель запису, розмножена по вузлах: ті самі таблиці, ті самі обмеження, той самий контракт.

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

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

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

Звідси ключова відмінність. У репліки відставання — це затримка копіювання при тому самому сенсі даних. У CQRS моделі розходяться за визначенням: проєкція не є джерелом істини, її можна перезібрати з нуля, а помилка в розрахунку лікується перерахунком, а не відновленням з копії. Плата за це — асинхронність.

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

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

Технічні сумніви: узгодженість, дублі команд, міграція

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

Як гарантується ідемпотентність команд?

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

Що з узгодженістю даних між записом і читанням?

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

Чи можна перебудувати проєкції без простою?

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

Як проходить міграція без зупинки запису?

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

Що станеться при недоступності транспорту подій?

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

Обговоримо задачу: що потрібно для оцінки та плану робіт

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

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

Що помітно прискорює підготовку:

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

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

Наші компетенції:

Останні роботи

  • Розробка сайту компанії B2B ADVANCE
    Розробка сайту компанії B2B ADVANCE
    1542
  • Розробка веб-додатків для компанії FEEDME
    Розробка веб-додатків для компанії FEEDME
    1394
  • Розробка веб-сайту для компанії БЕЛФІНГРУП
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    1081
  • Розробка інтернет магазину для компанії FURNORO
    Розробка інтернет магазину для компанії FURNORO
    1369
  • Розробка логотипу компанії B2B Advance
    Розробка логотипу компанії B2B Advance
    816
  • Розробка веб-додатків для компанії Enviok
    Розробка веб-додатків для компанії Enviok
    1142