Стратегии декомпозиции монолитных систем
Модель и масштаб — Основы БД, опорные темы, проектирование БД, пакетная работа. Карта — о разделе.
Здесь (104) — что и зачем резать: метрика "до/после", карта монолита, стратегии выделения (Strangler, DDD-контексты, нагрузка), данные, саги, кейсы.
Паттерны перехода от монолита к микросервисам — как вести миграцию по шагам без "большого взрыва": Strangler Fig, Parallel Run, Decorating Collaborator, CDC, сочетание паттернов и чеклист cutover. Strangler Fig подробно — в 2125.
Перед стартом: пять вопросов о готовности, Event Storming для границ домена, оценка альтернатив для выбора стиля.
Декомпозиция монолита
Декомпозиция — средство достижения целей — ускорение разработки, повышение отказоустойчивости, возможность независимого масштабирования, снижение времени развёртывания, сокращение инцидентов. Поэтому первое, что следует сделать перед началом — чётко сформулировать метрику успеха: что должно улучшиться и на сколько? Без этого декомпозиция превращается в техническое упражнение с неопределённой отдачей.
Декомпозиция — процесс разделения единого программного модуля на несколько независимых компонентов с чёткими границами ответственности и взаимодействием через явные интерфейсы.
Метрика успеха — количественный или качественный показатель, используемый для оценки эффективности декомпозиции, например время развёртывания, частота сбоев или скорость разработки новых функций.
Пять вопросов перед декомпозицией
Перед картированием монолита оцените готовность команды и платформы. Микросервисы в первую очередь дают независимый жизненный цикл частей системы; без ответа "да" на пункты ниже разделение часто превращается в распределённый монолит с той же координацией, но большей операционной ценой.
| № | Вопрос | Признак "да, резать имеет смысл" | Куда углубиться |
|---|---|---|---|
| 1 | Деплои блокируют работу команды | Десятки человек регулярно синхронизируют выкладки; релиз — общее окно, а не редкое пересечение двух разработчиков | Практика, антипаттерн в Паттерны микросервисной архитектуры §17 |
| 2 | У частей системы разная нагрузка | Профили роста расходятся (например, аутентификация ~100 RPS, поиск ~10 000 RPS); есть модуль, который масштабируют отдельно от остального | §1.3 ниже, кейс 1 в конце статьи |
| 3 | Систему можно отлаживать через несколько сервисов | Есть распределённая трассировка, структурированные логи с correlation id, единый контур метрик и логов | Наблюдаемость, метрики в 2.06, этап 4 ниже |
| 4 | Есть запас по задержкам сети | Понятен латентностный бюджет; цепочки из нескольких синхронных вызовов укладываются в SLO пользователя | Сеть и интернет, Распределённые системы, §17 в Паттерны микросервисной архитектуры |
| 5 | Закреплено владение сервисами | У каждой будущей границы есть команда с полным циклом (build → run); границы совпадают с Conway | Модульный монолит как альтернатива |
Декомпозицию откладывают: ускоряют CI и модули в монолите, настраивают observability на текущем артефакте, фиксируют метрику успеха. Резать имеет смысл, когда узкое место — координация релизов, разная нагрузка или явное владение доменом; одной долгой сборки или желания "перейти на микросервисы" для этого мало.
Без трассировки и correlation id инцидент в распределённой системе легко растягивается на часы вместо минут — observability лучше внедрить до первого выноса сервиса.
Если нагрузка растёт одинаково по всем модулям, разделение часто дублирует инфраструктуру без выгоды — достаточно горизонтального масштабирования монолита.
Этап 0. Анализ и картирование
Анализ — систематическое изучение структуры, поведения и зависимостей монолитного приложения с целью выявления кандидатов на выделение в отдельные компоненты.
Картирование — создание структурированного представления архитектуры системы, включающего компоненты, их связи и потоки данных.
Анализ зависимостей — исследование связей между модулями кода с целью выявления циклических, тесных или неочевидных взаимозависимостей, препятствующих разделению.
Зависимости — отношения между компонентами, при которых один компонент требует наличия, корректного состояния или поведения другого для выполнения своей функции.
Декомпозиция начинается с понимания текущей структуры — особенно если монолит развивался без чётких границ.
Техники анализа
- Статический анализ зависимостей. Инструменты (NDepend, Structure101, JDepend, SonarQube) строят граф вызовов между модулями, классами, пакетами. Выявляются:
- циклические зависимости — признак смешанных зон ответственности;
- модули с высокой входящей связанностью (много кто зависит от них) — кандидаты в ядро;
- модули с высокой исходящей связанностью (зависят от многих) — "клей", который нужно изолировать.
Связанность — степень, с которой компоненты зависят друг от друга; низкая связанность способствует гибкости и независимому развёртыванию.
- Анализ по данным. Постройте heat-map обращений к таблицам БД: какие таблицы часто читаются/пишутся вместе? Какие запросы объединяют данные из разных логических областей? Это помогает выявить неявные границы домена — даже если код смешан.
heat-map — графическое представление интенсивности использования или взаимодействия между частями системы, где цвета отражают нагрузку, частоту вызовов или объём передаваемых данных.
-
Анализ по логам и метрикам. Где происходят пиковые нагрузки? Какие эндпоинты дают наибольшее количество ошибок? Какие компоненты наиболее часто меняются? Это указывает на "горячие точки", где выделение в отдельный сервис даст максимальный эффект.
-
Анализ по истории Git. Какие файлы чаще всего меняются вместе? Можно использовать
git log --follow --name-onlyи построить матрицу коизменяемости. Файлы, которые почти всегда коммитятся в одном MR — кандидаты на один компонент (CCP в действии).
git log --follow --name-only
Результат — архитектурная карта — визуализация связей, выделение потенциальных границ, ранжирование по приоритету (влияние / стоимость).
Архитектурная карта — диаграмма, описывающая компоненты системы, их границы, связи и распределение по уровням абстракции или доменным областям.
Визуализация — отображение архитектурных элементов и их отношений в графической форме для упрощения понимания структуры и динамики системы.
Этап 1. Выбор стратегии
Не все части монолита одинаково подходят для выделения. Существует несколько стратегий, каждая — с разными рисками и выгодами.
На этапе миграции чаще всего комбинируют переходные паттерны — Strangler Fig (маршрутизация трафика), Parallel Run (сравнение ответов до cutover), Decorating Collaborator (новая логика через прокси) и CDC (синхронизация данных из БД монолита). Сводная таблица, схема сочетания и чеклист — в статье Паттерны перехода от монолита к микросервисам.
1. По функциональной независимости (Strangler Fig Pattern)
Суть: постепенно "оборачивать" функционал монолита новыми компонентами, перехватывая трафик через шлюз (API Gateway). Например:
- Добавляется шлюз перед монолитом.
- Для нового функционала (например, "подписки") создаётся отдельный сервис.
- Шлюз направляет
/api/subscriptions/*в новый сервис, всё остальное — в монолит. - Постепенно переносятся сценарии из монолита в сервис (например, "отмена подписки").
- Когда весь функционал перенесён — монолитный код удаляется.
Преимущества: минимальный риск — если новый сервис падает, шлюз может вернуть трафик в монолит (fallback). Можно развивать параллельно.
Недостатки: требуется шлюз; возможны проблемы с согласованностью данных (сервис и монолит могут работать с одной БД); сложность при shared-логике (например, проверка прав доступа).
Шлюз — компонент, обеспечивающий единый входной интерфейс для внешних клиентов и маршрутизацию запросов к внутренним сервисам.
fallback — заранее определённое поведение компонента при недоступности зависимости, направленное на сохранение частичной функциональности или информативного отклика.
2. По доменным контекстам (DDD-подход)
Выделяются ограниченные контексты — логически целостные области с собственной терминологией и правилами. Например:
- Контекст
Customer Management— профиль, аутентификация, роли. - Контекст
Order Fulfillment— заказы, инвентарь, доставка. - Контекст
Billing— платежи, выставление счётов, возвраты.
Для каждого контекста:
- выделяется подмножество классов и таблиц;
- строится антикоррупционный слой (ACL) — адаптеры для взаимодействия с другими контекстами;
- постепенно мигрируются данные и логика.
Преимущества: высокая семантическая целостность; минимизация межконтекстных зависимостей.
Недостатки: требует глубокого понимания домена; сложность при legacy-коде, где границы размыты.
Подмножество — выделенная часть функциональности или данных, обладающая внутренней согласованностью и пригодная к автономному управлению.
Антикоррупционный слой — промежуточный компонент, изолирующий новую архитектуру от устаревшей части системы, преобразующий данные и протоколы взаимодействия.
Семантическая целостность — соответствие данных и операций над ними смысловым правилам предметной области, обеспечиваемое даже при распределении логики между компонентами.
3. По техническим характеристикам
Выделяются компоненты, чьи требования к инфраструктуре резко отличаются:
- Вычислительно тяжёлые задачи (расчёт кредитного скоринга, обработка изображений) — выносятся в отдельные сервисы с GPU или HPC-оптимизацией.
- Высокочастотные API (публичные эндпоинты) — выносятся в лёгкий фронт-сервис (например, на Go или Rust), снижая нагрузку на основной монолит.
- Асинхронные процессы (отправка email, генерация отчётов) — переводятся на событийную модель с очередями.
Эта стратегия даёт быстрый эффект по производительности и отказоустойчивости.
Этап 2. Управление данными
Наиболее частая ошибка — выделить сервис, но оставить ему прямой доступ к той же базе данных, что и у монолита. Это иллюзия: связанность сохраняется на уровне схемы, и любое изменение таблицы может сломать оба компонента.
Корректные подходы:
1. Собственная БД на первом этапе (Shared Database → Owned Schema)
- Шаг 1 — Новый сервис получает доступ к отдельной схеме в той же БД (например,
subscriptions_schema), данные дублируются через триггеры или ETL (пакетная загрузка, чанки, checkpoint). - Шаг 2: Постепенно мигрируется логика записи — сначала чтение из новой схемы, потом запись.
- Шаг 3: Отключается доступ к старой схеме; данные конвертируются окончательно.
Плюс: минимизирует простои.
Минус: требует двойной записи (dual-write) и механизмов разрешения конфликтов.
Двойная запись — стратегия одновременного обновления данных в старой и новой системах во время переходного периода для обеспечения согласованности и возможности отката.
2. Событийная синхронизация (Event Sourcing / CDC)
- Изменения в монолитной БД фиксируются как события (через лог транзакций — Change Data Capture, или через доменные события в коде).
- Новый сервис подписывается на эти события и обновляет свою БД.
- Когда синхронизация стабильна — переключается трафик.
Этот подход обеспечивает eventual consistency, но требует идемпотентных обработчиков и инфраструктуры трассировки.
eventual consistency — модель согласованности данных, при которой система гарантирует достижение одинакового состояния во всех репликах после завершения всех обновлений и отсутствия новых изменений.
Идемпотентность — свойство операции, при котором повторный вызов с теми же параметрами не изменяет результат по сравнению с первым вызовом.
3. Антикоррупционный слой (ACL)
Если прямой доступ к данным невозможен (например, внешняя система), строится ACL — компонент, инкапсулирующий всю логику взаимодействия:
- преобразование типов (монолитный
DateTime→ ISO-строка); - повторные попытки, таймауты;
- кэширование;
- адаптация к семантике ("статус 3 в монолите" →
OrderStatus.Shipped).
ACL защищает новый сервис от нестабильности и несогласованности внешнего API.
Этап 3. Управление транзакциями в распределённой среде
В монолите транзакция BEGIN → INSERT → UPDATE → COMMIT работает "из коробки". В распределённой системе — нет. Варианты:
-
Саги (Saga Pattern) — разбиение транзакции на последовательность локальных транзакций с компенсирующими действиями. Например:
ReserveInventory— резервируем товар.ChargePayment— списываем деньги.- Если шаг 2 падает →
CancelReservation.
Реализуется через orchestration (центральный оркестратор) или choreography (события).
orchestration — централизованный подход к координации взаимодействия сервисов, при котором один компонент управляет последовательностью шагов рабочего процесса.
choreography — децентрализованный подход к координации, при котором каждый сервис реагирует на события, публикуемые другими, без центрального контроллера.
-
Двухфазный коммит (2PC) — возможен в рамках одной СУБД (например, распределённые транзакции в MS DTC), но не масштабируется и не поддерживается в большинстве NoSQL и облачных БД.
-
Идемпотентность — проектирование операций так, чтобы повторный вызов не менял результат (
PUT /orders/{id}с полным состоянием, а неPATCHс инкрементом).
Выбор зависит от требований к консистентности: для банков — саги с компенсацией; для соцсетей — eventual consistency допустима.
Этап 4. Эволюция
Декомпозиция — это многолетний процесс. Успешные примеры (например, Netflix, Amazon) заняли годы.
Важно:
- Начинать с малого — выделить один сервис, отработать процессы (CI/CD, мониторинг, отладка), затем масштабировать подход.
- Измерять эффект — до и после — время развёртывания, MTTR, количество инцидентов, скорость внедрения фич.
- Не стремиться к "чистым" микросервисам — гибридные архитектуры (микросервисы + монолитные модули) — норма на промежуточных этапах.
- Инвестировать в инфраструктуру — без централизованного логирования, трассировки, health-check’ов декомпозиция обернётся ростом сложности без выгод.
Разбор — когда резать рано
Сверьтесь с пятью вопросами. Команда 8 человек, боль — "сборка 12 минут". Плохо: 15 микросервисов. Лучше: модули в монолите, ArchUnit, CI-кэш; резать при конфликте двух команд над одним релизом, отдельной нагрузке на модуль или отсутствии владения границей.
Что запомнить
Пять вопросов → карта → стратегия → свои данные → саги → измерение. Дальше: Практика архитектурного проектирования · Паттерны микросервисной архитектуры · Паттерны перехода от монолита к микросервисам · Модульный монолит · Проектирование и архитектура — итоги
Как не "перерезать" систему
Распространённая ошибка — пытаться выделить слишком много сервисов сразу. Практичнее идти волнами:
- сначала выделить один хорошо понятный контекст с чёткой метрикой эффекта;
- стабилизировать эксплуатацию (логи, алерты, трассировка, rollback-сценарий);
- только потом брать следующий контекст.
Эволюционная декомпозиция почти всегда дешевле и безопаснее, чем одномоментный рефакторинг "большим взрывом".
Критерии готовности к следующему шагу
К следующему выделению сервиса стоит переходить, когда:
- у текущего выделенного сервиса понятные SLO/SLA и стабильный прод;
- команда умеет быстро диагностировать инциденты в распределённом взаимодействии;
- контракты между компонентами формализованы и версионируются;
- время релиза и MTTR объективно улучшились относительно монолита.
Кейсы декомпозиции: удачные и неудачные
Кейс 1: удачное выделение по нагрузке
- Исходно: модуль генерации отчётов периодически "ронял" общий монолит под пиковыми задачами.
- Решение: вынесли отчёты в отдельный асинхронный сервис с очередью.
- Результат: основной пользовательский контур стабилизировался, пики изолированы.
Кейс 2: неудачное выделение без собственных данных
- Исходно: новый сервис заказов продолжил работать напрямую с таблицами монолита.
- Проблема: формально сервис отдельный, но зависимость осталась на уровне схемы БД.
- Исправление — собственная схема, событийная синхронизация, ACL.
- Результат: реальная, а не номинальная независимость.
Кейс 3: слишком ранняя декомпозиция
- Исходно: маленькая команда и основная боль только в долгой сборке.
- Ошибка: сразу выделили много микросервисов.
- Исправление: вернулись к модульному монолиту и автоматизировали CI.
- Результат: быстрее time-to-market при меньшей операционной цене.