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







