Монолит не означает провала. Большинство успешных продуктов начинаются с него, потому что одна кодовая база с одной базой данных быстрее всего показывает, чего хотят клиенты. Проблемы начинаются позже: релизы замедляются, изменение в биллинге ломает поиск, а к модулю заказов никто не хочет прикасаться. В этот момент кто-то предлагает миграцию, и появляется риск, что лечение навредит больше, чем болезнь.
В этой статье о том, как решить, нужна ли миграция, как найти, где резать, и как переносить работающую систему по частям, не останавливая бизнес, который от неё зависит.
Нужна ли миграция вообще
Начните с проблемы, а не с архитектуры. Фраза «нам нужны микросервисы» описывает решение. Проблема же обычно одна из этих:
- Релизы идут медленно или с риском, потому что любое изменение затрагивает всё.
- Одна часть системы должна масштабироваться совсем не так, как остальные.
- Команды блокируют друг друга, потому что все работают в одном коде.
- Часть стека больше не поддерживается, и обновить её на месте нельзя.
Запишите проблему и спросите себя, какое исправление самое дешёвое. Медленные релизы часто оказываются проблемой сборочного конвейера и тестов, а не архитектуры. Одну нагруженную точку API часто можно масштабировать кешем, очередью или репликой для чтения. Устаревший фреймворк иногда удаётся обновлять модуль за модулем внутри той же кодовой базы.
Если дешёвые исправления уже испробованы или явно не дотянут, миграция оправдана. Но и тогда у неё должна быть сформулированная цель, которую может проверить человек не из инженерной команды, например «мы можем выпустить оформление заказа без полного регрессионного прогона», а не «мы перешли на новую архитектуру».
Найдите швы, прежде чем резать
Швом называют место, где систему можно разделить с наименьшими потрясениями. Хорошие швы проходят по границам бизнеса, а не по структуре папок. Ищите области, которые:
- Говорят на своём языке. Если склад говорит «отгрузка», а отдел продаж «заказ» о связанных, но разных вещах, эта разница и есть граница.
- Меняются по своим причинам. Код, который всегда правят вместе, должен жить вместе. Код, который меняется в другом ритме, можно рассматривать как кандидата на перенос.
- Общаются с остальной системой через несколько понятных вызовов, а не через десятки общих таблиц.
Здесь помогает история версий. Файлы, которые меняются вместе в одних и тех же коммитах, показывают реальную связность, и она часто отличается от задуманной. Прежде чем выбрать первую часть, нарисуйте найденные зависимости, включая зависимости в базе данных.
Первая часть для переноса должна быть достаточно ценной, чтобы это имело значение, и достаточно небольшой, чтобы её закончить. Уведомления, отчёты, поиск и работа с файлами часто идут первыми, потому что находятся на краю системы. Ядро предметной области, то, что делает продукт самим собой, обычно переносят позже, когда команда уже потренировалась на чём-то менее критичном.
Вытесняйте старую систему по одному маршруту
Паттерн «фикус-душитель» (strangler fig), названный Мартином Фаулером в честь растения, которое разрастается вокруг дерева-хозяина, остаётся самым безопасным общим подходом. Вы не переписываете монолит. Вы ставите что-то перед ним и по частям уводите от него трафик.
На практике это выглядит так:
- Поставьте перед монолитом слой маршрутизации. Это может быть обратный прокси, API-шлюз или тонкий фасад в самом приложении.
- Постройте за этим слоем новую версию одной возможности.
- Направьте в новую версию небольшую долю трафика или одну группу пользователей и сравните результаты со старой.
- Увеличивайте долю, пока старый путь в коде не перестанет получать запросы, а затем удалите его.
Последний шаг важнее всего. Миграция, которая добавляет новый код, не удаляя старый, оставляет вам две системы в эксплуатации. Каждый срез должен заканчиваться удалением старого пути и упрощением правила маршрутизации.
Там, где новая и старая версии должны вести себя одинаково, какое-то время запускайте их параллельно. Отправляйте каждый запрос в обе, пользователю возвращайте ответ старой и записывайте в лог любые расхождения. Такой теневой период выявляет недокументированное поведение, которое есть в любой старой системе.
Решите, кто владеет данными
С кодом справиться проще всего. Большинство миграций застревает на общей базе данных. Если новый компонент и монолит читают и пишут одни и те же таблицы, вы ничего не разделили, а лишь добавили второго писателя в схему, которую теперь никто не может изменить.
Правило, к которому стоит стремиться: у каждого фрагмента данных один владелец. Записывает их только владелец. Все остальные обращаются к нему через API или слушают события, которые он публикует.
Прийти к этому можно только постепенно:
- Выясните, кто что пишет. Перечислите все таблицы и весь код, который в них пишет. Беспокоиться стоит о таблицах, в которые пишут из многих мест.
- Сначала сведите запись в одно место. Прежде чем переносить данные, сделайте так, чтобы сам монолит писал в каждую таблицу через единственный модуль. Одно это часто убирает половину связности.
- Скопируйте, затем переключите. Дайте новому компоненту собственное хранилище, синхронизируйте его со старым (захват изменений данных, change data capture, или двойная запись со сверкой) и переключите чтение раньше записи.
- Проверьте, что копии совпадают. Запустите задачу сверки, которая сравнивает старые и новые данные и сообщает о расхождениях. Не переключайтесь, пока она какое-то время не проработает без расхождений.
Отчёты и аналитика часто читают отовсюду. Дайте им собственную модель для чтения, которая наполняется событиями или репликацией, чтобы они перестали быть причиной сохранять старую схему.