Микросервисы или монолит: когда переходить и как не получить распределённый монолит
Монолит, модульный монолит или микросервисы: признаки, что архитектура тормозит бизнес, реальная цена микросервисов, чек-лист готовности, пошаговый переход по паттерну Strangler Fig и уроки Segment и Prime Video.
Микросервисы часто выбирают не под задачу, а под образ «так строят большие компании». В результате продукт с одной командой получает десяток сервисов, Kubernetes, очередь сообщений и распределённую отладку — и начинает развиваться медленнее, чем развивался бы монолит. Обратная ошибка тоже встречается: система давно переросла одну кодовую базу, команды мешают друг другу, а архитектуру не трогают, потому что «переписывание — это риск». Ниже — как понять, какая архитектура нужна именно вашему продукту, по каким признакам монолит действительно тормозит бизнес, что стоят микросервисы в эксплуатации и как переходить постепенно, не останавливая разработку и не получив в итоге «распределённый монолит». Из чего на самом деле выбирают Спор «монолит или микросервисы» упрощает картину. На практике вариантов три, и средний из них чаще всего оказывается самым разумным. Вариант Как устроен Когда подходит Монолит Одна кодовая база, один процесс развёртывания, одна база данных Новый продукт, одна команда, домен ещё меняется Модульный монолит Одна кодовая база и один деплой, но жёсткие границы между модулями и доступ друг к другу только через публичные интерфейсы Продукт растёт, команд несколько, но независимый деплой пока не критичен Микросервисы Набор независимо развёртываемых сервисов, у каждого свои данные и своя команда-владелец Много команд, разные требования к масштабированию и надёжности частей системы Ключевое отличие микросервисов — не размер кода, а независимость: сервис можно изменить, протестировать и выкатить, не согласовывая релиз с остальными. Если этой независимости нет, дробление на сервисы добавляет сетевые вызовы, но не добавляет скорости. Почему монолит — не устаревшее решение Монолит выигрывает там, где важны скорость изменений и простота: локальный запуск всей системы, обычные транзакции в базе данных, стектрейс вместо распределённой трассировки, один конвейер сборки. Для продукта, чья модель ещё ищет форму, это преимущество решающее: границы между модулями меняются каждые несколько месяцев, и в одной кодовой базе такой рефакторинг стоит дни, а между сервисами — недели. Мартин Фаулер в заметке MonolithFirst (2015) описывает закономерность: успешные микросервисные системы обычно вырастали из монолита, который стал слишком большим, а системы, сразу спроектированные как микросервисы, чаще сталкивались с серьёзными проблемами. Причина в том, что правильные границы сервисов видны только после того, как домен изучен на практике. Крупные публичные примеры это подтверждают. Stack Overflow в 2016 году, по описанию архитектора Ника Крейвера, обслуживал около 1,3 млрд просмотров страниц в месяц монолитным приложением на .NET и девяти веб-серверах. Shopify — одна из крупнейших кодовых баз на Ruby on Rails — вместо дробления на микросервисы пошла по пути модульного монолита: разделила код на компоненты с проверяемыми границами и описала этот опыт в своём инженерном блоге. Когда от микросервисов возвращаются обратно Две истории разбирают на архитектурных конференциях чаще других — именно потому, что они про обратный переход. Segment, 2018. Компания вынесла отправку данных в каждую внешнюю интеграцию в отдельный сервис. Интеграций становилось больше, вместе с ними росло число репозиториев, очередей и сервисов, а операционная нагрузка росла линейно. В статье «Goodbye Microservices» команда рассказала, как объединила более 140 сервисов в один и упростила тестирование и поддержку общих библиотек. Prime Video, 2023. Команда сервиса мониторинга качества видео и звука построила его как распределённую систему на бессерверных компонентах. По словам инженеров, такая архитектура упёрлась в предел масштабирования примерно на 5% ожидаемой нагрузки, а стоимость составных частей оказалась слишком высокой. Компоненты объединили в одно приложение, работающее в одной задаче Amazon ECS, и расходы на инфраструктуру этого сервиса снизились на 90%. Важная деталь, которую часто упускают при пересказе: речь об одной подсистеме, а не о переводе всего Prime Video на монолит. Вывод из обеих историй не в том, что микросервисы плохи. Архитектура должна соответствовать характеру нагрузки и устройству команды. Когда части системы всё время обмениваются большими объёмами данных и меняются вместе, разнесение их по сети превращает внутренние вызовы функций в дорогую и медленную инфраструктуру. Признаки того, что монолит тормозит бизнес Решение о переходе стоит принимать по наблюдаемым проблемам, а не по ощущению, что «пора». Вот сигналы, которые можно измерить в своей команде. Релизы становятся редкими и крупными Сборка и полный прогон тестов занимают столько времени, что изменения копят и выкатывают пачками. Каждый релиз превращается в событие с заморозкой кода, а исправление ошибки ждёт ближайшего окна. Сначала стоит проверить, не решается ли это ускорением конвейера: параллельные тесты, кэш сборки, автоматический деплой. Если после этого релизы всё равно блокируют друг друга — проблема архитектурная. Команды мешают друг другу Несколько команд работают в одной кодовой базе, и ошибка одной откатывает релиз другой. Постоянные конфликты при слиянии, общие файлы, которые правят все, и долгие согласования изменений схемы данных — признак того, что границы ответственности не совпадают с границами кода. Сбой второстепенной функции роняет основную Зависшая генерация отчётов или перегруженная отправка писем делает недоступным оформление заказа. Частично это решается внутри монолита — отдельными процессами-обработчиками, очередями, ограничением ресурсов. Но если изоляция нужна многим частям системы, отдельные сервисы становятся естественным решением. Части системы требуют разного масштабирования Модулю обработки изображений нужны мощные процессоры, каталогу — много реплик на чтение, административной части — почти ничего. В монолите масштабируется всё сразу, и за ресурсы для одной функции платит весь продукт. Технологический стек блокирует развитие Часть системы написана на устаревшей версии языка или фреймворка, обновить её целиком слишком рискованно, а специалистов на этот стек найти всё сложнее. Постепенный вынос функций в новые сервисы позволяет обновлять систему по частям. Что стоят микросервисы в эксплуатации Выгоды микросервисов реальны, но за них платят постоянно, а не один раз при переходе. До решения стоит честно оценить каждую статью расходов. Инфраструктура. Оркестрация контейнеров, реестр образов, API-шлюз, брокер сообщений, отдельные базы данных, среды для тестирования взаимодействия сервисов. Наблюдаемость. Централизованные логи, метрики по каждому сервису, распределённая трассировка. Без них поиск причины медленного запроса превращается в расследование. Сетевые отказы. Каждый вызов между сервисами может завершиться таймаутом. Нужны повторы, ограничители, резервное поведение и продуманные таймауты. Согласованность данных. Транзакция, которая в монолите занимала одну строку кода, превращается в последовательность шагов с компенсирующими действиями. Контракты. Изменение формата данных требует версионирования API и проверки обратной совместимости. Люди. Появляется работа по платформе: кто-то должен владеть кластером, конвейерами и стандартами для всех сервисов. Если выгоды — независимый деплой, изоляция отказов, раздельное масштабирование — не перекрывают эти расходы, переход ухудшит экономику продукта. Практичнее всего оценивать это на своих данных: сколько времени сейчас уходит на ожидание релизов и разбор конфликтов и сколько потребует поддержка новой инфраструктуры. Чек-лист готовности к микросервисам Есть конкретная проблема. Вы можете назвать, что именно решит переход: частоту релизов, изоляцию отказов, масштабирование конкретного модуля. «Так правильно» — не проблема. Есть команды-владельцы. Каждый будущий сервис кому-то принадлежит. Микросервисы — прежде всего организационное решение: по закону Конвея структура системы повторяет структуру коммуникаций в организации. Деплой автоматизирован. Сборка, тесты и выкатка монолита уже проходят без ручных шагов, приложение упаковано в контейнер. Есть наблюдаемость. Логи, метрики и алерты настроены хотя бы для текущей системы. Границы доменов понятны. Команда может нарисовать схему бизнес-доменов и их зависимостей без многочасового спора. Если нет — начните с анализа домена, например с сессии Event Storming. Заложены ресурсы. Переход идёт параллельно с продуктовой разработкой и отнимает у неё заметную часть времени на протяжении месяцев. Если большинство пунктов пока не выполнено, выгоднее вложиться в модульность и автоматизацию текущей системы. Эта работа всё равно понадобится при переходе, но окупится и без него. Как переходить: пошаговая стратегия «Большое переписывание» — остановить развитие старой системы и написать новую с нуля — рискованный путь: бизнес месяцами не получает новых функций, а новая система должна повторить поведение старой, включая неописанные особенности. Проверенная альтернатива — постепенная замена. Шаг 1. Модульный монолит Прежде чем выносить что-то в сеть, наведите порядок внутри. Выделите модули по бизнес-доменам, запретите обращение к внутренностям чужих модулей, переведите взаимодействие на явные интерфейсы и внутренние события. На этом этапе становится видно, где границы проведены удачно, а где модули связаны сильнее, чем казалось. Одна из самых полезных практик — разделить данные логически, ещё не разделяя физически. В PostgreSQL для этого подходят схемы и права доступа: -- Логические домены в одной базе данных CREATE SCHEMA orders; CREATE SCHEMA catalog; CREATE SCHEMA notifications; -- Модуль заказов работает только со своей схемой GRANT USAGE ON SCHEMA orders TO orders_app; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA orders TO orders_app; REVOKE ALL ON SCHEMA catalog FROM orders_app; После этого запросы, которые соединяют таблицы разных доменов, сразу перестают работать, и их приходится заменять вызовом интерфейса другого модуля или копированием нужных данных на момент события: -- Было: заказ читает название товара из чужого домена SELECT o.id, p.name FROM orders.order_items o JOIN catalog.products p ON p.id = o.product_id; -- Стало: в позиции заказа хранится снимок данных товара на момент покупки INSERT INTO orders.order_items (order_id, product_id, product_name, price) VALUES ($1, $2, $3, $4); Снимок данных здесь — не компромисс, а корректная бизнес-модель: цена и название в заказе должны оставаться такими, какими были при покупке, даже если каталог изменится. Шаг 2. Паттерн Strangler Fig Название паттерна предложил Мартин Фаулер по аналогии с фикусом-душителем, который постепенно обрастает дерево-носитель. Перед монолитом ставится маршрутизирующий слой — API-шлюз или обратный прокси. Новая функциональность и вынесенные модули обслуживаются отдельными сервисами, остальные запросы по-прежнему идут в монолит. Маршруты переключаются по одному, и откатить неудачное переключение можно без выпуска новой версии. Монолит при этом не обязан исчезнуть полностью. Нормальный итог перехода — несколько сервисов для частей с особыми требованиями и ядро, которое удобнее развивать единой кодовой базой. Шаг 3. Первый сервис Для первого выноса выбирайте модуль с понятными границами, небольшим числом зависимостей и невысокой ценой ошибки: уведомления, генерация документов, поиск, обработка файлов. Цель первого сервиса — не столько польза сама по себе, сколько отработка всей цепочки: шаблон репозитория, сборка образа, деплой, мониторинг, контракт взаимодействия, дежурства. Платёжный модуль, корзина и ядро бизнес-логики — плохие кандидаты для старта: у них много зависимостей и высокая цена сбоя. Шаг 4. Разделение данных Разделить код обычно проще, чем данные. Главное правило: сервис владеет своими данными, и никто другой не читает и не пишет его таблицы напрямую. Общая база данных у нескольких сервисов — классический признак распределённого монолита: схему нельзя менять без согласования всех команд, а независимый деплой остаётся на бумаге. Отказ от общей базы означает отказ от транзакций между сервисами. Данные в разных сервисах ст