Spring Cloud до сих пор стоит в половине проектов, но состав его изменился радикально: половину задач, ради которых проект создавался, забрала платформа — Kubernetes. Разберём по задачам: что раньше решали библиотеками в приложении, что теперь решает платформа и что из Spring Cloud остаётся живым и нужным.
Откуда взялся Spring Cloud
Когда микросервисы разъезжались по виртуалкам без оркестратора, всю «сетевую обвязку» приходилось делать внутри приложений: находить друг друга (Eureka), хранить общий конфиг (Config Server), балансировать запросы (Ribbon), рвать цепочки отказов (Hystrix), маршрутизировать с края (Zuul). Spring Cloud собрал это в набор стартеров — и для своего времени это был лучший ответ.
Без оркестратора сервис тянет сетевую обвязку внутрь себя: смотрите, сколько библиотек он обязан знать сам.
Kubernetes решает те же задачи, но снаружи приложения: сервис не обязан знать, что его балансируют и перезапускают.
Одна оговорка про версии, чтобы советы ниже можно было приложить к своему проекту. Spring Cloud выпускается «поездами»: имя релиза — это год с порядковым номером (2024.0.x, 2025.0.x), и каждый поезд привязан к своей линейке Spring Boot. Сочетать их вразнобой нельзя — сверяйтесь с таблицей совместимости, а не с последней версией в Maven Central.
Те же задачи в Kubernetes закрыты объектами вокруг пода: смотрите, что теперь стоит снаружи, а не в коде сервиса.
Задача за задачей
Читать её просто: если задача инфраструктурная (кто где живёт, как доставить конфиг, как перезапустить) — её место на платформе. Приложение, тянущее Eureka и Config Server внутри Kubernetes, дублирует платформу и платит за это сложностью и памятью.
| Задача | Spring Cloud (классика) | Kubernetes |
|---|---|---|
| Найти соседний сервис | Eureka + клиент в каждом сервисе | DNS: http://orders — Service сам балансирует |
| Конфигурация | Config Server + Git-репозиторий | ConfigMap / Secret, монтируются в под |
| Балансировка | раньше Ribbon, теперь Spring Cloud LoadBalancer на клиенте | Service / kube-proxy, при нужде — mesh |
| Маршрутизация с края | Zuul / Spring Cloud Gateway | Ingress / Gateway API |
| Перезапуск упавшего | — (руками) | liveness / readiness probes |
| Раскатка без простоя | — | rolling update — но «без простоя» только при корректном завершении |
Три строки таблицы требуют оговорок.
Ribbon умер, клиентская балансировка — нет. Сам Ribbon в архиве с 2020 года, но его место занял Spring Cloud LoadBalancer, и он вполне живой: именно он выбирает экземпляр для запросов OpenFeign и RestClient, помеченных @LoadBalancerClient. Внутри Kubernetes он обычно не нужен — Service справляется сам, — но списывать клиентскую балансировку со счетов вместе с Ribbon неверно.
Rolling update «из коробки» — только наполовину. Kubernetes умеет заменять поды по одному, но живые запросы при этом рвутся, если приложение не умеет корректно завершаться: не дожидается текущих запросов, не снимает себя с балансировки заранее (preStop плюс честный readiness-пробник). Без этого пользователь ловит порцию ошибок при каждой раскатке. Как это настраивается — в разделе про Kubernetes.
Переезд конфигурации в ConfigMap меняет способ обновления. Config Server умел разослать изменение на лету: приложение получало сигнал и перечитывало бины, помеченные @RefreshScope, без перезапуска. Изменение ConfigMap само в бины Spring не приезжает — смонтированный файл через некоторое время обновится, но приложение об этом не узнает, а значения из переменных окружения не обновятся вовсе. Обычный ответ — перезапустить поды с новой версией конфигурации; если перезапуск неприемлем, нужен отдельный механизм, который заметит изменение и дёрнет обновление.
Как сосед находится в Kubernetes на самом деле
В таблице стоит «DNS: http://orders», и за этой строчкой — механизм, который стоит разобрать: именно здесь чаще всего ломается переход с клиентского обнаружения сервисов.
Что такое http://orders. В кластере объект Service получает имя в службе имён, и полное имя выглядит как orders.default.svc.cluster.local; внутри того же пространства имён достаточно orders. Запрос по этому имени приходит не на сервис, а на один из его подов, и выбирает его не служба имён, а сетевой слой узла: на каждом узле правила перенаправляют соединение на один из живых адресов. То есть балансировка происходит на уровне соединения, в ядре, без всякого участия приложения.
Из этого следует главное практическое отличие от клиентского обнаружения: приложение не выбирает экземпляр и не знает, сколько их. Клиентская балансировка выбирала экземпляр на каждый запрос; сетевой слой выбирает под один раз на соединение.
Отсюда главная боль перехода: залипание на одном поде. Современный HTTP-клиент держит постоянные соединения (keep-alive) и переиспользует их. Соединение установилось к поду номер один — и все последующие запросы через это соединение идут в тот же под, часами. Последствия видны сразу:
- Новый под не получает трафика. Масштабировали с двух до шести — нагрузка не перераспределилась, потому что старые соединения живут.
- Нагрузка перекошена. Клиент с пулом из десяти соединений при трёх подах раскладывается как попало, и один под может получить половину запросов.
- С gRPC хуже всего. Там одно долгоживущее соединение обслуживает все вызовы, то есть весь трафик клиента идёт в один под по определению.
Чем лечат, по возрастанию цены:
- Ограничить срок жизни соединения в клиенте: он периодически переоткрывает их, и распределение выравнивается. Настройка на минуты, стоит почти ничего и закрывает большинство случаев.
- Обнаружение поверх адресов подов:
Serviceбез собственного адреса (безголовый) отдаёт в службе имён все адреса подов, и клиентская балансировка снова становится осмысленной — теперь на данных от кластера, а не от Eureka. Тот же приём нужен для gRPC. - Сетевая прослойка (mesh): она балансирует запросы, а не соединения, и проблема исчезает вместе со всеми настройками клиента. Цена — отдельный уровень инфраструктуры.
Второй частый сюрприз: имя есть, а ответа нет. Служба имён отдаёт адрес пода, как только он готов (по проверке готовности), и убирает не мгновенно при остановке. Значит, часть запросов может уйти в под, который уже останавливается, — и это лечится не настройкой имён, а корректной остановкой и повторами на стороне клиента.
И третье, о чём узнают на первой аварии: запросов имён много, и они не бесплатны. Клиент, который на каждый вызов заново разрешает имя, при тысяче запросов в секунду создаёт заметную нагрузку на кластерную службу имён; плюс по умолчанию короткое имя разворачивается в несколько попыток поиска по суффиксам. Практика — полное имя с точкой на конце для частых вызовов и разумное время жизни кеша имён в приложении. Разбор — в статье про сеть в Kubernetes.
Что остаётся приложению
Платформа не видит смысла запросов — поэтому прикладные задачи остались библиотекам:
- Устойчивость на клиенте: circuit breaker, retry с джиттером, таймауты, bulkhead — это Resilience4j (Hystrix давно в архиве). Платформа перезапустит упавший под, но не решит за вас, ждать ли ответа платёжного шлюза 30 секунд.
- Распределённая трассировка: Micrometer Tracing (бывший Sleuth) — контекст запроса живёт в коде.
- API Gateway как приложение: Spring Cloud Gateway жив и уместен, когда на краю нужна прикладная логика — авторизация, лимиты по пользователю, агрегация ответов. С 2024 года он разделён на два серверных варианта — на реактивном WebFlux и на обычном Spring MVC, — и стартеры у них разные; вариант на MVC как раз для тех, кто не хочет тащить Reactor ради краевой маршрутизации. Для чистой маршрутизации хватает Ingress.
- Feign / HTTP-клиенты: декларативные клиенты остаются удобством кода, discovery им больше не нужен — ходят по DNS-имени.
Отдельная ниша — service mesh (Istio, Linkerd): mTLS, канареечные раскатки, сетевые политики без изменения кода. Это следующий уровень платформенного подхода, и он ещё сильнее сужает роль библиотек. Цена у него тоже есть: ещё одна панель управления и её версии, sidecar в каждом поде с памятью и миллисекундой-другой на каждый вызов, и отладка сетевой ошибки, в которой теперь участвует третий компонент. Берут его под конкретную задачу (mTLS между всеми сервисами, канареечные раскатки по проценту трафика), а не «чтобы было».
Прослойка ставит посредника с обеих сторон вызова: смотрите на два лишних участка, которыми оплачены mTLS и повторы.
И цена, без которой этот абзац был бы рекламой:
- Задержка на каждый вызов. Запрос проходит через посредника у отправителя и у получателя: два дополнительных участка, обычно единицы миллисекунд суммарно. На вызове, который сам занимает 3 миллисекунды, это заметная доля; на 200-миллисекундном — шум. То есть цена зависит от того, насколько ваши вызовы короткие.
- Расход ресурсов. Посредник рядом с каждым подом — это его память и процессор, десятки-сотни мегабайт на под. При двухстах подах это отдельная строка в счёте, сравнимая с небольшим сервисом.
- Ещё одна система, которую надо знать. Своя модель настройки, свои правила маршрутизации, своя панель управления, свои версии и обновления. При аварии добавляется вопрос «а не прослойка ли виновата», и ответ на него требует людей, которые её понимают.
- Отладка усложняется. Запрос, который «не дошёл», теперь может не дойти в четырёх местах вместо двух, и половина из них — не ваш код.
Когда она оправдана: десятки сервисов, требование шифрования между сервисами, потребность в тонком управлении трафиком (канареечная раскатка по весам, зеркалирование) и наличие людей, которые будут это обслуживать. Когда нет: пять сервисов, где те же задачи решаются проще — шифрование на входе в кластер, канареечная раскатка средствами выката, сетевые политики самого кластера.
Конфигурация: чем платим за отказ от Config Server
Таблица присуждает победу платформе одной строкой, а у этой победы есть цена, и её надо назвать вслух — иначе нарушается принцип, который декларирует соседняя статья фазы.
Что теряется вместе с Config Server.
- История изменений конфигурации. Config Server хранил конфигурацию в репозитории: кто, когда и что поменял, видно в истории, а откат — это возврат коммита. Объект конфигурации в кластере, отредактированный командой, истории не имеет: последнее значение есть, предыдущего нет, автора нет.
- Ревью правки конфигурации как кода. «Поменять срок ожидания с 2 до 30 секунд» — изменение, которое стоит посмотреть вторым глазом; в репозитории это пул-реквест, в кластере — правка одной командой без следа.
- Обновление без перезапуска. Про это сказано выше:
@RefreshScopeпереставал работать не полностью, но механизм доставки изменения исчезает. - Один источник для всех окружений с наследованием общих значений: в Config Server общее лежало в одном файле, а различия — в профилях; в кластере это превращается в несколько объектов, которые легко разъезжаются.
Чем это возвращают — и это правильный ответ, а не Config Server. Конфигурация кластера хранится в репозитории как код, а в кластер попадает только автоматикой: объекты конфигурации описаны файлами, лежат рядом с манифестами, правятся пул-реквестом, применяются конвейером или оператором согласования. Тогда история, ревью и откат возвращаются — только на уровне инфраструктуры, а не приложения. Разбор — в статье про выкат и конфигурацию в Kubernetes.
Правило, которое из этого следует: правка конфигурации командой в кластере — такое же нарушение, как правка кода на сервере. Если она возможна, вы потеряли историю независимо от выбранного инструмента.
Что такое «Spring Cloud» на самом деле
Слово используется как название одной библиотеки, а это зонтик над десятком независимых проектов, у которых разная судьба. Читателю полезно знать состав, чтобы не отвергать вместе с Eureka то, что живёт своей жизнью.
Живое и полезное вне зависимости от Kubernetes:
- Spring Cloud Gateway — шлюз как приложение, когда на краю нужна прикладная логика.
- OpenFeign — декларативные HTTP-клиенты. Формально это самостоятельный проект (Spring Cloud даёт интеграцию), и с ним стоит знать альтернативу: в современном Spring есть свои декларативные клиенты на основе интерфейсов, и для нового кода часто достаточно их.
- Spring Cloud Stream — абстракция над брокером: код обработчика пишется один раз, а привязка к Kafka или RabbitMQ задаётся настройками. Полезно, когда брокеров несколько или их может заменить; цена — ещё один слой абстракции над и без того непростой библиотекой брокера.
- Spring Cloud Contract — контрактные тесты со стороны поставщика: из описания контракта генерируются и тесты поставщика, и заглушки для потребителей. Это не про инфраструктуру и платформой не заменяется. Разбор — в статье про моки и внешние системы.
- Spring Cloud Config — тот самый сервер конфигурации: вне Kubernetes всё ещё разумен.
Ушло в архив и заменено: Hystrix (заменён Resilience4j), Ribbon (заменён Spring Cloud LoadBalancer), Zuul (заменён Gateway), Sleuth (стал частью Micrometer Tracing — то есть переехал из Spring Cloud в общую библиотеку метрик и трассировки). Eureka и Config Server живы, но внутри Kubernetes избыточны.
Практический вывод: «не берём Spring Cloud» — неверная формулировка. Верная звучит так: не берём из него то, что делает платформа (обнаружение сервисов, конфигурацию, балансировку, краевую маршрутизацию без прикладной логики), и берём то, что платформа не делает (контрактные тесты, шлюз с логикой, абстракция над брокером, декларативные клиенты).
Два источника адресов в одном кластере: смотрите на верхнюю строку, где запись Eureka ещё жива, а пода уже нет.
Как выбирать
- Вы в Kubernetes (типовой случай): discovery, конфиг, балансировка, маршрутизация — у платформы. Из Spring Cloud берёте точечно: Resilience4j-интеграцию, Gateway при нужде в прикладном крае. Eureka, Config Server, Ribbon, Hystrix, Zuul — не начинать.
- Вы без оркестратора (виртуалки, свой хостинг): классический Spring Cloud всё ещё решает реальные проблемы — но честнее спросить, почему без оркестратора.
- Наследованный проект с полным стеком Netflix: работает — не трогайте ради моды; мигрируйте задачами вместе с переездом в Kubernetes.
Глубже: секреты: Secret — это не хранилище секретоврасширенное
Одна ячейка таблицы объединяет ConfigMap и Secret, и это объединение скрывает важное: Secret по умолчанию не шифрует, а лишь кодирует значение. Из этого следуют три вещи, которые надо знать до того, как положить туда пароль базы:
- В репозиторий его класть нельзя. Закодированное значение читается одной командой, то есть секрет в репозитории — это секрет в открытом виде. Если конфигурация лежит как код (а она должна), секреты в неё попадают либо зашифрованными (инструменты шифрования файлов с ключом, который не в репозитории), либо не попадают вовсе — только ссылка на внешнее хранилище.
- Хранилище кластера по умолчанию не шифрует. Шифрование значений в хранилище состояния кластера включается отдельно, и без него секреты лежат открыто там же, где остальные объекты.
- Прав на чтение секретов больше, чем кажется. Кто может создать под в пространстве имён, может смонтировать любой секрет из него. Значит, изоляция — это пространства имён и права, а не сам факт использования
Secret.
Что делают в проектах, где секреты важны: держат их во внешнем хранилище (Vault или менеджер секретов облака), а в кластер они попадают одним из трёх способов — подтягиваются оператором в обычный Secret, монтируются в под через специальный драйвер, или приложение читает хранилище само по своей учётной записи. У первых двух приложение остаётся простым (читает файл или переменную), у третьего появляется зависимость от хранилища в момент старта.
И отдельная практика, которую стоит взять независимо от выбора: ротация. Секрет, который никогда не меняли, — это секрет, срок жизни которого равен сроку жизни проекта. Способ смены (и то, переживёт ли приложение смену пароля без перезапуска) — часть решения, а не «потом разберёмся».
Глубже: штатный мост: Spring Cloud Kubernetesрасширенное
Выбор между двумя мирами не двоичный — есть проект, который их сшивает. Spring Cloud Kubernetes читает ConfigMap и Secret как обычные источники конфигурации Spring (Config Server при этом не нужен), даёт DiscoveryClient поверх API самого Kubernetes — то есть привычный код обнаружения сервисов продолжает работать, а отдельный реестр вроде Eureka не заводится, — и умеет заметить изменение ConfigMap и дёрнуть обновление @RefreshScope-бинов.
Когда он уместен: приложение уже написано под абстракции Spring Cloud и переезжает в Kubernetes — мост позволяет не переписывать код разом. Когда лишний: новый сервис, который и так ходит по DNS-имени и читает конфигурацию из смонтированного файла или переменных окружения. Прослойка тогда ничего не добавляет, но требует прав на чтение объектов Kubernetes из-под приложения — а это лишняя дырка в правах доступа.
Коротко
- Spring Cloud возник до эпохи оркестраторов и решал инфраструктурные задачи внутри приложения; Kubernetes решает их снаружи.
- Забрала платформа: discovery (DNS), конфигурацию (ConfigMap/Secret), балансировку, маршрутизацию с края, перезапуски и раскатки — но раскатка без простоя требует корректного завершения со стороны приложения.
- Осталось приложению: circuit breaker и retry (Resilience4j), трассировка, Gateway с прикладной логикой, декларативные HTTP-клиенты.
- Eureka, Config Server, Ribbon, Hystrix, Zuul в новом проекте на Kubernetes — признак копирования старых рецептов; на замену Ribbon есть живой Spring Cloud LoadBalancer, а между мирами — Spring Cloud Kubernetes.
- Service mesh двигает границу ещё дальше в сторону платформы — следите за этой границей, а не за модой.
- Имя сервиса разрешается в адрес пода, а балансировка идёт на уровне соединения: постоянные соединения залипают на одном поде, новый под трафика не получает; лечится сроком жизни соединения, безголовым сервисом с адресами подов или сетевой прослойкой.
- Отказ от сервера конфигурации стоит истории правок, ревью и обновления без перезапуска; возвращают это тем, что конфигурация кластера лежит как код и применяется автоматикой, а не правится командой.
Secretне шифрует, а кодирует: в репозиторий он попадает только зашифрованным или ссылкой на внешнее хранилище, шифрование в хранилище кластера включают отдельно, а ротацию продумывают заранее.- Сетевая прослойка стоит миллисекунд на вызов, памяти на каждый под, ещё одной системы в эксплуатации и усложнённой отладки; оправдана на десятках сервисов с требованием шифрования и людьми на обслуживание.
- «Spring Cloud» — зонтик: Gateway, контрактные тесты, абстракция над брокером и декларативные клиенты живут и вне Kubernetes; Hystrix, Ribbon, Zuul и Sleuth заменены, Eureka и сервер конфигурации внутри кластера избыточны.
Что почитать дальше
- Kubernetes и graceful shutdown — как платформа управляет жизнью пода.
- Надёжность сети: таймауты и повторы — прикладная сторона, которая осталась вам.
- Монолит или микросервисы — нужен ли вообще этот разговор вашему проекту.