

Евгений Чернышев
Техлид в «Долгосрочной аренде»
Здравствуйте, уважаемые читатели. Вот и настал тот день, когда мы поняли, что наш замечательный проект стал сложным. Спустя три года разработки его поддержка начала требовать значительных ресурсов, внедрение новых фич бизнеса замедлилось, а команда давно утратила прежний интерес. Большинство разработчиков неоднократно проходили эту стадию. Если посмотреть на ситуацию с другого ракурса, то всё далеко не так уж плохо: проект стал «взрослым», приносит доход, что позволяет его развивать. Однако сложность продолжает нарастать, проблемы напоминают снежный ком, и с этим необходимо что-то делать.
Решение лежит на поверхности: давайте разделим монолит на микросервисы! На первый взгляд, это сплошные преимущества: горизонтальное масштабирование, независимые команды, автономные сервисы, отказоустойчивость и независимые релизы. Недостатки отсутствуют, ну, или почти отсутствуют. А может, если быть честными, мы просто «заметаем их под ковёр»?
Что, если я скажу, что при таком подходе мы, скорее всего, получим с десяток микросервисов, которые синхронно вызывают друг друга по цепочке, знают подробности внутреннего устройства каждого, обращаются к общим таблицам (крайне запущенный случай), и отказ одного из них вызовет крах всей системы? Прямо как с костяшками домино. Встречайте, Его Величество Распределённый Монолит!
Что такое распределённый монолит
Распределённый монолит — это система, которая со стороны выглядит как микросервисная архитектура, но при этом сохраняет все свойства монолита:
Отсутствует возможность независимо разрабатывать и выпускать сервисы
Бизнес-операции проходят через цепочку синхронных вызовов нескольких сервисов
Изменения в одном сервисе требуют значительных доработок в других
Падение одного сервиса ломает если не всю, то большую часть системы
Данные размазываются, и ответственность за них между командами неясна
Границы сервисов проведены по техническим, а не по бизнес-признакам
То есть после запуска таких микросервисов мы не только не ушли от проблем монолита, но и добавили к ним неизбежную цену внедрения распределённых систем (согласованность данных, сложность администрирования, проблемы с тестированием и т.д.).
Как к этому приходят
Путь, неизбежно ведущий на тёмную сторону, начинается вполне безобидно. Сначала у нас есть небольшое монолитное приложение. Со временем оно растёт: бизнес проверяет гипотезы, некоторые из которых взлетают, большинство же оказываются ошибочными. Но код, написанный для тестирования этих гипотез, чаще всего остаётся мёртвым грузом. Так в монолите появляются большие модули, сложные связи, неочевидные зависимости, больные места в коде, да и просто костыли.
Затем у команды возникает идея: «Монолит не даёт проекту развиваться, давайте разберём его на микросервисы». Однако вместо анализа предметной области и бизнес-процессов разработчики начинают разрезать проект по техническим признакам.
В качестве примера возьмём гипотетический интернет-магазин строительных товаров. Изначально это монолит: каталог товаров, корзина, оплата, складские остатки, личный кабинет, какие-то интеграции с 1С и контрагентами. Всё в одном репозитории. Совсем не идеально, порой болезненно, но разработчик хотя бы может открыть код и проследить путь от нажатия пользователем кнопки «Оформить заказ» до статуса «Передан в доставку». И тут микросервисы появляются не как результат тщательного проектирования и анализа бизнес-потребностей, а как реакция на боль:
Тормозит каталог товаров? Вынесем каталог в отдельный сервис.
Доставка мешает релизить фичи? Вынесем доставку.
Платежи требуют повышенной надёжности? Вынесем платежи.
Каждое отдельное решение кажется разумным. Но если у команды нет общей модели и понимания бизнеса, то через год она получает не стройную архитектуру, а карту боевых действий: сервисы, которые нельзя менять независимо; контракты, которые никто не может вспомнить; события, которые вроде бы кто-то слушает; бизнес-процессы, размазанные по нескольким кодовым базам.
В итоге команда не выделила Bounded Context (границы связанности), а просто раскидала больные куски по отдельным репозиториям.
Признаки строительства распределённого монолита
На начальном этапе внедрения микросервисов этот антипаттерн увидеть непросто. Однако спустя год-полтора разработки можно понять, что:
Сервис нельзя менять независимо. Когда изменение в одном сервисе требует синхронного внесения изменений в три других, это не автономность, а лишь более дорогая форма связанности. Сервисы приходится релизить пачками, несмотря на то, что формально они разные. Типичный пример — изменение контракта одного сервиса, которое влечёт за собой изменения в других.
Сервисы постоянно ходят друг в друга. Обычный вызов HTTP endpoint пользователя с фронтенда превращается в последовательную цепочку вызовов уже где-то внутри приложения. Если в середине этой цепочки сервис тормозит или падает, то клиент ждёт или получает неожиданный ответ.
Отсутствие у сервисов чётких владельцев данных. Например, изначально предполагалось, что информация о количестве оставшихся мешков штукатурки хранится в сервисе «Склад». Однако данные об актуальных остатках требуются и каталогу товаров. Сервис «Корзина товаров» проверяет наличие выбранных покупателем стройматериалов и т.д.
Сложности с локальной разработкой. Для старта разработки новой фичи разработчику приходится запускать на локальной машине множество компонентов: несколько сервисов, брокер сообщений, мок-сервисы, миграции и другие внутренние инструменты. Возможно, Docker Compose частично решит эту проблему. Но почему её вообще приходится решать?
Раньше с монолитом было всё плохо. Теперь тоже плохо, но к тому же всё распределено по микросервисам.

Почему «просто распилить монолит» не работает
Проблемы в монолитной архитектуре возникают не из-за её размера, а из-за размытых границ ответственности.
Например, где-то в дебрях исходного кода есть метод checkout, к которому за годы разработки много чего прилипло: проверка остатков цемента и штукатурки на складе, расчёт скидок по акциям, обращение к 1С, создание заказа, отправка уведомлений клиенту и событий в брокер сообщений, а также отправка на фронтенд сообщения: «Сегодня не можем доставить заказ в ваш район». Да, это одна часть бизнес-процесса, но внутри смешались в кучу кони, люди бизнес-правила, SQL-запросы, интеграции, форматы ответа клиенту и даже костылик для бухгалтерии. Если этот код вырвать и вынести в отдельный микросервис checkout, это не улучшит архитектуру. Это будет тот же «комок» логики, но теперь его отладка и тестирование станут сложнее, а изменения — болезненнее, поскольку к старым проблемам добавились сеть, контракты и связанное развёртывание.
Плохая архитектура, вынесенная в микросервисы, остаётся плохой архитектурой. Только теперь она работает через сеть.
Прежде чем приступать к распиливанию монолита на микросервисы, необходимо честно ответить себе на вопросы:
Понимаем ли мы границы области?
Какие части системы действительно могут меняться независимо?
Существуют ли различия в требованиях к нагрузке?
Есть ли разные команды и зоны ответственности?
Требуется ли отдельный жизненный цикл релизов?
Существуют ли данные, которые действительно живут независимо?
Возможно ли сначала привести в порядок сам монолит?
Что можно сделать вместо преждевременных микросервисов
Если монолит начинает вызывать проблемы, первым шагом должен стать не слепой переход на микросервисы, а тщательный рефакторинг монолита. В примере с методом checkout полезнее сначала «разложить всё по полочкам»: отдельно описать правила резервирования, расчёт доставки, применение скидок, интеграции с 1С и контрагентами. Весьма вероятно, что после такого рефакторинга необходимость в отдельном микросервисе отпадёт. Если же он всё-таки понадобится, то это будет уже не случайный запутанный набор логики, а понятный набор бизнес-правил с чёткими границами ответственности.
Поэтому перед выделением микросервисов из монолитного приложения рекомендуется выполнить следующие шаги:
Выделить модули внутри приложения
Устранить циклические зависимости
Отделить бизнес-логику от инфраструктуры
Применить подход DDD (Domain Driven Design) и выделить Bounded Contexts
Описать явные контракты между модулями
Ограничить прямой доступ к данным для чужих модулей
Навести порядок в моделях, сервисах, транзакциях и событиях
Покрыть ключевые сценарии тестами

В целом, все эти советы можно найти в любой популярной книге по Domain Driven Design. Для Python, например, рекомендую почитать «Паттерны разработки на Python» Гарри Персиваля и Боба Грегори. Понимание и применение принципов DDD помогает подготовить код к будущему выделению микросервисов. Такой подход можно назвать «сначала модульный монолит, а потом микросервисы». Возможно, это не модно, но зато честно и практично, ведь архитектурные ошибки могут очень дорого обойтись бизнесу.
Когда микросервисы уместны
Создавать микросервисы стоит только при наличии веских предпосылок и прочного фундамента. Как я уже упоминал, сама по себе сложность и плохая архитектура монолитного приложения не являются предпосылкой к микросервисам — скорее наоборот. Советую разбирать проект на микросервисы, когда:
Разные части продукта развиваются разными командами
Существуют независимые бизнес-домены
Отдельные части приложения требуют разного масштабирования
Разные компоненты имеют различные требования к отказоустойчивости
Необходимы независимые релизы
В компании уже есть зрелая распределённая инфраструктура (Observability, CI/CD, контрактное тестирование, мониторинг, оповещения, трассировка)
Компания осознаёт стоимость распределённых систем и понимает возможные риски
Микросервисы — это не способ исправить плохой код и слабую архитектуру. Это инструмент управления сложностью, когда она объективно существует. Представьте небольшое агентство недвижимости с маленькой CRM для заявок, несколькими риелторами, собственным сайтом с сотней объявлений и интеграциями с Домклик, Авито и ЦИАН. Очевидно, что рано разносить по отдельным сервисам каталог объектов, заявки и показы — команда просто получит больше инфраструктуры, интеграций и мест, где что-то может поломаться. Но ситуация меняется, если речь идёт о крупном застройщике: десятки жилых комплексов, тысячи квартир в продаже, бронирование, динамическое ценообразование, скидки, CRM, личный кабинет покупателя. Здесь сложность исходит из бизнеса, а не из архитекторов. В таком случае отдельные микросервисы бронирования или создания ипотечных заявок оправданы не потому, что «так современнее», а потому, что эти части системы живут по своим правилам, имеют собственную нагрузку, интеграции и зоны ответственности.
Цена микросервисов
Если внимательно присмотреться, то все IT-системы — это компромисс. Выигрывая в одном, мы неизбежно теряем в другом. Так и с микросервисами: при переходе к ним появляются дополнительные издержки:
Сетевые ошибки
Задержки и случайный рост времени отклика
Ретраи и таймауты
Проблемы с идемпотентностью данных
Временное расхождение данных между сервисами
Версионирование API
Миграции из-за изменений контрактов
Распределённая трассировка
Усложнение локальной разработки и отладки
Дублирование данных
Повышенные требования к инфраструктуре и DevOps
Если компания не готова к этим издержкам, микросервисы быстро превращаются не в архитектуру, а в постоянное расследование: где потерялся статус, почему в CRM бронь есть, а в личном кабинете — нет, кто обработал событие и почему в очередь за 10 минут прилетело 1,5 млн сообщений.
Практическая рекомендация
Самый главный вопрос, на который стоит ответить перед выделением куска монолита в микросервис:
Сможет ли этот сервис жить самостоятельно?
Это означает, что сервис должен:
Иметь понятную бизнес-ответственность
Владеть своими данными
Обладать стабильным и ясным контрактом
Быть независимым в релизах
Не нарушать работу всей системы в случае отказа
Быть удобным для разработки и тестирования в изоляции от других
А главное, если команда хорошо понимает, зачем существует этот сервис, тогда «овчинка стоит выделки».
Если хотя бы по половине пунктов ответ отрицательный, скорее всего, речь идёт о вынесенном куске монолита, а не о микросервисе. Архитектурные ошибки проявляются быстрее, чем кажется. Антипаттерн «распределённый монолит» в будущем обойдётся дорого и команде, и бизнесу. Ваши текущие проблемы никуда не денутся, а к ним добавятся новые сложности, присущие распределённым системам.
Заключение
Микросервисная архитектура — отличный инструмент для снижения реальной сложности в крупных проектах. Однако это лишь средство для достижения цели, а не сама цель. Плохая архитектура проекта останется плохой как внутри монолита, так и при наличии множества распределённых сервисов.
Часто ошибки проектирования возникают из-за того, что сложная архитектура на первый взгляд выглядит более солидно. На IT-митапе или в неформальных беседах термины вроде «распределённая трассировка», «производители и потребители сообщений в Kafka», а также «CAP-теорема» вызовут гораздо больше интереса, чем скучный рассказ о рефакторинге. Но для бизнеса и конечного пользователя важны реальные результаты: чтобы квартиры бронировались, мешки штукатурки и цемента доставлялись в срок, а статус не терялся между CRM и личным кабинетом.

Настоящая инженерная зрелость заключается не в том, чтобы усложнить систему до уровня «как в большом IT», а в том, чтобы не добавлять сложности там, где бизнес за неё не платит.
Сталкивались ли вы с распределёнными монолитами? Удавалось ли решать проблемы сложности путём наведения порядка в исходном проекте? Делитесь вашим опытом в комментариях.
kira_degtereva
Часто правда кажется, что разбить на микросервисы это решение всех проблем, а по факту просто переносишь хаос в другое место. Гораздо сложнее, но честнее сначала навести порядок в самом монолите и понять границы ответственности.
malmstine
Еще хуже сразу начинать микросервисы лишь по принципу "чтобы в будущем монолит не переписывать" и с первых шагов создать еще больший хаос при изначально более дорогой разработке