Как работает Service Discovery в Spring Cloud и зачем он нужен
Зачем вообще нужен Service Discovery?
Когда мы только начинаем строить микросервисную архитектуру и запускаем сервисы в одной сети Docker (например, через docker-compose
), может показаться, что все просто: сервисы могут обращаться друг к другу по имени контейнера. Например:
1
2
3
# application.yml
profiles-service:
base-url: http://profiles:8080
Но это работает только на раннем этапе. В реальных условиях, как только проект начинает развиваться, появляются сложности:
- Микросервисы масштабируются — возникает несколько экземпляров одного сервиса;
- Контейнеры могут перезапускаться — IP-адреса становятся непостоянными;
- Нужно обеспечить доступность и отказоустойчивость;
- Возникает необходимость в балансировке нагрузки между экземплярами;
- Требуется обновляемый реестр сервисов, чтобы не искать их вручную.
И что важно - все это касается внутреннего взаимодействия между сервисами, а не внешнего клиентского трафика.
⚠️ Сложность возникает не только в том, чтобы найти нужный сервис, но и чтобы понять, сколько их сейчас в сети, где они, какие из них работают и как правильно распределить запросы между ними. Сервисы в контейнерах работают недолго. Они запускаются, останавливаются, масштабируются. Мир микросервисов требует динамики — а не статической конфигурации.
Почему классическая архитектура обнаружения не помогает
Когда речь заходит о взаимодействии сервисов, может показаться, что достаточно DNS и load balancing. На первый взгляд это кажется логичным: клиенты обращаются по адресу балансировщика, который сам решает, в какую реплику отправить запрос, обращаясь например в таблицу маршрутизации.
Что здесь не так:
- Централизация — единая точка. Традиционный балансировщик — это одна точка, через которую проходит весь трафик. Если он перегружен или упал — взаимодействие рушится.
- Балансировщик не выполняет функции обнаружения. Т.е. не может определить, когда сервис появился, когда исчез, сколько всего их стало. Обычно это решается через ручное добавление IP или интеграцию с health-check системой, что сложно и ненадежно.
- DNS и IP-адреса не отражают актуальное состояние сервисов. Использование DNS-записей и IP-адресов позволяет обратиться к сервису по имени, однако такой подход имеет ряд ограничений. IP-адреса в контейнеризированной среде нестабильны — при перезапуске контейнера его адрес может измениться. DNS не отслеживает текущее состояние сервиса — он разрешает имя в IP, не проверяя, доступен ли по этому адресу экземпляр. Нет встроенной поддержки проверки доступности или отказов — маршрутизация не учитывает, работает ли сервис, перегружен ли он или уже завершил работу.
Эти ограничения особенно критичны в облачной и микросервисной архитектуре, где сервисы часто масштабируются, запускаются и завершаются динамически. В результате DNS-резолвинг без дополнительной логики может привести к попыткам обращения к неработающим или неактуальным экземплярам сервисов.
Именно для этого вводятся три ключевых концепта:
- Service Discovery — автоматический поиск других сервисов по имени;
- Service Registration — регистрация адресов при запуске сервисов;
- Load Balancing — распределение нагрузки между работающими экземплярами.
Как Service Discovery решает эти проблемы в cloud-native архитектуре
Для облачных и микросервисных приложений использование Service Discovery — это не просто удобство, а необходимость, вытекающая из природы самой архитектуры.
В современных окружениях экземпляры сервисов могут запускаться и завершаться в любой момент, их IP-адреса назначаются динамически, количество реплик может меняться из-за автомасштабирования или отказов. Невозможно поддерживать стабильную сетевую схему вручную в таких условиях.
Service Registry как центральный источник правды
Чтобы обеспечить актуальную информацию о состоянии сервисов, используется централизованный реестр — например, Eureka:
- Когда сервис запускается, он регистрируется в реестре.
- Пока онсервис работает, он периодически отправляет heartbeat в реестр — специальный сигнал, подтверждающий, что сервис доступен и готов к обработке запросов. Если heartbeat перестает приходить — например, из-за сбоя или выключения сервиса, — реестр удаляет запись автоматически.
- При завершении — удаляется из реестра автоматически.
- В любой момент клиент может выполнить поиск нужного сервиса по имени и получить список его доступных экземпляров.
Это устраняет необходимость прописывать IP вручную, позволяет работать с любым числом реплик и повышает надежность межсервисных вызовов.
Интеграция с балансировкой нагрузки
Service Discovery может использоваться в связке с балансировщиками нагрузки (например, Spring Cloud LoadBalancer):
- При обращении к сервису, клиент получает список доступных экземпляров от Discovery-сервера;
- Дальше применяется стратегия (например, Round Robin) — на стороне клиента или через прокси;
- Таким образом, достигается распределение нагрузки без участия внешнего балансировщика и без центральной точки отказа.
⚠️ Round Robin — это одна из самых простых стратегий балансировки нагрузки. Суть ее в том, что запросы к сервису равномерно распределяются по всем доступным экземплярам по очереди, без учета их загрузки. Например, если у нас 3 экземпляра
profiles-service
, то запросы будут идти примерно так:instance-1 → instance-2 → instance-3 → instance-1 → ...
Такой подход особенно хорошо работает, если все экземпляры сервиса равны по производительности и нет потребности в адаптивной балансировке.
Два подхода к Service Discovery
Существуют два архитектурных стиля, которые мы можем использовать.
- Client-side discovery. Клиент сам делает запрос в реестр, получает список адресов. Сам выбирает, куда отправить запрос.
- Server-side discovery. Клиент отправляет запрос в балансировщик (например, API Gateway), который выполняет lookup в реестре и перенаправляет запрос. Пример - Kubernetes Service + Envoy.
Client-side Service discovery
Как работает
В паттерне client-side discovery клиент (то есть вызывающий микросервис) сам отвечает за поиск нужного сервиса в реестре, выбор конкретного экземпляра и балансировку нагрузки между доступными адресами. Такой подход активно используется в Spring Cloud с Eureka и OpenFeign.
Принцип работы (по шагам)
- 1) Регистрация Сервис, например profiles-service, при запуске регистрируется в Service Registry и начинает отправлять heartbeat, подтверждая свою доступность.
- 2) Запрос от клиента Когда некий courses-service хочет вызвать profiles-service, он запрашивает адреса у реестра по логическому имени сервиса.
- 3) Получение списка адресов Реестр возвращает список IP-адресов всех доступных экземпляров profiles-service.
- 4) Выбор экземпляра и вызов Клиент использует свою локальную стратегию балансировки (например, round-robin) и направляет запрос на конкретный адрес.
Регистрация сервиса происходит автоматически при старте, если он сконфигурирован как клиент Eureka. Поиск сервисов — это обычный REST-запрос к Discovery Server, выполняемый библиотеками Spring Cloud. Балансировка выполняется на клиентской стороне, что снижает нагрузку на центральный компонент и увеличивает отказоустойчивость.
Кэширование на клиенте
Еще одна важная деталь: чтобы не обращаться к Service Registry при каждом вызове, адреса сервисов кэшируются на стороне клиента. Кэш периодически обновляется из реестра.
Если в кэше уже есть актуальный IP — вызов происходит напрямую. Если нет — идет запрос в реестр. Это повышает производительность и снижает нагрузку на реестр.
Поддержка в Spring Cloud
Spring Cloud предоставляет полноценный набор компонентов, упрощающих реализацию Service Discovery на стороне клиента. Основные библиотеки:
- Eureka (Spring Cloud Netflix Eureka) — централизованный реестр;
- Spring Cloud LoadBalancer — обеспечивает клиентскую балансировку;
- OpenFeign — декларативный HTTP-клиент, автоматически интегрируется с Discovery.
⚠️ Ранее использовался Netflix Ribbon, но он официально переведен в режим поддержки и больше не развивается. Вместо него рекомендован Spring Cloud LoadBalancer.
Как это работает вместе
- 1) Сервис (profiles-service, courses-service) регистрируется в Eureka при старте;
- 2) Другой сервис (например, content-service) использует Feign-клиент с логическим именем (
@FeignClient(name = "profiles-service")
); - 3) При вызове Feign сам запрашивает из Eureka список инстансов;
- 4) Spring Cloud LoadBalancer выбирает один из них (например, через round-robin) и перенаправляет запрос.
Server-side Service discovery
До сих пор мы рассматривали Client-side discovery — подход, при котором клиент сам запрашивает адреса из реестра, сам выбирает экземпляр, сам выполняет балансировку.
Но это не единственный вариант.
В server-side подходе клиент не обращается напрямую к реестру, вместо этого он отправляет запрос на централизованный компонент (например, API Gateway или сервисный прокси), который:
- Выполняет поиск нужного сервиса в реестре;
- Выбирает экземпляр по своей логике;
- Перенаправляет туда запрос.
Сравнение подходов
Характеристика | Client-side Discovery | Server-side Discovery |
---|---|---|
Кто делает lookup? | Клиент | Прокси или gateway |
Где балансировка? | На клиенте | На стороне сервера |
Зависимость от клиента | Высокая (каждый сервис должен | Низкая |
уметь обращаться к реестру) | (клиенты не знают о реестре) | |
Пример | Eureka + Feign | Kubernetes Service + kube-proxy |
Используется где? | Spring Cloud, ECS | Kubernetes, Istio, Consul Connect |
Режим самосохранения в Eureka
Одна из ключевых особенностей Eureka — механизм самосохранения (Self-Preservation). Он защищает систему от ложных срабатываний в нестабильных сетях.
Как это работает:
- Каждый экземпляр сервиса отправляет heartbeat на Eureka каждые ~30 секунд;
- Если heartbeat не поступает, Eureka по умолчанию исключает инстанс из реестра;
- Но если таких инстансов много одновременно, и есть риск обнулить все — Eureka включает режим самосохранения.
В этом режиме реестр замораживается — никто не удаляется, даже если не приходит heartbeat, что позволяет избежать каскадного удаления всех сервисов. Реестр выходит из этого режима вручную или автоматически, когда восстановится стабильная связь.
⚠️ Это важно для продакшн-сред — иначе при сетевом лаге можно потерять все инстансы.
Например, поднято 5 экземпляров profiles-service. Если случился сбой сети, и Eureka временно не видит 3 из 5 — он не будет удалять их из реестра, а продолжит возвращать список всех 5, пока режим самосохранения активен.
Статьи серии
- Микросервисы: серия материалов о принципах, паттернах и практике
- Эволюция архитектур: от монолита к микросервисам
- Микросервисная архитектура
- Нужен ли Service Discovery в Docker-среде?
- Изоляция данных и Feign: архитектура без сквозных связей
- Вызов других микросервисов с помощью Feign
- API Gateway в микросервисной архитектуре
- (в разработке)