CQRS в інфраструктурі: де ламається розділення читання і запису
CQRS — це не бібліотека і не окремий сервіс, а розділення двох контурів з різними вимогами до відмови. Команди пишуть через транзакційну модель: строга узгодженість, коротке блокування, передбачувана затримка.
Читання обслуговують проєкції: їх можна перезібрати, вони терпимі до відставання й денормалізовані під конкретний екран.
Основна маса інцидентів живе на стику. Розсинхрон проєкцій проявляється як читання після запису: користувач надіслав команду, інтерфейс запитав модель і отримав попередній стан.
Лікується це не паузою перед запитом, а версією агрегату у відповіді на команду — клієнт читає доти, доки проєкція не наздожене цю версію.
Дублі команд — друга типова точка відмови: транспорт доставляє подію щонайменше один раз, повтор після таймауту проходить як нова команда і проводить операцію двічі.
Ідемпотентність стає частиною контракту команди: ключ дедуплікації на вході, унікальний індекс за ідентифікатором, перевірка очікуваної версії під час запису.
Деградація запису під аналітичним навантаженням майже завжди є наслідком спільної інфраструктури: важкі звіти займають пул з'єднань і диск, хвостова затримка запису зростає нелінійно.
Тому контур читання виносимо на окремі репліки та сховища — наприклад, проєкції на PostgreSQL зі своїм профілем навантаження, — а на запис ставимо таймаути, обмеження одночасних запитів і явну відмову замість накопичення черги.
Що дає CQRS-контур: запис, читання і клас усунених відмов
Розділення запису і читання дає два незалежні контури, і кожен вимірюється своєю метрикою.
Команди пишуть у нормалізоване сховище із синхронною реплікацією (у нашому контурі — PostgreSQL): час відгуку запису вкладається в частки секунди й не залежить від того, скільки читачів одночасно будують звіти. Читання обслуговують денормалізовані проєкції, які наповнюються асинхронно з журналу подій.
Відмовостійкість читання тримається на тому, що проєкції виводяться з журналу і не є джерелом істини. Вузол читання можна втратити повністю: приймання заявок продовжується, а проєкція відновлюється програванням подій з останньої збереженої позиції.
Час відновлення тоді визначається добовим обсягом журналу, а не розміром основної бази та швидкістю її розгортання.
Навантаження на запис перестає конкурувати з аналітикою. Важкі вибірки та вивантаження йдуть на окремі репліки, тому піковий трафік звітів більше не блокує транзакції.
Змінюється й клас відмов: замість «база лягла під звітним запитом» отримуємо деградацію однієї проєкції, яка наздоганяє потік за хвилини й не заважає оформленню замовлень.
Вартість простою вважаємо у втрачених операціях, а не в годинах повної недоступності. Поки недоступний контур запису, нові заявки не приймаються — але читання історії, статуси та особистий кабінет продовжують працювати, бо йдуть по своїх репліках.
Час відгуку запису, час відновлення й частка відмов читання — три метрики, за якими ми приймаємо роботу після впровадження.
Варіанти розділення читання і запису: порівняння підходів
Розділення читання і запису — не перемикач, а шкала: від окремих ролей усередині однієї бази до журналу подій з асинхронними проєкціями. Кожен крок праворуч знімає навантаження з транзакційного запису, але додає затримку між записаним і прочитаним.
Вибір диктує вимога до свіжості даних: десь достатньо «за секунду», десь рахунок іде на частки секунди.
| Варіант | Коли підходить | Обмеження |
|---|---|---|
| Окремі ролі в одній базі | Аналітика та адмінка заважають транзакційному запису | Спільний ресурс: важкий звіт усе ще гальмує запис |
| Репліка для читання | Навантаження переважно на читання, відставання в секунди допустиме | Затримка реплікації: читання відразу після запису потребує маршруту на майстер |
| Матеріалізовані подання | Важкі агрегати, старіння даних прийнятне | Перерахунок упирається в блокування; набір подань жорстко прив'язаний до запитів |
| Журнал подій та асинхронні проєкції | Кілька моделей читання, потрібна історія та перезбирання | Узгодженість зрештою: дублі, повторна обробка, відставання проєкції |
Узгодженість зрештою — не дефект, а параметр: її затримку задають явно й перевіряють за метрикою відставання проєкцій. Починаємо з окремих ролей там, де цього достатньо, і переходимо до журналу подій лише там, де потрібна історія та перезбирання моделей читання.
Порядок впровадження: від аудиту схеми даних до передачі регламентів
Впровадження CQRS починається не з коду, а з опису того, які запити обслуговує система і де запис заважає читанню. Без цього кроку розділення моделей перетворюється на два сховища з розбіжними даними та ручним звірянням.
Маршрут впровадження
- Аудит схеми даних. Знімаємо карту сутностей: що тільки читається, що постійно пишеться, де операції конкурують за блокування рядків. На виході — перелік сценаріїв із профілем навантаження за кожним.
- Схема потоків команд. Фіксуємо межі агрегатів і те, яка команда породжує яку подію. Тут же вирішуємо, які зміни допустимо застосовувати асинхронно, а які мають бути строго узгодженими.
- Проєктування моделей читання. Під кожен екран і звіт — своє подання з потрібними індексами, без універсальної таблиці «на все». Окремо обговорюємо допустиме відставання проєкції для кожного сценарію.
- План впровадження. Йдемо по одному обмеженому контексту, зберігаючи попередній шлях запису як запасний. Поки новий контур не підтверджено, відкат можливий перемиканням без втрати даних.
- Налаштування синхронізації. Визначаємо чергу, порядок доставки подій, ідемпотентність обробників і поведінку при повторній доставці. Для збійних повідомлень — окрема гілка з розбором, щоб одна подія не блокувала потік.
- Перевірка на навантаженні. Проганяємо запис і читання окремо: дивимося відставання проєкцій під піком, поведінку при недоступності споживача і час прогріву подань після перебудови.
- Передача регламентів. Віддаємо інструкції щодо перегравання подій, відновлення проєкції з нуля та розбору розбіжностей, а також набір метрик стану контуру для чергової зміни.
Кожен крок закінчується артефактом: схемою, конфігурацією або регламентом. Так впровадження залишається керованим, а експлуатація не залежить від авторів рішення.
Комплект на виході: схеми потоків, політики доступу, регламенти
Комплект документації — частина результату, а не додаток до нього. Без нього контур CQRS працює, але змінювати його команда замовника не може: незрозуміло, де пишеться істина, хто читає проєкції і що робити при розбіжності. Тому разом із кодом передаємо артефакти, кожен з яких закриває свій клас завдань при експлуатації.
- Карта потоків подій — які команди їх породжують, хто споживачі, який порядок доставки гарантовано і чим гаситься повторна обробка.
- Схема даних — розділення сторони запису і сторони читання, модель проєкцій, ключі та індекси під конкретні запити екранів.
- Документація щодо проєкцій — джерело подій, правило перебудови, нормальне відставання від потоку, порядок повного перерахунку без зупинки читання.
- Політики доступу — ролі на надсилання команд і на читання, розмежування за агрегатами, окреме право на перебудову проєкції.
- Регламент супроводу — як змінювати схему подій, як версіонувати події і що робити зі споживачами, які ще не перейшли на нову версію.
- Порядок розбору інцидентів — ознаки розбіжності читання і запису, кроки діагностики за журналами, відкат на попередню версію проєкції.
- Схема розгортання — склад вузлів, параметри брокера, черги повторів і ліміти, після яких підключається інженер.
Комплект живе в репозиторії замовника й оновлюється разом із кодом. Перевіряємо його на прийманні просто: якщо за ним не можна перезібрати проєкцію або ввести в роботу нового інженера — завдання не закрито.
Кейс: розділення читання і запису у високонавантаженому сервісі
«Сервіс починався з одного вузла: і запис, і читання йшли в одну й ту саму таблицю. На піку транзакції запису впиралися в блокування — час відгуку запису зростав із секунди до десятка, а частка відмов читання сягала 8%. Найгірше було з відмовами: втрата вузла означала ручне відновлення і до півгодини недоступності.
Моделі розвели. Запис залишився в транзакційному сховищі PostgreSQL, читання пішло на окремі денормалізовані проєкції, які оновлюються з журналу змін. Обробники зробили ідемпотентними, а журнал — джерелом, з якого проєкцію можна перезібрати повністю після збою. Читальні репліки перемикаються автоматично, без ручного втручання.
Що змінилося за метриками. Час відгуку запису на піку скоротився в 6–8 разів і перестав залежати від обсягу читань. Частка відмов читання впала нижче десятих часток відсотка: запит більше не чекає блокувань, а падіння окремої репліки не валить видачу. Відновлення після збою вузла — автоматичне, за пару хвилин, без втрати підтверджених записів.
Окремо винесли спостережуваність: відставання проєкцій, глибина черги та розбіжність між моделями видно на панелі моніторингу, тому деградація ловиться раніше, ніж її помітить користувач.»
Чим CQRS відрізняється від репліки для читання?
Репліка для читання — це та сама модель запису, розмножена по вузлах: ті самі таблиці, ті самі обмеження, той самий контракт.
Змінюється лише те, хто відповідає на запити, а розбіжність обмежена відставанням репліки — зазвичай це частки секунди, і для більшості звітних запитів воно непомітне.
Розділення моделей починається там, де вимоги читання і запису розходяться настільки, що одна схема їх більше не обслуговує.
У CQRS модель запису відповідає за стан та інваріанти: транзакції, перевірки, блокування. Модель читання — окрема проєкція під конкретні запити: денормалізовані таблиці, заздалегідь пораховані агрегати, пошукові структури. Будується вона з потоку змін, а не з копії таблиць, і живе за своїми правилами.
Звідси ключова відмінність. У репліки відставання — це затримка копіювання при тому самому сенсі даних. У CQRS моделі розходяться за визначенням: проєкція не є джерелом істини, її можна перезібрати з нуля, а помилка в розрахунку лікується перерахунком, а не відновленням з копії. Плата за це — асинхронність.
Той, хто читає відразу після запису, може не побачити своїх змін. Там, де це неприпустимо, версіонуємо проєкцію й чекаємо підтвердження потрібної версії або залишаємо синхронний шлях для вузького набору операцій.
Стежимо при цьому за різними метриками: відставання репліки й затримка оновлення проєкції — не одне й те саме. Якщо читання і запис симетричні за навантаженням, розділення моделей не потрібне — достатньо реплік і розподілу запитів.
Технічні сумніви: узгодженість, дублі команд, міграція
Заперечення до CQRS знімаються на етапі проєктування, а не після запуску: у кожного з них є конкретне інженерне рішення, яке перевіряється тестом або навантаженням. Нижче — відповіді на ті, що найчастіше спливають перед стартом робіт.
Як гарантується ідемпотентність команд?
Кожна команда приходить з унікальним ключем, а обробник веде журнал уже застосованих ключів. Повторна доставка при повторі не створює другого запису: операція або вже застосована, або приймається як нова.
Що з узгодженістю даних між записом і читанням?
Проєкція наздоганяє журнал із затримкою в частки секунди, і це вікно вимірне — його видно на метриках. Для критичних сценаріїв читання йде з очікуванням потрібної версії проєкції або за самим журналом, а інтерфейс показує «прийнято, обробляється» замість застарілих даних.
Чи можна перебудувати проєкції без простою?
Проєкція — похідна від журналу, тому нова версія збирається в окрему схему паралельно з працюючою, наздоганяє потік і перемикається маршрутом читання. Відкат — повернення маршруту на попередню схему.
Як проходить міграція без зупинки запису?
Розширюємо схему, пишемо у старий і новий формат одночасно, переносимо історію фоновими пакетами, звіряємо контрольні суми за ключами і лише потім прибираємо старий шлях. Кожен крок оборотний і не блокує запис.
Що станеться при недоступності транспорту подій?
Подія пишеться в тій самій транзакції, що й зміна стану, а публікує її окремий воркер із підтвердженням доставки. Непідтверджені надсилаються повторно, отримувач відкидає дублі за ідентифікатором події.
Обговоримо задачу: що потрібно для оцінки та плану робіт
Щоб оцінка робіт спиралася на факти, а не на припущення, нам потрібен короткий розбір поточного контуру: де проходить запис, де читання і як вони пов'язані між собою. Чим конкретніші вихідні дані, тим точніший план впровадження і тим менше припущень у строках та обсязі змін.
Підійде те, що є під рукою: схеми, конфігурації сервісів, вивантаження метрик, опис відомих вузьких місць. Якщо документації немає, відновимо картину за вашими відповідями та за журналами, які ви готові показати.
Що помітно прискорює підготовку:
- схема контуру: сервіси, точки запису й читання, як зараз ідуть запити;
- профіль навантаження: частка читання й запису, піки, терпимість до відставання проєкцій;
- вимоги до узгодженості: де допустиме читання із затримкою, а де потрібна актуальність;
- сховища та черги: що вже використовується, обмеження за доступами й середовищами;
- викат і відкат: як випускаються зміни, чи є набір регресійних перевірок;
- критерії приймання: за якими метриками зрозуміємо, що рішення працює;
- обмеження: регламенти, вікна робіт, хто приймає рішення на вашій стороні.
У відповідь надсилаємо оцінку за етапами, план впровадження з порядком робіт і точками відкату, а також склад робіт із межами відповідальності. Опишіть задачу в зручному вигляді — повернемося з оцінкою та планом.







