Обещание микросервисной архитектуры звучит убедительно: маленькие приложения, которые пишутся за неделю, выкладываются независимо и масштабируются по отдельности. Реальность появляется позже, в счёте за облако и в ночном звонке о заказе, пропавшем между сервисом корзины и сервисом платежей. Spring Cloud даёт готовые детали для распределённого стека, но не отменяет главного: гибкость развёртывания покупается инфраструктурой, наблюдаемостью и постоянной дисциплиной версий. Ниже сравнение обеих сторон на конкретных механизмах и сценариях, с честным ответом, когда монолит выигрывает.
Стек Spring Cloud вырос из набора библиотек Netflix, и его история поучительна сама по себе. Выключатель Hystrix переведён в режим поддержки в 2018 году, а в релиз-поезде 2020.0 из состава ушли Ribbon, Hystrix и Zuul: балансировку принял Spring Cloud LoadBalancer, функцию выключателя взял Resilience4j, роль шлюза перешла к Spring Cloud Gateway. С выходом Spring Boot 3.0 в конце 2022 года трассировка переехала из Sleuth в Micrometer Tracing. Нумерация релиз-поездов стала календарной, к осени 2026 года параллельно поддерживаются линии 2025.0 и 2025.1, а версия 2026.0 выходит через вехи.
Что Spring Cloud реально кладёт на стол разработчика
Четыре кита закрывают главные задачи связи между сервисами. Реестр Eureka хранит список живых инстансов, клиенты находят друг друга по имени, а не по адресу, и перезапуск узла перестаёт быть событием. Config Server раздаёт конфигурацию из репозитория, и смена порта базы перестаёт быть релизом. Шлюз Gateway принимает весь внешний трафик, маршруты описываются предикатами путей:
spring.cloud.gateway.routes[0].id=orders
spring.cloud.gateway.routes[0].uri=lb://orders
spring.cloud.gateway.routes[0].predicates[0]=Path=/api/orders/**
Декларативные клиенты OpenFeign убирают шаблонный код вызовов:
@FeignClient(name = "orders")
public interface OrderClient {
@GetMapping("/orders/{id}")
Order find(@PathVariable("id") Long id);
}
Отказоустойчивость собирается на Resilience4j с понятными настройками по умолчанию: скользящее окно в сто вызовов, порог отказов в половину измерений, разомкнутый выключатель держится минуту. Аннотация на методе включает защиту и запасной путь:
@CircuitBreaker(name = "orders", fallbackMethod = "cachedOrders")
public Order find(Long id) {
return orderClient.find(id);
}
Для контроля контрактов между сервисами в стеке есть Spring Cloud Contract: поставщик описывает контракт, генерирует из него заглушки, потребитель гоняет свои тесты против заглушек, и несоответствие ловится до релиза, а не на инциденте.
Набор рабочий и зрелый. Он решает задачи обнаружения, конфигурации, маршрутизации и защиты, но не берёт на себя цену эксплуатации: за реестром, сервером конфигурации и шлюзом кто-то должен следить, обновлять и чинить.
Гибкость масштабирования отдельных сервисов и частота их релизов
Выигрыш распределённого стека работает на трёх конкретных сценариях. Первый: масштаб по нагрузке. Монолит масштабируется копиями целиком, и если тяжёл только модуль платежей, вместе с ним плодятся копии всех остальных модулей с их памятью и подключениями к базе. Микросервис платежей масштабируется отдельно одной строкой:
kubectl scale deployment payments --replicas=4
Второй сценарий: частота релизов. Сервис корзины обновляется трижды в неделю, сервис уведомлений раз в месяц, и выкладки не ждут друг друга. В монолите релиз собирается из всего, что готово на дату релиза, и одна затянувшаяся задача держит выкладку остальных изменений.
Третий: изоляция последствий. Отказ уведомлений не роняет корзину, медленный отчёт не занимает поток общего веб-контейнера, зона отказа сжимается до одного сервиса, и метрика доступности считается по частям, а не по целому. Именно эти три эффекта продают микросервисы на презентациях, и все три реальны. Проверка всегда одна: сколько из них нужны конкретному продукту прямо сейчас, а не через два года роста.
Инфраструктура из восьми движущихся частей для пяти сервисов
Команда, написавшая пять сервисов, обнаруживает, что получила тринадцать приложений. К бизнес-коду добавляются реестр, сервер конфигурации, шлюз, брокер сообщений, сборщик трассировки, хранилище метрик, агрегатор журналов и платформа управления контейнерами. Локальная разработка превращается в файл из десятка контейнеров:
services:
orders:
build: orders-service
depends_on: [eureka, config, postgres]
payments:
build: payments-service
depends_on: [eureka, config, kafka, postgres]
gateway:
build: gateway
ports: ["8080:8080"]
eureka:
build: eureka-server
config:
build: config-server
Новому разработчику нужен день, чтобы собрать окружение, и ещё один, чтобы понять, почему у него не стартует шлюз. Каждый сервис тянет собственный конвейер сборки, набор образов, контрактные тесты и отдельный график дежурств. Реестр и сервер конфигурации в продакшене живут не в одном экземпляре, а минимум в паре на зоны доступности, иначе отказ одной нити реестра превращается в отказ всей системы. Обновление версий превращается в проект: согласование релиз-поезда со сборкой Spring Boot и версиями всех клиентов занимает спринты, а на горизонте появляется сервисный меш вроде Istio, и число движущихся частей снова растёт.
Арифметика честная: часть производительности команды уходит на обслуживание разделения ещё до первой пользы от него. Инфраструктурная плата фиксированная, она не зависит от того, насколько удачно проведены границы сервисов.
Сетевые вызовы вместо вызовов методов и их настоящая цена
Внутри монолита обращение к соседнему модулю это вызов метода, микросекунды и один стек. Сетевой вызов между сервисами это сериализация в JSON, заголовки, пул соединений, единицы миллисекунд в лучшем случае и хвост девяносто девятого перцентиля в худшем. На цепочке из пяти сервисов задержки складываются, и бюджет таймаута приходится делить по участкам заранее.
Надёжность требует новой дисциплины. Повтор запроса при таймауте безопасен только для идемпотентных операций, для остального нужны ключи идемпотентности и хранение результатов. Отказ одного сервиса без выключателей раздувает очереди и пулы соседей, каскадный сбой останавливает весь набор. Транзакции распадаются: то, что внутри монолита закрывалось одной операцией фиксации, в распределённом виде собирается из саги с компенсациями и событий, а интерфейс вместо мгновенного ответа переходит на статус "обрабатывается". Согласованность становится итоговой, и часть сценариев переписывается заново только ради этого.
Отдельно платят за изменение границ. Переезд поля из одной подсистемы в другую в монолите это рефакторинг внутри одного конвейера, в распределённом наборе это правки в двух-трёх сервисах, согласование версий контракта и совместные релизы. Число связей заранее оценить сложно, и лучше всех это показывают первые полгода эксплуатации.
Короткая формула сравнения: монолит меняет доступность отдельных частей на простоту целого, микросервисы меняют простоту на независимость частей. Обе сделки честные, выбирать нужно по тому, чего в проекте действительно не хватает.
Наблюдаемость и сквозная трассировка как обязательная плата
Один заказ проходит пять сервисов, и в logs каждого своя правда. Какой из них потерял два процента запросов? Без сквозного идентификатора вопрос остаётся без ответа, а с ним превращается в минуту работы: идентификатор трассировки передаётся заголовком стандарта W3C traceparent, все участники цепочки пишут его в журнал, а отправка трасс в сборщик через Micrometer Tracing по протоколу OTLP складывает картину вызова.
Плата за картину складывается из трёх частей. Бэкенд трассировки требует хранилища и ресурсов: без выборочного семплирования, хранящего единицы процентов трасс, объёмы съедают заметную часть бюджета наблюдаемости. Метрики RED по каждому сервису, готовность, ошибки, длительность, обязаны быть у всех, иначе слепой сервис портит общую картину. Идентификатор трассы обязан попадать в каждую строку журнала: в Spring Boot вопрос закрывается связкой Micrometer Tracing с MDC, после чего идентификатор печатается всеми участниками автоматически. И наконец, команда должна уметь читать трассы, а это навык, который не появляется сам.
В монолите картина дешевле на порядок: один журнал, один стек вызовов, одна транзакция в базе. Разница настолько заметна, что зрелые микросервисные команды вкладывают в наблюдаемость больше, чем в сами сервисы, и это правильная расстановка приоритетов.
Что монолит даёт бесплатно и как ему помогает Spring Modulith
У монолита список бесплатных возможностей длиннее, чем принято признавать. Транзакции через модули закрываются средствами базы. Вызов метода стоит микросекунды и не ломается от падения соседнего узла. Один деплой, один журнал, один профилировщик, один набор тестов окружения. Слабое место ровно одно: без дисциплины модули срастаются в ком, где всё зависит от всего, и тогда любая правка это операция на весь ком.
Инструмент, который держит границы в монолите, называется Spring Modulith. Проект вышел в версии 1.0 в ноябре 2023 года и к осени 2026 года дошёл до линии 2.1 при живой поддержке 1.4. Модуль здесь это пакет с явно заявленными зависимостями, а тест верификации ловит нарушение границ:
class ArchitectureTest {
@Test
void moduleBoundariesAreClean() {
ApplicationModules.of(OrderSystem.class).verify();
}
}
Реестр публикаций событий хранит доставку между модулями и повторяет недошедшее после сбоя, слушатели помечаются аннотацией по модулю, а документация с диаграммами границ генерируется автоматически. По сути, проект приносит в монолит главный микросервисный закон: модули общаются через контракт, а не через внутренности друг друга, и платой за это не становится сетевой вызов.
Критерии готовности к выносу сервиса из монолита
Вынос сервиса оправдан не тогда, когда архитектурный стиль кажется современным, а когда выполнены условия ниже:
- у модуля стабильный контракт с соседями, изменения внутри него не требуют парной правки чужого кода;
- нагрузка модуля растёт быстрее остального монолита и мешает масштабировать приложение целиком;
- отказ модуля не должен ронять остальное приложение, и требование подтверждено реальными инцидентами;
- команде нужна собственная частота релизов, и чужие циклы согласования действительно тормозят работу;
- домен модуля описан и понятен, границы не состоят из сплошных перекрёстных связей через общие таблицы;
- в конвейере уже есть трассировка и контрактные тесты, способные поймать расхождение поведения частей.
Если большинство пунктов не выполнено, монолит с жёсткими границами через Modulith дешевле, безопаснее и быстрее в разработке. Микросервисы это способ разделить работу большой организации, а не способ ускорить код: единый набор сервисов получает выгоду ровно тогда, когда рядом появляются независимые команды с независимыми приоритетами.
Технический маршрут выноса отработан годами. Шлюз начинает маршрутизировать часть путей в новый сервис, старый код продолжает обслуживать остальное, пути переезжают по одному, а контракт версионирован с самого начала. Откат любого шага это переключение маршрута, а не восстановление базы. Таких постепенных миграций в живых системах больше, чем полных переписываний, и это самый честный путь: он не требует ставки "снести и построить заново", только аккуратности на каждом шве.
Считать нужно по собственному проекту. Гибкость выкладки отдельных сервисов существует, она реальна и дорога. Инфраструктура, трассировка и дисциплина версий оплачиваются ежемесячно, командой и деньгами, и не исчезают от выбора модного инструмента. Монолит первого года разработки почти всегда дешевле, быстрее и проще в эксплуатации, а грамотные границы внутри него готовят почву к разделению безболезненно: когда условия из списка выше созреют, вынос пройдёт по готовым швам, а не по живому.