В разработке корпоративных приложений всё больше команд обращают внимание на микросервисную архитектуру. Но с этой архитектурой приходят и специфичные трудности: как сервисам находить друг друга в распределённой среде? Как отслеживать путь запроса через десятки взаимодействующих сервисов? Spring Cloud расширяет возможности Spring Boot и упрощает создание распределённых систем. Особенно важны его компоненты для обнаружения сервисов и распределённой трассировки.
Обнаружение сервисов — важный механизм, позволяющий микросервисам динамически находить и взаимодействовать друг с другом без жёсткого закрепления IP-адресов или имён хостов. В мире, где контейнеры появляются и исчезают, а облачные инстансы автоматически масштабируются, статическая конфигурация быстро становится неактуальной. Сервис-регистры, такие как Eureka или Consul, решают эту проблему, выступая посредниками между сервисами. Когда количество микросервисов растёт, а запросы проходят через множество компонентов системы, возникает вопрос: как отследить, что происходит с запросом? Где именно возникла задержка или ошибка? Spring Cloud Sleuth и Zipkin позволяют маркировать запросы уникальными идентификаторами и визуализировать их путь через различные сервисы, делая отладку и мониторинг намного проще.
Исследования показывают, что компании, внедрившие микросервисную архитектуру с правильными инструментами обнаружения и трассировки, смогли значительно сократить время локализации и устранения проблем. Например, команда Netflix, стоявшая у истоков многих компонентов Spring Cloud, сообщала о сокращении времени отладки на 70% после внедрения распределённой трассировки.
Основы обнаружения сервисов
Обнаружение сервисов — фундаментальный компонент любой микросервисной архитектуры. В традиционных монолитных системах мы знаем точные адреса всех компонентов заранее. Но в мире микросервисов, где десятки или сотни сервисов могут работать на разных серверах, в разных ЦОДах или даже в разных облачных провайдерах, этот подход перестаёт работать. Особенно когда добавляешь в уравнение контейнеризацию, автомасштабирование и отказоустойчивость.
Работа Eureka Server
Netflix Eureka — один из наиболее популярных инструментов для обнаружения сервисов в Spring Cloud. По сути, Eureka Server — это реестр сервисов на базе REST, который отслеживает местоположение всех сервисов в вашей экосистеме.
Принцип работы Eureka достаточно прямолинеен:1. Сервис регистрируется в Eureka, сообщая информацию о себе (имя, IP-адрес, порт).
2. Eureka сохраняет эту информацию в реестре.
3. Клиенты запрашивают у Eureka информацию о нужном сервисе.
4. Eureka возвращает список доступных экземпляров сервиса.
5. Клиент выбирает один из экземпляров и делает запрос напрямую. Что особенно удобно — Eureka предоставляет REST API, что делает его универсальным. Технологии на стороне клиента или сервиса могут быть самыми разными, главное — умение работать с REST. Настроить Eureka Server в Spring Boot проекте на удивление просто:
Java | 1
2
3
4
5
6
7
| @SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
} |
|
Всего пара аннотаций — и бэкенд-разработчику уже доступны все возможности сервера обнаружения. В файле конфигурации application.yml нужно указать несколько базовых настроек:
YAML | 1
2
3
4
5
6
7
8
| server:
port: 8761
eureka:
client:
register-with-eureka: false
fetch-registry: false
server:
enable-self-preservation: true |
|
Параметры register-with-eureka и fetch-registry отключены, поскольку сам Eureka Server не должен регистрироваться в реестре — он и есть реестр. Cамосохранение (enable-self-preservation ) — это механизм защиты от массовых сетевых сбоев, который мы рассмотрим подробнее.
Настройка и запуск регистра сервисов
Когда Eureka Server запущен, его веб-интерфейс доступен по адресу http://localhost:8761 . На этой странице можно увидеть все зарегистрированные сервисы, их статус и различную диагностическую информацию.
Для корпоративных сред одного экземпляра Eureka недостаточно — он становится единой точкой отказа. Обычно запускают как минимум два экземпляра, которые синхронизируются между собой. Такая конфигурация требует чуть больше настроек:
YAML | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # Для первого экземпляра
eureka:
client:
serviceUrl:
defaultZone: [url]http://eureka-peer:8762/eureka/[/url]
instance:
hostname: eureka-server
# Для второго экземпляра
eureka:
client:
serviceUrl:
defaultZone: [url]http://eureka-server:8761/eureka/[/url]
instance:
hostname: eureka-peer |
|
Так мы создаём кластер Eureka серверов, где каждый экземпляр знает о другом. При регистрации сервиса в одном из них информация автоматически реплицируется на другой. Если один экземпляр выйдет из строя, система продолжит функционировать. А что если клиент не может связаться с реестром? Spring Cloud предусматривает локальное кэширование данных о сервисах. Клиентский экземпляр регулярно запрашивает обновлённую информацию у Eureka Server и хранит её локально. Если сервер недоступен, клиент использует закэшированные данные.
Стратегии отказоустойчивости регистра сервисов
Одной из интересных особенностей Eureka является механизм самосохранения (self-preservation mode). Если количество сервисов, которые перестали отправлять сигналы активности (heartbeats), превышает определённый порог, Eureka переходит в режим самосохранения. В этом режиме он перестаёт удалять "пропавшие" сервисы из реестра, предполагая, что проблема в сети, а не в самих сервисах. Это важный механизм, который предотвращает каскадные отказы. Например, если есть временная проблема с сетью, из-за которой Eureka не получает сигналы от сервисов, удаление их из реестра только усугубит ситуацию. Вместо этого Eureka сохраняет последнее известное состояние и ждёт восстановления связи. Для настройки параметров самосохранения можно использовать следующие свойства:
YAML | 1
2
3
4
5
| eureka:
server:
renewal-percent-threshold: 0.85
renewal-threshold-update-interval-ms: 900000
enable-self-preservation: true |
|
Порог обновлений (renewal-percent-threshold ) — это процент ожидаемых heartbeats, который должен поддерживаться. Если процент падает ниже этого значения, включается режим самосохранения.
В нашем проекте мы столкнулись с интересной ситуацией: при развёртывании в нескольких зонах AWS были случаи, когда зональные проблемы приводили к тому, что Eureka в одной зоне переходил в режим самосохранения и перестал корректно отражать состояние сервисов. Решением стала тонкая настройка параметров и внедрение дополнительных проверок здоровья (health checks).
Другая важная стратегия — это настройка таймаутов. По умолчанию Eureka считает, что сервис "умер", если не получает heartbeat в течение 90 секунд. В некоторых случаях это слишком долго, в других — слишком быстро. Настройка этих параметров критична для правильной работы системы:
YAML | 1
2
3
4
| eureka:
instance:
lease-renewal-interval-in-seconds: 30
lease-expiration-duration-in-seconds: 90 |
|
Здесь lease-renewal-interval-in-seconds определяет, как часто клиент отправляет heartbeat, а lease-expiration-duration-in-seconds — через какое время отсутствия heartbeat сервис считается недоступным. Для особо требовательных к доступности систем Eureka можно интегрировать с AWS Route 53 или другими службами DNS для еще большей отказоустойчивости. В таком случае клиенты могут использовать DNS для поиска активного экземпляра Eureka.
Интеграция Eureka с другими компонентами Spring Cloud
Сила Spring Cloud заключается не только в отдельных компонентах, но и в их слаженном взаимодействии. Eureka естественным образом интегрируется с другими инструментами экосистемы. Например, Spring Cloud LoadBalancer — клиентская библиотека для балансировки нагрузки — автоматически использует информацию из Eureka для распределения запросов между экземплярами сервисов. Она применяет различные алгоритмы балансировки, такие как Round Robin (по умолчанию) или Weighted Response Time.
Java | 1
2
3
4
5
6
7
8
| @Configuration
public class LoadBalancerConfig {
@Bean
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
DiscoveryClient discoveryClient) {
return new DiscoveryClientServiceInstanceListSupplier(discoveryClient);
}
} |
|
Эта конфигурация позволяет Spring Cloud LoadBalancer получать список доступных экземпляров из Eureka и выбирать между ними при завтрашнем запросе.
Spring Cloud Gateway, современный API шлюз, также тесно интегрирован с Eureka. Он может динамически создавать маршруты на основе зарегистрированных сервисов:
YAML | 1
2
3
4
5
6
7
| spring:
cloud:
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true |
|
С такой конфигурацией Gateway автоматически создаёт маршруты для всех сервисов в Eureka, позволяя обращаться к ним через URL вида /service-name/** . Интересно и то, как Eureka взаимодействует с Circuit Breaker паттерном, реализованным в Resilience4j (ранее использовался Hystrix). Когда сервис недоступен, Circuit Breaker может временно отключать запросы к нему, предотвращая каскадные отказы. Информация из Eureka помогает быстро переключиться на доступный экземпляр.
Из личного опыта: в одном из проектов мы столкнулись с проблемой, когда сервис был зарегистрирован в Eureka, но из-за проблем с JVM не мог обрабатывать запросы. Circuit Breaker перенаправлял запросы на другие экземпляры, но Eureka считал проблемный сервис доступным. Решением стало внедрение более глубоких проверок здоровья через Spring Boot Actuator:
YAML | 1
2
3
4
| eureka:
instance:
health-check-url-path: /actuator/health
status-page-url-path: /actuator/info |
|
Это позволило Eureka получать более точную информацию о реальном состоянии сервиса, а не просто о его сетевой доступности.
Наконец, стоит отметить альтернативные решения. Хотя Eureka прекрасно работает в большинстве сценариев, в некоторых случаях лучше подходит Consul. Он предлагает не только обнаружение сервисов, но и хранение конфигураций, более продвинутые проверки здоровья и поддержку нескольких центров обработки данных из коробки. Spring Cloud обеспечивает примерно одинаковый API для обоих решений, что упрощает переход при необходимости.
Микросервисы на Spring Boot Всем привет. Написал два микросервиса на Spring Boot, засекьюрил один обычным вводом пароля и мыла. Но проблема теперь в доступе с одного... Spring Cloud Stream неясно Добрый день.
Есть учебный проект.У нас есть множество датчиков ,которые являются output их нужно завязать на сервер-контроллер,который связан с БД... Spring cloud gateway & websockets Привет.
Делаю небольшую задачку, в которой нужно из сервиса1 передать информацию в сервис2 с помощью websocket'a.
Работает все через шлюз, и в... spring cloud feign add header Есть feignClientы, надо к каждому запросу добавлять header.
Аннотация @Headers по какой-то причине полностью игнорируется.
Interceptor скорее...
Интеграция сервисов с Eureka
Теперь, когда мы разобрались с Eureka Server, давайте рассмотрим, как интегрировать с ним микросервисы. На практике именно клиентская сторона вызывает больше вопросов у разработчиков, особенно когда дело доходит до тонкой настройки, балансировки нагрузки и обработки отказов.
Регистрация клиентских приложений
Чтобы сервис мог зарегистрироваться в Eureka, ему необходимо иметь зависимость spring-cloud-starter-netflix-eureka-client и соответствующую конфигурацию. Базовая настройка выглядит следующим образом:
Java | 1
2
3
4
5
6
7
| @SpringBootApplication
@EnableDiscoveryClient
public class MyServiceApplication {
public static void main(String[] args) {
SpringApplication.run(MyServiceApplication.class, args);
}
} |
|
В конфигурационных файлах нужно указать местоположение Eureka Server и имя сервиса:
YAML | 1
2
3
4
5
6
7
8
9
10
| spring:
application:
name: payment-service
eureka:
client:
service-url:
defaultZone: [url]http://localhost:8761/eureka/[/url]
instance:
prefer-ip-address: true |
|
Параметр prefer-ip-address указывает, что сервис должен регистрироваться с IP-адресом, а не с hostname. Это особенно полезно в контейнеризированных средах, где имена хостов могут быть динамическими и непредсказуемыми.
При запуске сервис автоматически регистрируется в Eureka и начинает отправлять heartbeats. В консоли можно увидеть что-то вроде:
Java | 1
2
| INFO 14828 --- [main] c.n.d.s.r.DiscoveryClient: Initializing Eureka in region us-east-1
INFO 14828 --- [main] c.n.d.s.r.DiscoveryClient: Registering service instance payment-service/192.168.1.5:payment-service:8080 with eureka |
|
Если сервис останавливается корректно, он сообщает Eureka о своём выключении. Однако не всегда сервисы завершаются штатно – сбой, аварийное отключение или проблемы с сетью могут помешать этому. Поэтому Eureka полагается не только на явное уведомление о выключении, но и на механизм heartbeat.
Взаимодействие между сервисами
После того как сервисы зарегистрированы, они могут взаимодействовать друг с другом, используя имена сервисов вместо конкретных адресов. В Spring Cloud есть несколько способов организовать такое взаимодействие:
1. REST-клиент с балансировкой нагрузки
Самый простой способ – использовать RestTemplate с LoadBalancer:
Java | 1
2
3
4
5
6
7
8
| @Configuration
public class RestTemplateConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
} |
|
Аннотация @LoadBalanced указывает Spring, что этот RestTemplate должен использовать механизм балансировки нагрузки. С такой конфигурацией можно делать запросы, указывая имя сервиса вместо хоста:
Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| @Service
public class OrderService {
private final RestTemplate restTemplate;
public OrderService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public PaymentResponse processPayment(Long orderId, BigDecimal amount) {
PaymentRequest request = new PaymentRequest(orderId, amount);
// payment-service - это имя сервиса в Eureka
return restTemplate.postForObject("http://payment-service/api/payments", request, PaymentResponse.class);
}
} |
|
За кулисами Spring Cloud LoadBalancer интерпретирует "payment-service" как имя сервиса, запрашивает у Eureka список доступных экземпляров и выбирает один из них, применяя алгоритм балансировки.
2. Использование WebClient (для реактивных приложений)
Если вы работаете с реактивным стеком Spring WebFlux, предпочтительнее использовать WebClient вместо RestTemplate:
Java | 1
2
3
4
5
6
7
8
| @Configuration
public class WebClientConfig {
@Bean
@LoadBalanced
public WebClient.Builder webClientBuilder() {
return WebClient.builder();
}
} |
|
И использование соответственно:
Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| @Service
public class ReactiveOrderService {
private final WebClient.Builder webClientBuilder;
public ReactiveOrderService(WebClient.Builder webClientBuilder) {
this.webClientBuilder = webClientBuilder;
}
public Mono<PaymentResponse> processPayment(Long orderId, BigDecimal amount) {
PaymentRequest request = new PaymentRequest(orderId, amount);
return webClientBuilder.build()
.post()
.uri("http://payment-service/api/payments")
.bodyValue(request)
.retrieve()
.bodyToMono(PaymentResponse.class);
}
} |
|
3. Декларативные клиенты с OpenFeign
OpenFeign позволяет создавать декларативные клиенты для REST API. Это значительно упрощает написание кода для взаимодействия между сервисами:
Java | 1
2
3
4
5
6
7
| @SpringBootApplication
@EnableFeignClients
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
} |
|
Затем определяется интерфейс клиента:
Java | 1
2
3
4
5
| @FeignClient(name = "payment-service")
public interface PaymentClient {
@PostMapping("/api/payments")
PaymentResponse processPayment(@RequestBody PaymentRequest request);
} |
|
И используется как обычный Spring компонент:
Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
| @Service
public class OrderService {
private final PaymentClient paymentClient;
public OrderService(PaymentClient paymentClient) {
this.paymentClient = paymentClient;
}
public PaymentResponse processPayment(Long orderId, BigDecimal amount) {
PaymentRequest request = new PaymentRequest(orderId, amount);
return paymentClient.processPayment(request);
}
} |
|
Feign автоматически интегрируется с Eureka и LoadBalancer, что делает его очень удобным инструментом для микросервисной архитектуры.
Примеры конфигурации и кода
Рассмотрим более сложный пример, который включает обработку ошибок и сетевых таймаутов:
Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| @FeignClient(
name = "payment-service",
fallback = PaymentClientFallback.class,
configuration = PaymentClientConfig.class
)
public interface PaymentClient {
@PostMapping("/api/payments")
PaymentResponse processPayment(@RequestBody PaymentRequest request);
}
@Component
class PaymentClientFallback implements PaymentClient {
@Override
public PaymentResponse processPayment(PaymentRequest request) {
// Возвращает результат по умолчанию или выбрасывает ошибку
return new PaymentResponse(request.getOrderId(), PaymentStatus.FAILED, "Service unavailable");
}
}
@Configuration
class PaymentClientConfig {
@Bean
public Request.Options options() {
// Таймауты для подключения и чтения в миллисекундах
return new Request.Options(2000, 5000);
}
} |
|
Этот код демонстрирует несколько важных концепций:- Определение fallback для случаев, когда сервис недоступен.
- Настройка таймаутов для клиента.
- Обогащение ответа контекстной информацией для облегчения отладки.
В практике разработки микросервисов очень важно правильно настроить таймауты и механизмы повторных попыток. Если таймауты слишком короткие, система будет считать сервисы недоступными при кратковременных задержках. Если слишком длинные – пользователи будут ждать ответа слишком долго.
Балансировка нагрузки между экземплярами сервисов
В реальных микросервисных архитектурах часто запускают несколько экземпляров одного и того же сервиса для обеспечения высокой доступности и распределения нагрузки. Spring Cloud LoadBalancer предоставляет несколько стратегий балансировки:
Java | 1
2
3
4
5
6
7
8
9
10
11
| @Configuration
public class CustomLoadBalancerConfig {
@Bean
ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment,
LoadBalancerClientFactory clientFactory) {
String serviceName = environment.getProperty("loadbalanced.service.name");
return new RandomLoadBalancer(clientFactory
.getLazyProvider(serviceName, ServiceInstanceListSupplier.class),
serviceName);
}
} |
|
Этот код настраивает случайную балансировку вместо Round Robin, используемой по умолчанию. Для конкретных сервисов можно использовать разные алгоритмы:
Java | 1
2
3
4
| @Configuration
@LoadBalancerClient(name = "payment-service", configuration = CustomLoadBalancerConfig.class)
public class LoadBalancerConfiguration {
} |
|
Если нужна более сложная логика балансировки, можно реализовать собственную стратегию. Например, мы однажды столкнулись с ситуацией, когда некоторые запросы должны были направляться на определённые экземпляры сервиса в зависимости от параметров запроса:
Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| public class HeaderBasedLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
private final String serviceId;
public HeaderBasedLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
String serviceId) {
this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
this.serviceId = serviceId;
}
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
// Извлекаем заголовок из контекста запроса
String targetDataCenter = ServerWebExchangeUtils.getContext(request).map(ctx ->
ctx.getExchange().getRequest().getHeaders().getFirst("X-Target-DataCenter")).orElse(null);
ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider.getIfAvailable();
return supplier.get().next().map(instances -> {
// Фильтруем экземпляры по метаданным
List<ServiceInstance> filteredInstances = instances.stream()
.filter(instance -> targetDataCenter == null ||
targetDataCenter.equals(instance.getMetadata().get("datacenter")))
.collect(Collectors.toList());
// Если нет подходящих, используем все доступные
if (filteredInstances.isEmpty()) {
filteredInstances = instances;
}
// Выбираем случайный экземпляр из отфильтрованных
ServiceInstance instance = filteredInstances.isEmpty() ? null :
filteredInstances.get(new Random().nextInt(filteredInstances.size()));
return new DefaultResponse(instance);
});
}
} |
|
Отключение и обнаружение новых сервисов в реальном времени
Одно из ключевых преимуществ Eureka – поддержка динамических изменений в экосистеме микросервисов. Когда новый экземпляр сервиса запускается и регистрируется, клиенты автоматически начинают включать его в балансировку. Аналогично, когда сервис выключается или становится недоступным, он исключается из списка кандидатов. Информация о доступных сервисах кэшируется на стороне клиентов, и они периодически обновляют свой кэш. По умолчанию это происходит каждые 30 секунд, но интервал можно настроить:
YAML | 1
2
3
| eureka:
client:
registry-fetch-interval-seconds: 5 |
|
Более частое обновление реестра повышает реактивность системы на изменения, но увеличивает нагрузку на сеть и сервер Eureka. В крупных системах с сотнями сервисов этот параметр нужно настраивать с осторожностью.
Распределенная трассировка
В микросервисной архитектуре один запрос пользователя может пройти через десятки сервисов, прежде чем сформируется полный ответ. При возникновении проблемы простой анализ логов каждого сервиса превращается в настоящую головоломку. Откуда начать? Как понять, какой сервис вызвал задержку или ошибку? Вот здесь и приходит на помощь распределённая трассировка.
Технология Zipkin для мониторинга
Zipkin — это система распределённой трассировки с открытым исходным кодом, созданная Twitter и вдохновлённая статьёй Google "Dapper, a Large-Scale Distributed Systems Tracing Infrastructure". В экосистеме Spring Cloud Zipkin служит центром сбора и визуализации данных о трассировке. Архитектура Zipkin включает несколько ключевых компонентов:1. Collector — получает данные трассировки от различных сервисов.
2. Storage — хранит данные (поддерживает in-memory, MySQL, Cassandra и Elasticsearch).
3. API — предоставляет доступ к собранным данным.
4. Web UI — визуализирует трассы для удобного анализа. Запустить Zipkin сервер можно несколькими способами. Самый простой — использовать Docker:
Bash | 1
| docker run -d -p 9411:9411 openzipkin/zipkin |
|
Для более сложных сценариев, особенно в продакшн-среде, Zipkin обычно настраивают для хранения данных во внешней базе данных и интегрируют с системой метрик:
YAML | 1
2
3
4
5
6
7
8
| zipkin:
storage:
type: elasticsearch
elasticsearch:
hosts: [url]http://elasticsearch:9200[/url]
index: zipkin
index-shards: 5
index-replicas: 1 |
|
Веб-интерфейс Zipkin доступен по адресу http://localhost:9411 и позволяет искать трассы по различным критериям: сервисам, времени выполнения, тегам и т.д. Это невероятно удобно для отладки проблем производительности.
Spring Cloud Sleuth
Spring Cloud Sleuth — это библиотека, которая добавляет контекст трассировки в логи и запросы микросервисов Spring. Она автоматически инструментирует связь через протоколы вроде HTTP и сообщения через брокеры, такие как RabbitMQ или Kafka.
Для начала работы с Sleuth нужно добавить зависимость в проект:
XML | 1
2
3
4
| <dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency> |
|
Для отправки данных в Zipkin добавляется еще одна зависимость:
XML | 1
2
3
4
| <dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency> |
|
После добавления этих зависимостей Sleuth автоматически инструментирует входящие HTTP запросы, исходящие REST вызовы через RestTemplate или WebClient, и даже асинхронные операции с использованием ExecutorService или @Async. В файле конфигурации необходимо указать адрес сервера Zipkin:
YAML | 1
2
3
4
5
6
| spring:
zipkin:
base-url: [url]http://localhost:9411[/url]
sleuth:
sampler:
probability: 1.0 |
|
Параметр probability определяет процент запросов, которые будут трассироваться. Значение 1.0 означает, что будут трассироваться все запросы, что подходит для разработки и тестирования, но может создать чрезмерную нагрузку в производственной среде с высоким трафиком. Для продакшна часто используют значения вроде 0.1 (10% запросов) или настраивают более сложные правила сэмплирования. После настройки Sleuth логи приложения обогащаются информацией о трассировке:
Java | 1
| 2023-08-15 14:23:07.782 INFO [order-service,5fe3d8f9c83da3ee,cbe8ef14e68a8304] 12232 --- [nio-8080-exec-3] c.e.o.controller.OrderController : Processing order for customer: 42 |
|
Здесь order-service — имя сервиса, 5fe3d8f9c83da3ee — ID трассировки (общий для всех сервисов в цепочке), cbe8ef14e68a8304 — ID спана (уникальный для конкретной операции). Эти идентификаторы присутствуют в логах всех сервисов, участвующих в обработке запроса, что позволяет связать их воедино при анализе.
Интересно, что Sleuth не только добавляет эти идентификаторы в логи, но и передает их между сервисами в заголовках HTTP-запросов или метаданных сообщений. Это позволяет поддерживать контекст трассировки через все сервисы.
Визуализация трассировки запросов с помощью Zipkin UI
Zipkin UI представляет собой интуитивно понятный инструмент для анализа трассировок. Основное представление — это временная шкала, которая отображает все спаны в рамках одной трассы, их продолжительность и вложенность. Для каждого спана доступны детали: метаданные запроса, заголовки, теги, аннотации и даже SQL запросы, если подключен соответствующий инструментарий. Это бесценно для понимания, что именно происходило в системе во время выполнения запроса.
Пример использования: пользователь жалуется на долгую загрузку страницы. Мы находим соответствующий запрос в Zipkin и видим, что из общего времени 3 секунды 2.7 секунды ушло на запрос к сервису каталога. Посмотрев детали этого спана, обнаруживаем, что выполнялся неоптимизированный SQL-запрос без индексов. Проблема локализована, и можно приступать к её решению.
В другом случае мы можем заметить, что сервис авторизации вызывается несколько раз в рамках одного запроса из разных сервисов, что указывает на возможную оптимизацию через кэширование или перепроектирование взаимодействия. Для более продвинутых сценариев анализа Zipkin поддерживает зависимости сервисов (Service Dependencies), которые визуализируют, какие сервисы взаимодействуют друг с другом и с какой интенсивностью. Это помогает выявить узкие места в архитектуре и спланировать оптимизации.
Настройка сбора данных о запросах
В дополнение к базовым возможностям Sleuth, можно настроить более детальный сбор данных о запросах. Например, добавить кастомные теги к спанам для более точной фильтрации в Zipkin UI:
Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| @Component
public class CustomTagsContributor implements TracingHandlerInterceptor.TagsContributor {
@Override
public Map<String, String> requestTags(HttpServletRequest request) {
Map<String, String> tags = new HashMap<>();
tags.put("customerType", request.getHeader("X-Customer-Type"));
return tags;
}
@Override
public Map<String, String> responseTags(HttpServletResponse response) {
Map<String, String> tags = new HashMap<>();
tags.put("httpStatus", String.valueOf(response.getStatus()));
return tags;
}
} |
|
Этот код добавляет к трассировке информацию о типе клиента из заголовка и HTTP статуса ответа, что может быть полезно для анализа проблем, специфичных для определенных клиентов или типов ответов. Для ручного управления спанами, например, при выполнении длительных операций внутри сервиса, можно использовать Tracer :
Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| @Service
public class OrderProcessingService {
private final Tracer tracer;
public OrderProcessingService(Tracer tracer) {
this.tracer = tracer;
}
public void processOrder(Order order) {
Span paymentSpan = tracer.nextSpan().name("payment-processing").start();
try (SpanInScope ws = tracer.withSpan(paymentSpan.start())) {
// Логика обработки платежа
paymentSpan.tag("orderId", order.getId().toString());
paymentSpan.tag("amount", order.getTotalAmount().toString());
} finally {
paymentSpan.finish();
}
// Остальная логика обработки заказа
}
} |
|
Такой подход позволяет разбивать длительные операции на отдельные спаны для более детального понимания, на что именно уходит время.
Для продакшн-окружений важно правильно настроить сэмплирование, чтобы не пересылать все трассы (что создаст излишнюю нагрузку), но при этом сохранить репрезентативную выборку или важные трассы. Spring Cloud Sleuth предоставляет несколько стратегий сэмплирования:
YAML | 1
2
3
4
5
6
| spring:
sleuth:
sampler:
probability: 0.1 # Базовая вероятность сэмплирования
web:
skipPattern: /health|/info|/metrics # Игнорировать трассировку для определённых эндпоинтов |
|
Для более продвинутой настройки можно реализовать собственную стратегию сэмплирования:
Java | 1
2
3
4
5
6
7
8
9
10
11
12
| @Component
class CustomSampler implements Sampler {
@Override
public boolean isSampled(Span.SpanBuilder spanBuilder) {
// Решение о сэмплировании на основе контекста
String path = MDC.get("http.path");
if (path != null && path.startsWith("/api/critical")) {
return true; // Всегда трассировать критически важные запросы
}
return Math.random() < 0.1; // Для остальных - 10% вероятность
}
} |
|
Такой самплер будет всегда трассировать запросы к критически важным API и выборочно — остальные запросы.
Интеграция с централизованными системами логирования
Чтобы получить максимум пользы от распределённой трассировки, рекомендуется интегрировать её с централизованной системой логирования, такой как ELK Stack (Elasticsearch, Logstash, Kibana) или Graylog. Это позволяет коррелировать трассы Zipkin с логами приложений, создавая полную картину происходящего. Для этого достаточно настроить аппендер в логгере, который будет включать трассировочные идентификаторы из Sleuth:
XML | 1
2
3
4
5
6
| <appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeMdcKeyName>X-B3-TraceId</includeMdcKeyName>
<includeMdcKeyName>X-B3-SpanId</includeMdcKeyName>
</encoder>
</appender> |
|
Теперь в Kibana или Graylog можно искать логи по ID трассировки, что позволяет быстро найти все записи, относящиеся к конкретному запросу, даже если они распределены по разным сервисам.
Для приложений с высоким объёмом трафика передача всех логов может стать проблематичной. В таких случаях можно настроить фильтрацию на уровне логгера:
Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| @Component
public class TraceContextPropagatingMdcFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws IOException, ServletException {
boolean isErrorContext = false;
try {
filterChain.doFilter(request, response);
} catch (Exception e) {
isErrorContext = true;
throw e;
} finally {
// Увеличиваем уровень логирования для запросов с ошибками
if (isErrorContext || response.getStatus() >= 400) {
MDC.put("logLevel", "TRACE");
}
}
}
} |
|
Подобный подход позволяет собирать более детальные логи только для проблемных запросов, сохраняя баланс между полнотой информации и нагрузкой на систему.
По опыту, интеграция Sleuth, Zipkin и ELK Stack даёт синергетический эффект: трассы помогают быстро идентифицировать проблемный сервис, а логи предоставляют детальный контекст для понимания причины проблемы. Это значительно сокращает среднее время устранения неисправностей (MTTR), что критично в микросервисной архитектуре с её распределённой природой.
При настройке всей этой инфраструктуры важно помнить и о безопасности: данные трассировки могут содержать конфиденциальную информацию. Многие команды реализуют фильтрацию чувствительных данных на уровне Sleuth:
Java | 1
2
3
4
5
6
7
8
9
10
11
12
| @Bean
SpanAdjuster customSanitizer() {
return span -> {
// Маскируем персональные данные в тегах
span.tags().forEach((key, value) -> {
if (key.equals("user.email") && value != null) {
span.tag(key, value.toString().replaceAll("(.*)@.*", "$1@***"));
}
});
return span;
};
} |
|
Такая настройка гарантирует, что даже при сборе данных о запросах чувствительная информация будет защищена, что особенно важно для систем, работающих с персональными данными или финансовой информацией.
Применение
Теперь, когда мы разобрались с теоретическими аспектами обнаружения сервисов и распределённой трассировки в Spring Cloud, давайте перейдём к применению этих технологий. На реальных примерах рассмотрим, как решаются типичные проблемы микросервисных архитектур с помощью Spring Cloud.
Типовые сценарии использования
Один из наиболее распространённых сценариев применения Spring Cloud — это создание API-шлюза с динамической маршрутизацией. Допустим, у нас есть несколько микросервисов: каталог продуктов, корзина покупок, управление заказами и система оплаты. Вместо того чтобы фронтенд обращался к каждому сервису напрямую, мы создаём единую точку входа через Spring Cloud Gateway.
Java | 1
2
3
4
5
6
7
| @SpringBootApplication
@EnableDiscoveryClient
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
} |
|
Настройка маршрутов может выглядеть так:
YAML | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| spring:
cloud:
gateway:
routes:
- id: catalog-service
uri: lb://catalog-service
predicates:
- Path=/api/catalog/[B]
- id: cart-service
uri: lb://cart-service
predicates:
- Path=/api/cart/[/B]
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/** |
|
Префикс lb:// указывает Gateway использовать клиентскую балансировку нагрузки через Eureka. Это позволяет API-шлюзу автоматически распределять запросы между несколькими экземплярами сервисов.
Другой типичный сценарий — обеспечение отказоустойчивости при взаимодействии между сервисами. Представьте, что сервис заказов должен вызывать сервис оплаты, но тот временно недоступен или работает медленно. С помощью Resilience4j (современная замена Hystrix в Spring Cloud) можно реализовать шаблоны Circuit Breaker и Fallback:
Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| @Service
public class OrderProcessingService {
private final PaymentClient paymentClient;
@CircuitBreaker(name = "paymentService", fallbackMethod = "processPaymentFallback")
@Retry(name = "paymentService", fallbackMethod = "processPaymentFallback")
@Bulkhead(name = "paymentService", fallbackMethod = "processPaymentFallback")
public PaymentResponse processPayment(Long orderId, BigDecimal amount) {
return paymentClient.processPayment(new PaymentRequest(orderId, amount));
}
public PaymentResponse processPaymentFallback(Long orderId, BigDecimal amount, Exception e) {
log.error("Failed to process payment for order: {}, amount: {}", orderId, amount, e);
// Сохраняем запрос оплаты в очередь для последующей обработки
paymentQueueService.queuePayment(orderId, amount);
return new PaymentResponse(orderId, PaymentStatus.PENDING, "Payment queued for processing");
}
} |
|
Здесь мы применяем сразу несколько паттернов отказоустойчивости:- Circuit Breaker временно приостанавливает вызовы сервиса после серии ошибок.
- Retry автоматически повторяет запрос несколько раз в случае временных сбоев.
- Bulkhead ограничивает количество одновременных запросов к сервису.
Решение проблем масштабирования
Масштабирование микросервисной архитектуры ставит ряд специфических задач. Одна из них — статическая конфигурация, которая становится узким местом при динамическом изменении инфраструктуры. Spring Cloud Config в сочетании с Eureka помогает решить эту проблему, обеспечивая централизованное хранение конфигураций и их динамическое обновление. Например, у нас есть микросервисы, которые должны подключаться к базе данных. Вместо хранения параметров подключения в каждом сервисе, мы храним их в центральном репозитории конфигураций:
YAML | 1
2
3
4
5
6
| # config-repo/database-config.yml
spring:
datasource:
url: jdbc:mysql://db-server:3306/app_db
username: ${DB_USER}
password: ${DB_PASSWORD} |
|
А сервисы запрашивают эту конфигурацию при запуске:
YAML | 1
2
3
4
5
6
| # Конфигурация микросервиса
spring:
application:
name: order-service
config:
import: configserver:[url]http://config-server:8888[/url] |
|
Теперь при необходимости изменить параметры подключения достаточно обновить один файл в репозитории. Более того, Spring Cloud Bus позволяет уведомлять все сервисы об изменениях, чтобы они могли обновить свою конфигурацию без перезапуска.
Другая проблема масштабирования — увеличение числа зависимостей между сервисами, что может привести к хрупкости системы. Здесь помогает асинхронное взаимодействие через событийную шину. Например, вместо прямого вызова сервиса доставки из сервиса заказов, мы можем использовать Spring Cloud Stream:
Java | 1
2
3
4
5
6
7
8
9
10
11
12
| @Service
public class OrderCompletionService {
private final StreamBridge streamBridge;
public void completeOrder(Order order) {
// Бизнес-логика обработки заказа
// Публикация события о завершении заказа
DeliveryRequestEvent event = new DeliveryRequestEvent(order.getId(), order.getDeliveryAddress());
streamBridge.send("delivery-requests", event);
}
} |
|
Сервис доставки подписывается на эти события:
Java | 1
2
3
4
5
6
7
| @Bean
public Consumer<DeliveryRequestEvent> processDeliveryRequests() {
return event -> {
log.info("Processing delivery request for order: {}", event.getOrderId());
// Логика организации доставки
}
} |
|
Такой подход уменьшает связанность между сервисами и делает систему более устойчивой к сбоям отдельных компонентов.
Сравнение с альтернативными подходами
Наряду со Spring Cloud существуют и другие решения для построения микросервисной архитектуры. Например, Kubernetes с Service Discovery и Istio для управления трафиком. В чём различия этих подходов?
Spring Cloud фокусируется на уровне приложения, предоставляя набор библиотек и паттернов для разработчиков Java. Это позволяет быстро начать и абстрагироваться от деталей инфраструктуры. Kubernetes же работает на уровне контейнеров и предоставляет универсальное решение для любых приложений, независимо от языка программирования.
В современных архитектурах часто комбинируют оба подхода: используют Spring Cloud для облегчения разработки, а Kubernetes — для оркестрации контейнеров в продакшн-среде. Eureka можно заменить на Kubernetes Service Discovery, Spring Cloud Gateway — на Istio, но продолжать использовать удобные абстракции Spring Cloud на уровне кода.
В некоторых случаях команды отказываются от клиентской балансировки нагрузки в пользу серверной (через прокси или Istio), что упрощает настройку клиентов, но требует более сложной инфраструктуры. Выбор подхода зависит от конкретных требований и ограничений проекта.
Плохо структурирован spring cloud проект Сказали плохо структурировано, конечно код не блещет красотой, есть косяки, но то что структурировано плохо хз...
А может я ничего не понимаю. В... Spring Cloud Sleuth и Zipkin - не получаю отчеты Всем доброго дня!
Пробую заюзать ZipKin для отслеживания запросов внутри микросервиса, подключил депенденси
<dependency>
... spring cloud gateway - куки из php сервиса Всем доброго дня!
Имеется CRM система написана на PHP, само собой сессии и куки...
Хотелось бы по тихой грусти переползти на Java Cloud с... spring cloud sleuth ignores skip-pattern Собственно надо прикрутить трэйсинг.
Добавляю spring cloud starter sleuth 2.2.7, в итоге в зипкин прилетает всякий мусор типа /actuator/health и... Spring Cloud Gateway и несколько node одного сервиса Всем доброго дня!
Имеется SpringCloud Gateway и несколько разных сервис (по одной копии)
Все работает в docker контейнерах...
Настройка... Микросервисы Добрый день, уважаемые коллеги!
Что подразумевает под собой понятие: «глубокое знание микросервисной архитектуры»?
Поделитесь своим мнением... Научить микросервисы передавать запросы друг другу Technologies:
· Spring core, MVC. (Don’t use Spring Boot)
· Hibernate
Task: develop 3 microservices using Spring MVC (as a servlet... SpringCloud ConfigServer и Микросервисы - ожидание запуска сервера конфигураций Всем доброго дня!
Подскажите куда копнуть, есть SpringCloudConfigServer - для распространения конфигураций микросервисов.
И есть парочка... Ошибка при создании Spring Tool Suite -> Spring MVC Project Добрый день. Подскажите в чем проблема. Я делаю Spring Tool Suite -> Spring MVC Project и создаю проект с каким-нибудь названием, например... Как установить/подключить Spring and Spring Initializr Service в IDEA 14.1.6? как в окне File->New->Project подключить вкладку Spring Initializr, Spring в IntelliJ IDEA 14.1.6 ? Spring MVC. 404 ошибка при включении Spring Data JPA в проект Добрый день. Есть простой шаблонный проект с использованием Spring MVC и Maven. С зависимостями Spring MVC проект собирается нормально и после... задания по spring core и spring mvc для новичков Какие задания можно предложить новичкам для выполнения после знакомства их с spring core и mvc ?
|