🏗️ Розділ 17 · Питання #7

Що таке Service Discovery і навіщо він потрібен

У сучасній хмарній та контейнерній інфраструктурі (Kubernetes, AWS ECS, Docker Swarm):


🟢 Junior Level

Service Discovery (Виявлення сервісів) — це автоматичний механізм, за допомогою якого мікросервіси в розподіленій системі знаходять мережеві координати (IP-адресу та порт) один одного без ручного налаштування і жорстко зашитих конфігурацій (hardcoded IPs).

Динамічне хмарне середовище:
[Піднявся новий інстанс] ──► Реєстрація: "Я payment-service на 10.244.2.15:8080"
                                                │
                                                ▼
                                    ┌───────────────────────┐
                                    │   Service Registry    │
                                    │  (Реєстр сервісів)    │
                                    │ order:   10.244.1.5   │
                                    │ payment: 10.244.2.15  │
                                    │ payment: 10.244.3.40  │
                                    └───────────────────────┘
                                                ▲
[Order Service] ── Запит: "Де payment?" ───────┘
                ◄── Відповідь: [10.244.2.15, 10.244.3.40]
[Order Service] ── Прямий виклик ──► [Payment Service (10.244.2.15)]

Навіщо це потрібно?

У сучасній хмарній та контейнерній інфраструктурі (Kubernetes, AWS ECS, Docker Swarm):

  1. Ефемерні IP-адреси: Контейнери (поди) регулярно перезапускаються, переміщуються між фізичними нодами і при кожному рестарті отримують нову динамічну IP-адресу.
  2. Динамічне масштабування (Auto-scaling): При зростанні навантаження за сигналом метрик створюються десятки нових копій сервісу, а при спаді — знищуються.
  3. Виявлення збоїв: Якщо інстанс завис або впав, він повинен бути негайно виключений із маршрутизації, щоб на нього не відправлявся користувацький трафік.

🟡 Middle Level

1. Архітектурні компоненти Service Discovery

  1. Service Registry (Реєстр сервісів): Високодоступна база даних мережевих адрес активних екземплярів мікросервісів (наприклад, HashiCorp Consul, Netflix Eureka, etcd, Apache ZooKeeper).
  2. Service Registration (Реєстрація):
    • Self-Registration: Сервіс при старті сам відправляє HTTP/gRPC запит у реєстр («я живий за адресою $X$»).
    • Third-Party Registration: Зовнішній агент (наприклад, Kubernetes Kubelet або Registrator) відстежує статус контейнерів і сам оновлює реєстр.
  3. Health Checking & Heartbeating (Пульс і перевірка здоров’я): Кожен інстанс періодично надсилає серцебиття (Heartbeat) кожні 5–30 секунд. Якщо пульс не прийшов протягом таймауту оренди (Lease TTL), реєстр автоматично видаляє інстанс.

2. Реалізація у Spring Cloud (Netflix Eureka / Consul)

// Сервіс-споживач (Order Service)
@Service
@RequiredArgsConstructor
public class PaymentOrderGateway {

    private final DiscoveryClient discoveryClient;
    private final RestClient restClient;

    public void chargeCustomer(String orderId, BigDecimal amount) {
        // Отримуємо список усіх живих інстансів із локально кешованого реєстру
        List<ServiceInstance> instances = discoveryClient.getInstances("payment-service");
        
        if (instances.isEmpty()) {
            throw new IllegalStateException("Немає доступних інстансів payment-service!");
        }

        // Обираємо інстанс (в реальності це робить LoadBalancer автоматично)
        ServiceInstance target = instances.get(0);
        String url = String.format("http://%s:%d/api/v1/charge", target.getHost(), target.getPort());

        restClient.post()
            .uri(url)
            .body(new ChargeRequest(orderId, amount))
            .retrieve()
            .toBodilessEntity();
    }
}

З використанням високорівневої декларативної абстракції @LoadBalanced:

@Configuration
public class WebClientConfig {
    @Bean
    @LoadBalanced // Spring Cloud автоматично резолвить ім'я "payment-service" через Discovery
    public RestClient.Builder restClientBuilder() {
        return RestClient.builder();
    }
}
// Виклик: restClient.get().uri("http://payment-service/api/v1/status").retrieve()...

🔴 Senior Level

1. CAP-теорема в Service Registry: AP (Eureka) vs CP (Consul / ZooKeeper / etcd)

При проєктуванні Service Registry інженери стикаються з фундаментальним вибором за теоремою CAP під час мережевого розділення (Network Partition):

       [Consul / etcd / K8s API]               [Netflix Eureka]
                (CP)                                 (AP)
  ┌───────────────────────────────┐   ┌───────────────────────────────┐
  │ Консистентність понад усе     │   │ Доступність понад усе         │
  │ Використовують консенсус Raft.│   │ Однорангова реплікація (P2P). │
  │ При розриві кворуму запис     │   │ Завжди відповідає на читання, │
  │ блокується. Адреси завжди     │   │ допускає застарілі дані       │
  │ строго актуальні.             │   │ (Stale Data) та помилки.      │
  └───────────────────────────────┘   └───────────────────────────────┘

Режим самозбереження в Eureka (Self-Preservation Mode):

Якщо в Eureka за 15 хвилин понад 15% серцебиттів перестають доходити, Eureka припускає, що сталася мережева аварія між дата-центрами, а не одночасна смерть усіх сервісів. Вона заморожує видалення записів за тайм-аутом. Краще спробувати відправити запит на можливо живий сервіс, ніж миттєво «поховати» всю систему через мережевий збій.

2. Еволюція: від Application-Level до Infrastructure-Level (Kubernetes & Service Mesh)

У сучасних enterprise-системах спостерігається явна відмова від саморобних прикладних реєстрів (Eureka) на користь нативних інфраструктурних рішень:

Еволюція Service Discovery:
1. Прикладний (Spring Cloud Eureka/Ribbon):
   [Java App + Eureka Client] ──HTTP──► [Eureka Server]
   (Мінуси: прив'язка до мови Java, важкі клієнти, дублювання ресурсів)

2. Інфраструктурний (Kubernetes CoreDNS + kube-proxy):
   [App (будь-яка мова)] ──DNS──► [CoreDNS] ──► [ClusterIP (iptables/IPVS)] ──► [Pod]
   (Плюси: прозоро для будь-якої мови, нативно підтримується платформою)

3. Service Mesh (Istio / Envoy):
   [App] ──localhost──► [Envoy Sidecar] ──mTLS/xDS──► [Envoy Sidecar] ──► [App B]
   (Плюси: xDS dynamic routing, Canary, Circuit Breaking, Zero-Trust mTLS)

У Kubernetes кожен Service отримує стабільне DNS-ім’я (payment-service.default.svc.cluster.local), а kube-proxy на рівні ядра Linux (через IPVS або eBPF) транслює віртуальний ClusterIP у реальні IP-адреси подів.


4 Tricky Questions

1. Що станеться, якщо центральний кластер Service Registry (наприклад, Consul) повністю впаде?

Відповідь: Грамотно спроєктовані клієнти (Spring Cloud Discovery, Envoy) реалізують Client-Side Caching:

  1. Клієнт періодично вичитує реєстр і кешує список актуальних IP-адрес у локальній пам’яті.
  2. При падінні Service Registry мікросервіси продовжують успішно викликати один одного, використовуючи кешовані адреси.
  3. Система переходить у деградований режим: старі сервіси працюють стабільно, але нові підняті інстанси не будуть виявлені, а померлі поди відсікатимуться клієнтським Circuit Breaker / Retry при спробі з’єднання.

2. Чому в інфраструктурі Kubernetes використання Netflix Eureka вважається антипатерном?

Відповідь:

  1. Дублювання функціоналу: Kubernetes вже містить вбудований Service Discovery промислового рівня на базі CoreDNS, Endpoints та kube-proxy.
  2. Мовний бар’єр (Polyglot restrictions): Eureka орієнтована на JVM. Якщо в компанії з’являється сервіс на Go, Python або Node.js, інтеграція з Eureka вимагає сторонніх милиць, тоді як DNS у Kubernetes однаково працює для будь-якої мови програмування.
  3. Рассинхронізація життєвого циклу: Eureka покладається на Heartbeat (TTL 30–90 с), а Kubernetes моментально оновлює Endpoints за сигналами контролера подів. При використанні Eureka запити йтимуть на вже вбиті поди ще десятки секунд.

3. Як вирішується проблема «холодного старту» (Warm-up problem), коли сервіс зареєструвався в Registry, але JVM ще не прогріта JIT-компілятором?

Відповідь: Якщо на щойно зареєстрований інстанс одразу спрямувати 100% робочого трафіку, холодний JVM-процес захлинеться і почне віддавати тайм-аути. Рішення:

  • Kubernetes Readiness Probe з початковою затримкою (initialDelaySeconds): под не включається в Endpoints до успішного прогріву.
  • Canary / Slow-Start балансування (в Envoy / Ribbon): новий інстанс отримує лінійно наростаючий відсоток трафіку (наприклад, від 1% до 100% протягом 2 хвилин), даючи JIT-компілятору скомпілювати байткод у нативний код.

4. Що таке «Stale Registry Entry» і чому один тільки Health Check на стороні Registry не гарантує відсутність помилок з’єднання?

Відповідь: Stale Entry (застарілий запис) — це ситуація, коли інстанс аварійно завершився (OOMKilled, відключення живлення сервера), але запис у реєстрі залишається активним до закінчення терміну оренди (Heartbeat TTL, зазвичай 30–90 секунд). У цей інтервал клієнти отримують мертвий IP. Жоден Health Check не здатний миттєво поширити інформацію розподіленою мережею з нульовою затримкою. Захист: Service Discovery ніколи не використовується ізольовано. На клієнті зобов’язана бути зв’язка з Retry (повторити запит на інший інстанс зі списку) та Circuit Breaker.


🎯 Шпаргалка для інтерв’ю

Головне:

  • Service Discovery вирішує проблему динамічних IP-адрес мікросервісів у хмарному середовищі при рестартах та автомасштабуванні.
  • Складається з трьох частин: Service Registry (база адрес), Registration (постановка на облік), Discovery (пошук живих адрес).
  • У Java-світі історично використовувалися Eureka / Consul / ZooKeeper, у сучасному продакшені стандарт — Kubernetes DNS + Service Mesh (Istio / Envoy).
  • За CAP-теоремою: Eureka — AP (при аварії вмикає Self-Preservation, жертвуючи актуальністю заради доступності); Consul/etcd — CP (строгий консенсус Raft).
  • Клієнт зобов’язаний кешувати адреси локально (Client-side cache), щоб пережити відмову самого реєстру.

Червоні прапорці (Чого говорити НЕ МОЖНА):

  • ❌ «У мікросервісах адреси інших сервісів прописуються в application.yml через IP-адреси» (У контейнерах IP ефемерні, хардкод виключений).
  • ❌ «При падінні Eureka вся система моментально зупиняється» (Клієнти кешують список інстансів і продовжують працювати на кеші).
  • ❌ «У Kubernetes обов’язково потрібно розгортати Eureka Server» (У K8s є нативний CoreDNS + Endpoints, Eureka там надлишкова і шкідлива).

Пов’язані теми: