Задача 14. Куда на самом деле идёт запрос к Service

Масштабировали сервис с 3 подов до 10, но нагрузка осталась на прежних трёх. Разбираем, почему балансировка происходит на уровне соединений, и что с этим делать для gRPC и HTTP/2.
Опубликовано:

Задача

Сервис A ходит в сервис B через обычный ClusterIP. Оба на gRPC. Под нагрузкой B масштабировали с 3 реплик до 10.

Ожидание: нагрузка размажется по десяти подам. Реальность: новые 7 подов простаивают, вся нагрузка по-прежнему на исходных трёх.

Вопрос: почему добавленные реплики не получают трафика?

Что кажется очевидным

«Endpoints не обновились» или «probe не прошли». Но kubectl get endpointslices показывает все 10 адресов, все поды Ready.

Как это работает на самом деле

Ключ в том, на каком уровне работает балансировка Kubernetes.

Как разобрано в задаче 6, ClusterIP — это правило DNAT в ядре. Подмена адреса происходит при установке соединения: пакет SYN попадает под правило, для него выбирается бэкенд, и дальше всё соединение идёт в этот под — таково свойство отслеживания соединений (conntrack).

То есть балансировка Kubernetes — это балансировка соединений (L4), а не запросов (L7).

Для обычного HTTP/1.1 без keep-alive это почти незаметно: каждый запрос — новое соединение, распределение получается равномерным. Но:

Отсюда и картина: 3 старых соединения — 3 нагруженных пода, независимо от того, сколько реплик добавлено.

Та же проблема касается любых пулов долгоживущих соединений: подключения к базам, HTTP-клиенты с keep-alive, WebSocket.

Ответ

Kubernetes балансирует соединения, а не запросы. gRPC/HTTP/2 держит одно долгоживущее соединение, которое уже привязано к конкретному поду, поэтому новые реплики не получают трафик — они появились после установки соединений.

Что с этим делать

Есть четыре рабочих подхода, от простого к основательному.

1. Клиентская балансировка

Клиент сам получает список адресов подов и держит соединение с каждым, распределяя запросы. Для этого нужен headless Service:

YAML
apiVersion: v1
kind: Service
metadata:
  name: backend-headless
spec:
  clusterIP: None # headless: DNS вернёт адреса всех подов
  selector:
    app: backend
  ports:
    - port: 50051
Нажмите, чтобы развернуть и увидеть больше

С clusterIP: None DNS-запрос возвращает не один виртуальный адрес, а список IP всех готовых подов. gRPC-клиенты это поддерживают штатно:

PLAINTEXT
grpc.Dial("dns:///backend-headless.default.svc.cluster.local:50051",
    grpc.WithDefaultServiceConfig(`{"loadBalancingConfig": [{"round_robin":{}}]}`))
Нажмите, чтобы развернуть и увидеть больше

Минус: клиент должен уметь пере-резолвить DNS и подхватывать изменения состава подов.

2. Ограничить время жизни соединений

Простой приём, если менять клиента нельзя: заставить соединения периодически переустанавливаться. На стороне сервера это настройки вроде максимального возраста соединения (в gRPC — MAX_CONNECTION_AGE), на стороне клиента — ограничение времени жизни в пуле.

Соединения будут регулярно перезаключаться и попадать на новые поды. Грубо, но работает без переписывания архитектуры.

3. Прокси с балансировкой на уровне запросов

Поставить перед сервисом прокси, понимающий HTTP/2: Envoy, NGINX, HAProxy или ingress-контроллер. Он держит соединения с каждым бэкендом и распределяет запросы, а не соединения.

4. Service mesh

Istio, Linkerd и подобные ставят sidecar-прокси к каждому поду и решают эту задачу прозрачно для приложения — заодно давая ретраи, таймауты, mTLS и наблюдаемость. Цена — заметная сложность инфраструктуры; ради одной балансировки внедрять mesh не стоит, но если он уже есть, проблема решена.

Что ещё стоит знать про Service

Диагностика

BASH
# Все ли поды в endpoints (тут обычно всё в порядке)
kubectl get endpointslices -l kubernetes.io/service-name=backend

# Реальное распределение: сравните нагрузку по подам
kubectl top pods -l app=backend

# Сколько установленных соединений держит конкретный под
kubectl exec backend-xxx -- ss -tn state established
Нажмите, чтобы развернуть и увидеть больше

Если endpoints полные, а нагрузка перекошена — это почти всегда описанный случай с долгоживущими соединениями.


Дальше: Задача 15. Медленный DNS и загадка ndots.

Начать поиск

Введите ключевые слова для поиска статей

↑↓
ESC
⌘K Горячая клавиша