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). Например:
- Добавляется шлюз перед монолитом.
- Для нового функционала (например, «подписки») создаётся отдельный сервис.
- Шлюз направляет
/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. - Шаг 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’ов декомпозиция обернётся ростом сложности без выгод.