От монолита к микросервисам: стратегии декомпозиции и цена распределенности
От монолита к микросервисам: стратегии декомпозиции и цена распределенности
В 2023 году команда Amazon Prime Video опубликовала статью, которая вызвала бурю в инженерном сообществе: они переписали свой сервис мониторинга качества видео из микросервисов обратно в монолит. Это решение сократило их инфраструктурные расходы на 90% и радикально упростило масштабирование. Как же так? Ведь годами индустрия твердила, что микросервисы — это единственный путь к High-Load.
В предыдущем модуле мы довели наш Go-монолит до предела: избавились от избыточных аллокаций, внедрили пулы воркеров и оптимизировали блокировки. Наш бинарник работает молниеносно. Но архитектура — это всегда компромисс. Рано или поздно монолит упирается не в процессор или память, а в людей и организационную структуру.
В этой статье мы разберем, когда действительно пора распиливать монолит, как сделать это правильно и какую цену придется заплатить за распределенность.
Пределы монолита: когда Scale-Out уже не спасает
Мы помним, что монолит легко масштабируется горизонтально (Scale-Out), если он спроектирован как Stateless-приложение. Вы ставите балансировщик нагрузки, запускаете 50 копий одного и того же Go-бинарника и успешно обрабатываете растущий RPS.
Проблемы начинаются не на серверах, а в репозитории:
- Когнитивная перегрузка: Кодовая база разрастается до сотен тысяч строк. Ни один разработчик больше не понимает систему целиком.
- Страх деплоя: Изменение в логике генерации PDF-отчетов может случайно сломать процесс оплаты, потому что они используют общую библиотеку работы с базой данных. Релизы становятся редкими и тяжелыми.
- Конфликт ресурсов: Модуль обработки изображений требует мощных CPU, а модуль кэширования профилей — огромного объема RAM. В монолите мы вынуждены покупать дорогие серверы, где есть и то, и другое, переплачивая за простаивающие ресурсы.
Микросервисы решают эти проблемы, позволяя масштабировать команды разработчиков и подбирать железо под конкретный профиль нагрузки.
Стратегии декомпозиции: как правильно резать?
Самая частая ошибка при переходе к микросервисам — техническая декомпозиция. Архитекторы выделяют «Сервис баз данных», «Сервис бизнес-логики» и «API-сервис». Это приводит к созданию распределенного монолита: чтобы добавить одно поле в профиль пользователя, вам придется править код и деплоить все три сервиса одновременно.
Правильный подход опирается на Domain-Driven Design (DDD) и декомпозицию по бизнес-возможностям.
Bounded Context (Ограниченный контекст)
В DDD есть ключевое понятие — Bounded Context. Это явная граница, внутри которой бизнес-термин имеет одно конкретное значение.
Рассмотрим пример e-commerce платформы. Что такое «Товар»?
- Для отдела Каталога товар — это фотография, описание, SEO-теги и отзывы.
- Для отдела Склада товар — это габариты, вес, номер полки и остаток в штуках.
- Для отдела Биллинга товар — это цена, ставка налога и скидка.
Если мы попытаемся создать единый ProductService с гигантской таблицей в БД, команды будут постоянно блокировать друг друга. Вместо этого мы выделяем три независимых микросервиса (Каталог, Склад, Биллинг), каждый из которых имеет свою базу данных и свое представление о «Товаре», связанное только общим идентификатором (ID).
Цена распределенности: заблуждения сетевых вычислений
Разрезав монолит на независимые сервисы, мы совершаем фундаментальный сдвиг: локальный вызов функции превращается в сетевой запрос.
В монолите вызов billing.Charge(userID, amount) — это инструкция процессора. Она выполняется за наносекунды. Она либо завершается успешно, либо вызывает панику (которую можно перехватить).
В микросервисах тот же вызов превращается в HTTP или gRPC запрос по сети. И здесь вступают в силу классические Fallacies of Distributed Computing (Заблуждения распределенных вычислений), сформулированные Питером Дойчем еще в 1994 году. Вот главные из них:
- «Сеть надежна». Маршрутизаторы падают, кабели повреждаются, балансировщики перегружаются. Запрос может потеряться по пути туда, или, что еще хуже, ответ может потеряться по пути обратно.
- «Задержка сети равна нулю». Вызов функции в памяти занимает ~1 наносекунду. Сетевой вызов внутри одного дата-центра — ~1 миллисекунду. Это в миллион раз медленнее.
- «Пропускная способность бесконечна». Передача огромных JSON-объектов между сервисами быстро забьет сетевые каналы, вызывая деградацию всей инфраструктуры.
Самая опасная проблема распределенных систем — частичный отказ (Partial Failure). Когда вы вызываете функцию в монолите, вы точно знаете результат. Когда вы делаете HTTP-запрос в сервис биллинга и получаете таймаут (ошибка сети), вы находитесь в слепой зоне:
- Запрос не дошел до биллинга? Значит, деньги не списаны.
- Запрос дошел, биллинг списал деньги, но ответ потерялся на обратном пути?
Если в этой ситуации мы просто повторим запрос (Retry), мы можем списать деньги с клиента дважды.
Теорема CAP: фундаментальный компромисс
Поскольку сеть ненадежна (мы доказали это выше), распределенные системы подчиняются жесткому математическому закону — теореме CAP. Она утверждает, что распределенная система может гарантировать не более двух из трех свойств одновременно:
- Consistency (Согласованность): Каждый клиент видит одни и те же данные в любой момент времени.
- Availability (Доступность): Любой запрос получает успешный ответ, даже если часть узлов системы вышла из строя.
- Partition Tolerance (Устойчивость к разделению): Система продолжает работать, даже если сеть между узлами разорвана.
В реальном мире отказы сети неизбежны. Поэтому свойство P (Partition Tolerance) не обсуждается — мы обязаны его поддерживать. Истинный выбор архитектора всегда сводится к компромиссу между C (Согласованностью) и A (Доступностью).
Пример выбора AP (Доступность важнее): Лента социальной сети. Если связь между дата-центрами прервалась, лучше показать пользователю слегка устаревшие посты (пожертвовать согласованностью), чем выдать ошибку 500 (пожертвовать доступностью).
Пример выбора CP (Согласованность важнее): Криптовалютная биржа или банковский перевод. Если сеть между сервисом балансов и сервисом транзакций нестабильна, система обязана отклонить операцию. Лучше отказать в обслуживании (пожертвовать доступностью), чем позволить потратить одни и те же деньги дважды (пожертвовать согласованностью).
Резюме
Переход от монолита к микросервисам — это не способ заставить код работать быстрее. Наоборот, из-за сетевых задержек (RTT) и накладных расходов на сериализацию данных (JSON/Protobuf) обработка одного запроса всегда становится медленнее.
Микросервисы — это инструмент масштабирования организационной структуры и изоляции отказов. Мы сознательно меняем простоту локальных вызовов на сложность сетевого взаимодействия.
В следующих главах мы начнем разбирать базовые паттерны, которые были изобретены индустрией для укрощения этой сетевой сложности: как управлять тысячами точек входа (API Gateway), как сервисам находить друг друга в динамическом облаке (Service Discovery) и как балансировать между ними трафик.