Сервис из монолита выделяют так: находят шов в коде и данных, фиксируют текущее поведение тестами, превращают часть в модуль с интерфейсом внутри того же приложения, разделяют данные по стадиям, запускают новый сервис рядом со старым кодом в теневом режиме, переводят нагрузку порциями и только потом удаляют старое. Каждый шаг можно откатить, и ни один не требует остановки бизнеса.

Выделение нужно не всегда: сначала решите, нужен ли вам сервис, по статье «Микросервисы или монолит». Здесь предполагается, что решение принято, и разбирается техника. Общие правила работы с системой, которую нельзя остановить, описаны в статье «Как рефакторить legacy, не останавливая бизнес». Услуги описаны на страницах «Рефакторинг legacy-систем» и «Event-driven системы».

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

Что должно быть готово до первого шага

Выделение сервиса меняет много частей сразу, поэтому у системы должна быть страховка. Без неё первый же перенос превратится в угадывание.

УсловиеПочему нужноЧто делать, если нет
Тесты на текущее поведениеПоказывают, что новая часть отвечает так же, как стараяЗаписать их до переноса, см. шаг 2
Частые и обратимые выпускиМалый шаг можно откатить, большой нетНаладить сборку и развёртывание до начала выделения
НаблюдаемостьНужно видеть ошибки и время ответа до и после переключенияЖурналы, метрики и трассировка запросов на границе
Владелец частиКто-то отвечает за новый сервис после переносаНазначить до переноса, а не после
Запас по времени на сопровождение двух версийСтарый и новый код живут вместе до удаленияЗаложить срок вывода старого кода в план

Если условий нет, начинайте с них, а не с сервиса. Обычно это уже половина полезного эффекта, и выделять после этого приходится меньше.

Шаг 1. Найдите шов: где проходит граница

Хорошая граница пересекает как можно меньше данных и вызовов. Искать её нужно по трём источникам.

  • Данные. Постройте матрицу «модуль — таблицы»: кто читает и кто пишет. Если часть читает и пишет собственные таблицы и лишь изредка заглядывает в чужие, это кандидат. Если ей нужны десять чужих таблиц, это не граница.
  • Вызовы. Соберите, кто кого вызывает. Много вызовов внутрь и мало наружу — хороший признак: часть можно закрыть интерфейсом.
  • История изменений. Файлы, которые меняются вместе, принадлежат одной части. Если правки разных модулей всегда идут в одном изменении, граница между ними проведена неверно.
Что видитеЧто это значитЧто делать
Часть пишет только свои таблицыДанные хорошо отделимыБрать первой
Часть читает чужие таблицы напрямуюСкрытая зависимостьЗаменить чтение запросом через интерфейс владельца
Два модуля всегда меняются вместеГраница проведена неверноОбъединить в одну часть или переопределить границу
Часть нужна почти всемОбщая функция: учёт пользователей, права, уведомленияВыделять после независимых
Внутри части транзакция охватывает заказ, склад и оплатуНельзя разрезать без изменения процессаОставить вместе или заменить транзакцию обменом событиями

Первой берут самую независимую часть: приём вебхуков, формирование файлов, уведомления. Ядро с заказами и деньгами выделяют последним, если выделяют вообще.

Шаг 2. Зафиксируйте текущее поведение

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

Практические приёмы:

  • Записанные запросы и ответы. Снимите реальный поток на границе части, обезличьте и сохраните вместе с ответами старой реализации. Это готовый набор для сравнения.
  • Контрактные тесты. Описание того, что принимает и отдаёт часть: поля, коды ошибок, форматы дат и сумм. Тест запускается и на старой реализации, и на новой.
  • Тесты на странные случаи. Пустые значения, длинные строки, повторные запросы, запросы не по порядку. Чаще всего ломается именно здесь.

Тест, написанный по старой реализации, называют характеризационным: он не говорит, что «правильно», он говорит, что «так есть сейчас». Менять это поведение надо отдельным решением, а не в ходе переноса.

Шаг 3. Отделите часть внутри монолита

Сервис лучше начинать как обычный модуль в том же приложении, и только потом выносить в отдельный процесс. Так проверяется граница без сети и без развёртывания.

  1. Соберите код части в одном месте и закройте его интерфейсом: остальной код обращается только к нему.
  2. Запретите обходные обращения: проверка в сборке, которая падает, если чужой код читает таблицы или вызывает внутренние функции модуля.
  3. Замените чужие обращения к его таблицам вызовами интерфейса. Пока всё работает в одном процессе, ошибки видны сразу.
  4. Включите контрактные тесты и убедитесь, что они проходят.

Часто на этом шаге выясняется, что выделять сервис уже не нужно: модуль с чёткими границами решил проблему порядка в коде и выпусков. Это нормальный результат.

Шаг 4. Разделите данные по стадиям

Данные разделяют не за один раз. Каждая стадия обратима, а переход к следующей делается после проверки.

СтадияКак устроеноЧто опасноКак вернуться
1. Общая база, отдельные праваТаблицы части видны только её учётной записи, остальные читают через интерфейсСкрытые чтения из отчётов и скриптов вскроются здесьВернуть права
2. Отдельная схемаТаблицы части лежат в своей схеме той же базыЗапросы с соединением таблиц разных схем перестанут работатьВернуть таблицы или представления
3. Копия данных в новом хранилищеНовый сервис пишет и читает свою базу, изменения доезжают в монолит событиями или репликациейРасхождение копий; задержка, которую увидит пользовательПереключить чтение обратно; копия остаётся
4. Единственный владелецПишет только сервис, монолит читает через его интерфейс или копиюТеперь откат означает возврат данныхТолько по подготовленному плану, с копией перед шагом

Между стадиями 3 и 4 нужна сверка: сравнение числа записей, сумм и выборки по полям на живых данных. Расхождение должно разбираться, а не списываться на задержку. Как делать так, чтобы событие не терялось и не дублировалось при обмене между базами, описано в статье «Outbox и гарантированная доставка простыми словами».

Шаг 5. Вынесите и переключите нагрузку порциями

Когда граница и данные готовы, часть выносят в отдельный процесс и ставят перед ней прослойку, которая решает, кого обслуживает старый код, а кого новый сервис. Переключение идёт по лестнице:

  1. Теневой режим. Запрос выполняет старый код, а копия уходит в новый сервис. Ответ нового никто не видит, но его сравнивают со старым. Расхождения разбираются до того, как кто-то пострадал.
  2. Малая доля. Новый сервис отвечает настоящим пользователям, но только небольшой доле: внутренним сотрудникам, одной организации, одному региону. Выбирайте долю по признаку, который проще откатить, а не по случайному проценту запросов.
  3. Расширение. Доля растёт ступенями; на каждой ступени выдерживается срок, достаточный, чтобы увидеть редкие случаи, например конец месяца.
  4. Полное переключение. Старый код остаётся доступным, и прослойку можно вернуть на него одним действием.

Откат должен быть проверен заранее: на тестовом стенде переключите трафик туда и обратно и убедитесь, что данные после возврата в порядке. Откат, который ни разу не пробовали, не является откатом. Переключатель лучше держать в конфигурации, а не в коде, чтобы возврат не требовал выпуска.

Что ломается чаще всего

Ломаются обычно одни и те же места. Они не зависят от языка и платформы.

Что ломаетсяПочемуЧто делаем
Транзакция через границуРаньше заказ, склад и оплата менялись одной транзакцией, теперь в разных хранилищахРазбить на шаги с обменом сообщений и компенсацией; или не резать там, где нужна единая транзакция
Отчёты и выгрузкиОни соединяли таблицы, которые теперь лежат в разных базахОтдельное хранилище для отчётов, наполняемое событиями
Сквозные номера и справочникиОдин счётчик или справочник обслуживал все частиНазначить владельца справочника; остальные читают копию или спрашивают его
Права доступаПрава проверялись в общем коде и по общей таблице пользователейЕдиный вход и проверка прав на каждом сервисе
Повторные сообщенияСеть и очереди доставляют повторноИдемпотентная обработка: повтор не создаёт дубль
Скрытые потребителиСкрипт или внешняя система читала таблицу напрямуюПоиск по журналам запросов к базе до смены прав, а не после

Обмен событиями между монолитом и сервисом удобен, но добавляет свои задачи: порядок сообщений, повторы, задержки. Когда событийный обмен уместен и когда избыточен, объясняет статья «Событийная архитектура: что это и когда нужна».

Шаг 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 ₽.

Что делать, если после переключения всё пошло не так?

Вернуть трафик на старый код переключателем в конфигурации, не дожидаясь выпуска. Поэтому откат нужно проверить заранее на стенде и не удалять старую реализацию, пока новая не отработала весь цикл, включая конец месяца.