Сервис из монолита выделяют так: находят шов в коде и данных, фиксируют текущее поведение тестами, превращают часть в модуль с интерфейсом внутри того же приложения, разделяют данные по стадиям, запускают новый сервис рядом со старым кодом в теневом режиме, переводят нагрузку порциями и только потом удаляют старое. Каждый шаг можно откатить, и ни один не требует остановки бизнеса.
Выделение нужно не всегда: сначала решите, нужен ли вам сервис, по статье «Микросервисы или монолит». Здесь предполагается, что решение принято, и разбирается техника. Общие правила работы с системой, которую нельзя остановить, описаны в статье «Как рефакторить legacy, не останавливая бизнес». Услуги описаны на страницах «Рефакторинг legacy-систем» и «Event-driven системы».
Что должно быть готово до первого шага
Выделение сервиса меняет много частей сразу, поэтому у системы должна быть страховка. Без неё первый же перенос превратится в угадывание.
| Условие | Почему нужно | Что делать, если нет |
|---|---|---|
| Тесты на текущее поведение | Показывают, что новая часть отвечает так же, как старая | Записать их до переноса, см. шаг 2 |
| Частые и обратимые выпуски | Малый шаг можно откатить, большой нет | Наладить сборку и развёртывание до начала выделения |
| Наблюдаемость | Нужно видеть ошибки и время ответа до и после переключения | Журналы, метрики и трассировка запросов на границе |
| Владелец части | Кто-то отвечает за новый сервис после переноса | Назначить до переноса, а не после |
| Запас по времени на сопровождение двух версий | Старый и новый код живут вместе до удаления | Заложить срок вывода старого кода в план |
Если условий нет, начинайте с них, а не с сервиса. Обычно это уже половина полезного эффекта, и выделять после этого приходится меньше.
Шаг 1. Найдите шов: где проходит граница
Хорошая граница пересекает как можно меньше данных и вызовов. Искать её нужно по трём источникам.
- Данные. Постройте матрицу «модуль — таблицы»: кто читает и кто пишет. Если часть читает и пишет собственные таблицы и лишь изредка заглядывает в чужие, это кандидат. Если ей нужны десять чужих таблиц, это не граница.
- Вызовы. Соберите, кто кого вызывает. Много вызовов внутрь и мало наружу — хороший признак: часть можно закрыть интерфейсом.
- История изменений. Файлы, которые меняются вместе, принадлежат одной части. Если правки разных модулей всегда идут в одном изменении, граница между ними проведена неверно.
| Что видите | Что это значит | Что делать |
|---|---|---|
| Часть пишет только свои таблицы | Данные хорошо отделимы | Брать первой |
| Часть читает чужие таблицы напрямую | Скрытая зависимость | Заменить чтение запросом через интерфейс владельца |
| Два модуля всегда меняются вместе | Граница проведена неверно | Объединить в одну часть или переопределить границу |
| Часть нужна почти всем | Общая функция: учёт пользователей, права, уведомления | Выделять после независимых |
| Внутри части транзакция охватывает заказ, склад и оплату | Нельзя разрезать без изменения процесса | Оставить вместе или заменить транзакцию обменом событиями |
Первой берут самую независимую часть: приём вебхуков, формирование файлов, уведомления. Ядро с заказами и деньгами выделяют последним, если выделяют вообще.
Шаг 2. Зафиксируйте текущее поведение
Задача сервиса — вести себя точно так же, как вела себя часть монолита, включая странности, на которые уже опираются другие. Правильность здесь определяется старым поведением, а не документацией.
Практические приёмы:
- Записанные запросы и ответы. Снимите реальный поток на границе части, обезличьте и сохраните вместе с ответами старой реализации. Это готовый набор для сравнения.
- Контрактные тесты. Описание того, что принимает и отдаёт часть: поля, коды ошибок, форматы дат и сумм. Тест запускается и на старой реализации, и на новой.
- Тесты на странные случаи. Пустые значения, длинные строки, повторные запросы, запросы не по порядку. Чаще всего ломается именно здесь.
Тест, написанный по старой реализации, называют характеризационным: он не говорит, что «правильно», он говорит, что «так есть сейчас». Менять это поведение надо отдельным решением, а не в ходе переноса.
Шаг 3. Отделите часть внутри монолита
Сервис лучше начинать как обычный модуль в том же приложении, и только потом выносить в отдельный процесс. Так проверяется граница без сети и без развёртывания.
- Соберите код части в одном месте и закройте его интерфейсом: остальной код обращается только к нему.
- Запретите обходные обращения: проверка в сборке, которая падает, если чужой код читает таблицы или вызывает внутренние функции модуля.
- Замените чужие обращения к его таблицам вызовами интерфейса. Пока всё работает в одном процессе, ошибки видны сразу.
- Включите контрактные тесты и убедитесь, что они проходят.
Часто на этом шаге выясняется, что выделять сервис уже не нужно: модуль с чёткими границами решил проблему порядка в коде и выпусков. Это нормальный результат.
Шаг 4. Разделите данные по стадиям
Данные разделяют не за один раз. Каждая стадия обратима, а переход к следующей делается после проверки.
| Стадия | Как устроено | Что опасно | Как вернуться |
|---|---|---|---|
| 1. Общая база, отдельные права | Таблицы части видны только её учётной записи, остальные читают через интерфейс | Скрытые чтения из отчётов и скриптов вскроются здесь | Вернуть права |
| 2. Отдельная схема | Таблицы части лежат в своей схеме той же базы | Запросы с соединением таблиц разных схем перестанут работать | Вернуть таблицы или представления |
| 3. Копия данных в новом хранилище | Новый сервис пишет и читает свою базу, изменения доезжают в монолит событиями или репликацией | Расхождение копий; задержка, которую увидит пользователь | Переключить чтение обратно; копия остаётся |
| 4. Единственный владелец | Пишет только сервис, монолит читает через его интерфейс или копию | Теперь откат означает возврат данных | Только по подготовленному плану, с копией перед шагом |
Между стадиями 3 и 4 нужна сверка: сравнение числа записей, сумм и выборки по полям на живых данных. Расхождение должно разбираться, а не списываться на задержку. Как делать так, чтобы событие не терялось и не дублировалось при обмене между базами, описано в статье «Outbox и гарантированная доставка простыми словами».
Шаг 5. Вынесите и переключите нагрузку порциями
Когда граница и данные готовы, часть выносят в отдельный процесс и ставят перед ней прослойку, которая решает, кого обслуживает старый код, а кого новый сервис. Переключение идёт по лестнице:
- Теневой режим. Запрос выполняет старый код, а копия уходит в новый сервис. Ответ нового никто не видит, но его сравнивают со старым. Расхождения разбираются до того, как кто-то пострадал.
- Малая доля. Новый сервис отвечает настоящим пользователям, но только небольшой доле: внутренним сотрудникам, одной организации, одному региону. Выбирайте долю по признаку, который проще откатить, а не по случайному проценту запросов.
- Расширение. Доля растёт ступенями; на каждой ступени выдерживается срок, достаточный, чтобы увидеть редкие случаи, например конец месяца.
- Полное переключение. Старый код остаётся доступным, и прослойку можно вернуть на него одним действием.
Откат должен быть проверен заранее: на тестовом стенде переключите трафик туда и обратно и убедитесь, что данные после возврата в порядке. Откат, который ни разу не пробовали, не является откатом. Переключатель лучше держать в конфигурации, а не в коде, чтобы возврат не требовал выпуска.
Что ломается чаще всего
Ломаются обычно одни и те же места. Они не зависят от языка и платформы.
| Что ломается | Почему | Что делаем |
|---|---|---|
| Транзакция через границу | Раньше заказ, склад и оплата менялись одной транзакцией, теперь в разных хранилищах | Разбить на шаги с обменом сообщений и компенсацией; или не резать там, где нужна единая транзакция |
| Отчёты и выгрузки | Они соединяли таблицы, которые теперь лежат в разных базах | Отдельное хранилище для отчётов, наполняемое событиями |
| Сквозные номера и справочники | Один счётчик или справочник обслуживал все части | Назначить владельца справочника; остальные читают копию или спрашивают его |
| Права доступа | Права проверялись в общем коде и по общей таблице пользователей | Единый вход и проверка прав на каждом сервисе |
| Повторные сообщения | Сеть и очереди доставляют повторно | Идемпотентная обработка: повтор не создаёт дубль |
| Скрытые потребители | Скрипт или внешняя система читала таблицу напрямую | Поиск по журналам запросов к базе до смены прав, а не после |
Обмен событиями между монолитом и сервисом удобен, но добавляет свои задачи: порядок сообщений, повторы, задержки. Когда событийный обмен уместен и когда избыточен, объясняет статья «Событийная архитектура: что это и когда нужна».
Шаг 6. Удалите старое, иначе получится две системы
Последний шаг чаще всего пропускают. Старый код «пока оставили на всякий случай», и через год две реализации отличаются, а команда не знает, какая настоящая. Поэтому в плане выделения заранее записывают:
- дату удаления старой реализации и условия, при которых она переносится;
- что именно удаляют: код, таблицы, настройки, тесты, документацию;
- кто подтверждает, что вызовов старого кода не осталось: по журналу, а не по ощущению;
- что делать с копией данных старой стороны: сколько хранить и когда удалить.
После удаления сервису нужен владелец, мониторинг и план обновлений. Выделение, которое не передали в сопровождение, откладывает проблему на другое место.
Что делаем мы и с чего начать
Начинаем с обследования: 200 000 ₽ и 2–3 недели на один контур, 350 000 ₽ и 3–4 недели, если система окружена несколькими другими. На выходе карта границ с матрицей «модуль — таблицы», рекомендация, какую часть выделять первой и нужно ли это вообще, план стадий и вилка бюджета. Сумма вычитается из первого этапа, если продолжаете с нами. Затем пилот на одной части: от 150 000 до 400 000 ₽ за 2–8 недель, с нагрузочными испытаниями до и после.
Мы выделяем одну часть или один контур, а не переписываем систему целиком, и не обещаем сервисов ради сервисов: если хватает модульного монолита, скажем об этом. Исходники лежат в вашем репозитории с первого коммита, остановиться можно после любой ступени. Цены и сроки — на странице «Тарифы», описание подхода — на странице «Микросервисы и высоконагруженные системы».
Частые вопросы
С какой части монолита лучше начинать выделение?
С самой независимой: приём вебхуков, формирование файлов и документов, уведомления. Они пишут свои данные и мало читают чужие. Ядро с заказами и деньгами трогают последним, если вообще трогают.
Можно ли выделить сервис, если нет автоматических тестов?
Можно, но сначала придётся записать тесты на текущее поведение: запросы и ответы на границе части, контрактные тесты, странные случаи. Без этого вы не узнаете, что новая реализация ведёт себя иначе, пока этого не заметят пользователи.
Что делать с общей базой данных?
Разделять по стадиям: сначала отдельные права, затем отдельная схема, затем копия в новом хранилище и только в конце единственный владелец. Каждая стадия обратима, а переход к следующей делается после сверки данных.
Как проверить, что новый сервис работает так же, как старый код?
Теневым режимом: запрос выполняет старый код, копия уходит в новый сервис, ответы сравниваются. Расхождения разбираются до того, как новый сервис начнёт отвечать пользователям.
Как быть с транзакциями, которые пересекают границу?
Либо не резать там, где нужна единая транзакция, либо разбить процесс на шаги с обменом сообщений и компенсирующими действиями. Второй путь сложнее и требует идемпотентной обработки повторов.
Когда выделять сервис не стоит?
Когда у части нет отдельного владельца, скорости изменений или нагрузки, которые отличались бы от остального, и когда в команде нет людей для эксплуатации нескольких сервисов. Часто хватает модульного монолита с проверкой границ в сборке.
Сколько времени занимает выделение одного сервиса?
Зависит от связности части и состояния данных. Узнать срок можно после обследования: 200 000 ₽ за 2–3 недели на один контур. Пилот на одной части занимает 2–8 недель и стоит 150 000–400 000 ₽.
Что делать, если после переключения всё пошло не так?
Вернуть трафик на старый код переключателем в конфигурации, не дожидаясь выпуска. Поэтому откат нужно проверить заранее на стенде и не удалять старую реализацию, пока новая не отработала весь цикл, включая конец месяца.