Перейти к основному содержимому

Стратегии декомпозиции монолитных систем

Разработчику Архитектору Аналитику
Теория данных (раздел 3)
Две статьи — два вопроса

Здесь (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). Например:

  1. Добавляется шлюз перед монолитом.
  2. Для нового функционала (например, "подписки") создаётся отдельный сервис.
  3. Шлюз направляет /api/subscriptions/* в новый сервис, всё остальное — в монолит.
  4. Постепенно переносятся сценарии из монолита в сервис (например, "отмена подписки").
  5. Когда весь функционал перенесён — монолитный код удаляется.

Преимущества: минимальный риск — если новый сервис падает, шлюз может вернуть трафик в монолит (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) — разбиение транзакции на последовательность локальных транзакций с компенсирующими действиями. Например:

    1. ReserveInventory — резервируем товар.
    2. ChargePayment — списываем деньги.
    3. Если шаг 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 при меньшей операционной цене.

См. также