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

У чому різниця між client-side та server-side discovery

У мікросервісній архітектурі Client-Side та Server-Side Discovery — це два фундаментальні способи виявлення мережевих координат (IP:Port) та балансування навантаження між динамі...


🟢 Junior Level

У мікросервісній архітектурі Client-Side та Server-Side Discovery — це два фундаментальні способи виявлення мережевих координат (IP:Port) та балансування навантаження між динамічними інстансами сервісів.

Client-Side Discovery (Клієнт знає все):
                  ┌────────────────────┐
                  │  Service Registry  │
                  └─────────▲──────────┘
             1. Query IP    │
             ┌──────────────┘
             ▼
      [Service A] ────────2. Direct Call (1 Hop)────────► [Service B (Pod 2)]
  (Client Balancer)

Server-Side Discovery (Клієнт простий, балансувальник розумний):
                                    ┌────────────────────┐
                                    │  Service Registry  │
                                    └─────────▲──────────┘
                                              │ Query IP
                                              ▼
      [Service A] ──1. Call──► [Load Balancer / Proxy] ──2. Forward (2 Hops)──► [Service B]

Головна відмінність:

  • Client-Side Discovery: Клієнт (викликаючий сервіс) сам звертається до реєстру, отримує список доступних IP-адрес, застосовує алгоритм балансування (Round Robin, Least Connections) і надсилає запит безпосередньо в цільовий інстанс.
  • Server-Side Discovery: Клієнт нічого не знає про реєстр і надсилає запит на проміжний балансувальник (Load Balancer / Reverse Proxy). Балансувальник сам опитує реєстр і перенаправляє запит на конкретний інстанс.

🟡 Middle Level

1. Архітектурне та технічне порівняння

Критерій Client-Side Discovery Server-Side Discovery
Кількість мережевих переходів (Hops) 1 Hop: Клієнт $\to$ Цільовий інстанс (мінімальна затримка) 2 Hops: Клієнт $\to$ Балансувальник $\to$ Цільовий інстанс
Складність клієнта Висока: у кожен сервіс потрібно впроваджувати SDK (Spring Cloud LoadBalancer, gRPC channel) Нульова: сервіс робить звичайний HTTP-запит на статичний DNS/IP
Поліглотність (Polyglot) Складна: бібліотеку виявлення та балансування потрібно реалізувати для Java, Go, Node.js, Python Ідеальна: працює з будь-якою мовою та стеком технологій «з коробки»
Єдина точка відмови (SPOF) Відсутня: балансування децентралізоване по всіх клієнтах Балансувальник є критичним компонентом (вимагає HA-кластера)
Обізнаність про контекст Висока: клієнт може враховувати локальну зону доступності (AZ-Affinity) Балансувальник бачить лише вхідний мережевий пакет

2. Реалізація Client-Side у Java (Spring Cloud LoadBalancer)

В екосистемі Java історичний Netflix Ribbon був замінений на Spring Cloud LoadBalancer:

@Configuration
public class ClientConfig {

    @Bean
    @LoadBalanced // Вмикає клієнтське балансування через реєстр (Consul / Eureka)
    public RestClient.Builder loadBalancedRestClientBuilder() {
        return RestClient.builder();
    }
}

@Service
@RequiredArgsConstructor
public class OrderService {

    private final RestClient.Builder restClientBuilder;

    public void invokePayment(PaymentDto dto) {
        // Ім'я "payment-service" резолвиться локально клієнтом без звернення до зовнішнього LB
        restClientBuilder.build()
            .post()
            .uri("http://payment-service/api/v1/charge")
            .body(dto)
            .retrieve()
            .toBodilessEntity();
    }
}

3. Server-Side: Nginx / AWS ALB / Kubernetes Service

У Server-Side підході клієнт викликає фіксовану адресу:

curl http://payment-internal.company.com/api/v1/charge

Маршрутизатор (Nginx Plus, AWS Application Load Balancer або K8s Ingress) динамічно оновлює свій upstream із реєстру і розподіляє запити.


🔴 Senior Level

1. Зональна маршрутизація (AZ-Affinity) та оптимізація витрат у хмарі

У хмарах (AWS, GCP, Azure) дата-центри розділені на зони доступності (Availability Zones: az-a, az-b, az-c).

  • Передача трафіку між подами всередині однієї AZ безкоштовна і має затримку $< 0.5$ мс.
  • Передача трафіку між різними AZ (Cross-AZ Traffic) тарифікується провайдером (наприклад, $0.01 за гігабайт в AWS) і додає $1\text{–}2$ мс latency.

Перевага Client-Side: Клієнтський балансувальник знає, у якій зоні знаходиться сам клієнт. Він фільтрує список інстансів із реєстру і віддає пріоритет подам із тієї самої зони доступності (Zone-Preference / Zone-Affinity). При використанні централізованого Server-Side балансувальника запити випадковим чином перетинають межі зон, генеруючи величезні рахунки за міжзональний трафік у великих високонавантажених системах.

2. Service Mesh (Envoy Sidecar) — гібридний прорив

Патерн Service Mesh (Istio / Linkerd) об’єднав найкращі якості обох світів, усунувши їхні недоліки:

Архітектура Service Mesh:
[App Container (Java/Go/Node)] 
       │ (1. HTTP-виклик на localhost:15001 — виглядає як Server-Side для застосунку)
       ▼
[Envoy Sidecar Proxy] ──(2. xDS Discovery + mTLS + Client-Side Routing: 1 Hop)──► [Target Envoy Sidecar]
                                                                                             │
                                                                                             ▼
                                                                                    [Target App Container]
  1. Для застосунку це виглядає як Server-Side Discovery: код пише простий запит http://payment-service/api, ніяких важких бібліотек Eureka/Spring Cloud підключати не потрібно (повна поліглотність).
  2. На рівні мережі це працює як Client-Side Discovery: локальний проксі Envoy вичитує актуальний список подів за протоколом xDS, виконує балансування з урахуванням зон (AZ) і шле трафік напряму в цільовий под (без центрального вузла балансування і зайвих hops).

3. Kubernetes ClusterIP: Server-Side чи Client-Side під капотом?

Архітектурний парадокс на співбесідах:

  • Зовні (концептуально): Kubernetes Service (ClusterIP) позиціонується як Server-Side Discovery — клієнт відправляє запит на єдину віртуальну IP-адресу сервісу.
  • Під капотом (реалізація): Насправді це децентралізоване клієнтське балансування! Компонент kube-proxy налаштовує таблиці маршрутизації (iptables або IPVS) безпосередньо на тій робочій ноді, де запущений клієнт. Запит транслюється в реальний IP пода-одержувача прямо в мережевому стеку ядра Linux (DNAT) без проходження через виділений фізичний балансувальник.

4 Tricky Questions

1. Чому Client-Side балансування критично важливе для протоколу HTTP/2 та gRPC?

Відповідь: Протокол HTTP/2 мультиплексує сотні логічних запитів в одне постійне TCP-з’єднання. Якщо перед gRPC-сервісом поставити класичний L4-балансувальник (Server-Side TCP LB), клієнт відкриє рівно одне TCP-з’єднання до балансувальника, а той прокине його до одного пода. Усі наступні мільйони gRPC-запитів підуть в один єдиний под, перевантаживши його, поки решта подів простоюють. Для gRPC необхідний або L7-балансувальник (який розбирає HTTP/2 фрейми з високим навантаженням на CPU), або Client-Side балансування (gRPC NameResolver + Subchannel balancing), де клієнт сам відкриває пул TCP-з’єднань до всіх подів і балансує окремі RPC-виклики.

2. У чому небезпека Client-Side Discovery при масштабному збої мережі (Thundering Herd на Registry)?

Відповідь: Якщо в кластері з 5000 мікросервісів відбувається ребалансування нод, сервіси починають масово перезапускатися. При Client-Side підході тисячі подів одночасно починають бомбардувати Service Registry HTTP-запитами на отримання оновленого списку адрес. Це призводить до відмови в обслуговуванні (DDoS) самого реєстру. Захист:

  1. Використання Push-моделі замість Polling (WebSocket, HTTP/2 Server Push, gRPC streaming, як у Consul Watches або Envoy xDS).
  2. Експоненційний джитер (Jitter) при повторних запитах.
  3. Локальне довготривале кешування списку інстансів на клієнті.

3. Як Server-Side балансувальник може стати «пляшковим горлом» (Bottleneck) за TLS-сертифікатами та шифруванням?

Відповідь: У Server-Side архітектурі центральний балансувальник виконує TLS Termination: він розшифровує вхідний клієнтський трафік і наново шифрує його перед відправленням у цільовий под (mTLS). При піковому трафіку в сотні тисяч RPS центральний балансувальник вичерпує ресурси CPU на операціях асиметричної криптографії. У Client-Side підході (або Service Mesh) криптографічне навантаження mTLS рівномірно розподілене по всіх клієнтських машинах кластера.

4. Що станеться, якщо в Client-Side підході один із клієнтів має забагований алгоритм балансування?

Відповідь: При Client-Side підході логіка розподілу трафіку розмазана по тисячах розподілених клієнтів. Якщо одна команда викатить реліз зі зламаним алгоритмом (наприклад, випадковий вибір падає завжди на перший інстанс списку), ця група клієнтів «заллє» один под трафіком, викликавши локальну аварію. У Server-Side підході алгоритм балансування централізований і керується командою інфраструктури/SRE, що виключає вплив клієнтських багів на розподіл навантаження.


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

Головне:

  • Client-Side: Клієнт сам ходить у Registry і вибирає інстанс. Плюси: 1 мережевий hop (мінімальний latency), немає центрального SPOF, підтримка зональної маршрутизації (AZ-affinity). Мінуси: прив’язка до клієнтських бібліотек кожною мовою програмування.
  • Server-Side: Клієнт шле запит на спільний балансувальник (Load Balancer). Плюси: повна поліглотність (клієнти прості), централізоване керування. Мінуси: +1 hop мережею, ризик утворення bottleneck та єдиної точки відмови.
  • Service Mesh (Istio / Envoy): Сучасний стандарт, що поєднує плюси обох підходів (для застосунку — Server-Side за простотою, за фактом — Client-Side Sidecar із мінімальним latency).
  • gRPC вимагає Client-Side балансування через мультиплексування HTTP/2 з’єднань.

Порівняльна матриця:

\(\begin{array}{|l|c|c|} \hline \textbf{Характеристика} & \textbf{Client-Side} & \textbf{Server-Side} \\ \hline \text{Мережеві Hops} & 1 & 2 \\ \text{Поліглотність} & \text{Низька (вимагає SDK)} & \text{Ідеальна} \\ \text{AZ-Affinity (економія \$)} & \text{Відмінна} & \text{Ускладнена} \\ \text{gRPC балансування} & \text{Нативна} & \text{Вимагає дорогий L7 LB} \\ \hline \end{array}\)

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

  • ❌ «У мікросервісах завжди краще використовувати зовнішній апаратний F5/Nginx балансувальник для кожного виклику між сервісами» (Породжує колосальний оверхед за latency та вартістю).
  • ❌ «У Kubernetes балансування чисто Server-Side через окремий сервер» (Віртуальний ClusterIP транслюється локально на ноді через iptables/IPVS компонентом kube-proxy).
  • ❌ «Netflix Ribbon — це сучасне рішення» (Ribbon мертвий багато років, у Spring Cloud використовується Spring Cloud LoadBalancer).

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