На middle и senior собесах по Spring почти всегда есть блок про микросервисы: как сервисы находят друг друга, что делать, когда соседний сервис лежит, как не потерять данные между сервисами и как понять, где тормозит запрос.
Зачем и когда
Микросервисы дают независимый деплой и масштабирование частей системы, разделение по командам. Цена: сеть вместо вызова метода, распределённые данные, сложная отладка и эксплуатация. Хороший ответ на собесе начинается с того, что модульный монолит часто разумнее, пока нет причин делить.
Конфигурация
- Переменные окружения и Kubernetes ConfigMap/Secret: самый частый вариант сегодня.
- Spring Cloud Config: централизованный сервер конфигурации поверх Git, клиенты получают свойства при старте (
spring.config.import=configserver:). Обновление на лету через@RefreshScopeи/actuator/refreshили шину.
Обнаружение сервисов и балансировка
- В Kubernetes обнаружение даёт сам кластер:
http://orders-serviceрезолвится через DNS Service, балансировку делает kube-proxy или service mesh. - Вне Kubernetes: реестр (Eureka, Consul), клиенты регистрируются и получают адреса,
spring-cloud-loadbalancerвыбирает экземпляр на стороне клиента.
Вызовы между сервисами
- Синхронно:
RestClient/WebClient, декларативные HTTP-интерфейсы (@HttpExchange, Spring 6) или OpenFeign. - Асинхронно: события через Kafka или RabbitMQ.
Каждому синхронному вызову нужен таймаут: без него один медленный сосед исчерпает потоки и уронит всю цепочку.
Отказоустойчивость (Resilience4j)
- Timeout: не ждать вечно.
- Retry: повтор с экспоненциальной задержкой и джиттером, только для идемпотентных операций и временных ошибок.
- Circuit breaker: при большой доле ошибок «размыкается» и сразу отвечает отказом или запасным ответом, через время пропускает пробные вызовы (half-open).
- Bulkhead: отдельный лимит параллельных вызовов на зависимость.
- Rate limiter: ограничение частоты.
@CircuitBreaker(name = "pricing", fallbackMethod = "cachedPrice")
@Retry(name = "pricing")
public Price price(String sku) { return pricingClient.get(sku); }
Аннотации работают через прокси: те же ограничения с self-invocation. Ретраи без circuit breaker во время аварии множат нагрузку на и так лежащий сервис.
API Gateway
Spring Cloud Gateway: единая точка входа, маршрутизация, аутентификация (проверка JWT), rate limiting, CORS, сбор логов. Сервисы за шлюзом не торчат наружу.
Данные между сервисами
- База на сервис: другие сервисы не читают её таблицы напрямую.
- Распределённых транзакций (2PC) избегают. Вместо них сага: цепочка локальных транзакций с компенсирующими действиями (хореография через события или оркестратор).
- Transactional outbox: событие пишется в таблицу outbox в той же транзакции, что и бизнес-данные, а отдельный процесс (поллер или Debezium) публикует его в брокер. Так не бывает «данные сохранились, а событие потерялось» или наоборот.
- Потребители должны быть идемпотентными: доставка «как минимум один раз» означает дубли.
Наблюдаемость
- Логи в JSON с идентификатором трассы.
- Метрики Micrometer → Prometheus → Grafana.
- Распределённая трассировка: в Boot 3 это Micrometer Tracing (Spring Cloud Sleuth больше не развивается и не поддерживает Boot 3) с экспортом в Zipkin, Jaeger или OpenTelemetry. Контекст трассы передаётся в заголовках
traceparent(W3C). - Health-пробы liveness и readiness для Kubernetes.
Частые ошибки
- Синхронная цепочка из пяти сервисов без таймаутов.
- Ретраи неидемпотентных операций (двойные платежи).
- Общая база на несколько сервисов.
- Публикация события после коммита «как получится», без outbox.
Что спросят на собесе
- Когда микросервисы оправданы, а когда нет?
- Как сервисы находят друг друга в Kubernetes и без него?
- Что такое circuit breaker и чем он отличается от retry?
- Как обеспечить согласованность данных без распределённых транзакций? Что такое сага и outbox?
- Как отследить запрос через несколько сервисов?