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

6.11. Декомпозиция монолита

Разработчику Архитектору Аналитику

Декомпозиция монолита

Декомпозиция — это средство достижения целей: ускорение разработки, повышение отказоустойчивости, возможность независимого масштабирования, снижение времени развёртывания, сокращение инцидентов. Поэтому первое, что следует сделать перед началом — чётко сформулировать метрику успеха: что должно улучшиться и на сколько? Без этого декомпозиция превращается в техническое упражнение с неопределённой отдачей.

Декомпозиция — процесс разделения единого программного модуля на несколько независимых компонентов с чёткими границами ответственности и взаимодействием через явные интерфейсы.

Метрика успеха — количественный или качественный показатель, используемый для оценки эффективности декомпозиции, например время развёртывания, частота сбоев или скорость разработки новых функций.


Этап 0. Анализ и картирование

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

Картирование — создание структурированного представления архитектуры системы, включающего компоненты, их связи и потоки данных.

Анализ зависимостей — исследование связей между модулями кода с целью выявления циклических, тесных или неочевидных взаимозависимостей, препятствующих разделению.

Зависимости — отношения между компонентами, при которых один компонент требует наличия, корректного состояния или поведения другого для выполнения своей функции.

Декомпозиция начинается с понимания текущей структуры — особенно если монолит развивался без чётких границ.

Техники анализа:

  • Статический анализ зависимостей. Инструменты (NDepend, Structure101, JDepend, SonarQube) строят граф вызовов между модулями, классами, пакетами. Выявляются:
    • циклические зависимости — признак смешанных зон ответственности;
    • модули с высокой входящей связанностью (много кто зависит от них) — кандидаты в ядро;
    • модули с высокой исходящей связанностью (зависят от многих) — «клей», который нужно изолировать.

Связанность — степень, с которой компоненты зависят друг от друга; низкая связанность способствует гибкости и независимому развёртыванию.

  • Анализ по данным. Постройте heat-map обращений к таблицам БД: какие таблицы часто читаются/пишутся вместе? Какие запросы объединяют данные из разных логических областей? Это помогает выявить неявные границы домена — даже если код смешан.

heat-map — графическое представление интенсивности использования или взаимодействия между частями системы, где цвета отражают нагрузку, частоту вызовов или объём передаваемых данных.

  • Анализ по логам и метрикам. Где происходят пиковые нагрузки? Какие эндпоинты дают наибольшее количество ошибок? Какие компоненты наиболее часто меняются? Это указывает на «горячие точки», где выделение в отдельный сервис даст максимальный эффект.

  • Анализ по истории Git. Какие файлы чаще всего меняются вместе? Можно использовать git log --follow --name-only и построить матрицу коизменяемости. Файлы, которые почти всегда коммитятся в одном MR — кандидаты на один компонент (CCP в действии).

Результат — архитектурная карта: визуализация связей, выделение потенциальных границ, ранжирование по приоритету (влияние / стоимость).

Архитектурная карта — диаграмма, описывающая компоненты системы, их границы, связи и распределение по уровням абстракции или доменным областям.

Визуализация — отображение архитектурных элементов и их отношений в графической форме для упрощения понимания структуры и динамики системы.

Этап 1. Выбор стратегии

Не все части монолита одинаково подходят для выделения. Существует несколько стратегий, каждая — с разными рисками и выгодами.

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.
  • Шаг 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’ов декомпозиция обернётся ростом сложности без выгод.