← Kubernetes: сеть
7 мин · средне · Урок 4 из 8

Endpoints и selector: как Service теряет поды

Откуда Service берёт список адресов, почему Endpoints уступил место EndpointSlice и что обнуляет список.

Кто такие Endpoints

Service api selector app: api selector controller endpoints-controller пишет EndpointSlice адреса Ready-подов читает kube-proxy правила на ноде
Список адресов ведёт контроллер, а читает его kube-proxy. Сам Service ничего не хранит.

Service хранит selector. Например:

spec:
  selector:
    app: api

Рядом с каждым таким Service kube-controller-manager автоматически создаёт объект с адресами Pod, которые попали под selector и готовы (Ready). Исторически это Endpoints, сейчас ему на смену пришёл EndpointSlice. Оба живут рядом с Service и показывают одну и ту же картину.

kubectl get svc api
kubectl get endpoints api
kubectl get endpointslice -l kubernetes.io/service-name=api

Endpoints устарел, актуален EndpointSlice

Объект Endpoints (API-группа v1) объявлен устаревшим начиная с Kubernetes 1.33. Он никуда не делся и продолжает создаваться, но kubectl теперь честно предупреждает:

kubectl get endpoints api
# Warning: v1 Endpoints is deprecated in v1.33+; use discovery.k8s.io/v1 EndpointSlice
# NAME   ENDPOINTS                           AGE
# api    10.42.0.142:8080,10.42.0.143:8080   1s

Замена - EndpointSlice (discovery.k8s.io/v1). Именно его читает kube-proxy, когда программирует правила маршрутизации трафика. kube-proxy - компонент на каждой ноде, который настраивает правила сети для Service.

Причина замены в масштабе. Endpoints - один объект на Service, и все адреса лежат в нём списком. У Service с тысячей Pod любое изменение (поднялся Pod, упал Pod) переписывает весь объект целиком, и эта версия рассылается каждому узлу кластера. На больших кластерах это заметная нагрузка на control plane и сеть.

EndpointSlice режет тот же список на срезы, по умолчанию до 100 адресов в каждом. Меняется один Pod - переписывается только его срез, остальные не трогаются. Отсюда и название: slice, кусок общего списка.

kubectl get endpointslice -l kubernetes.io/service-name=api
# NAME        ADDRESSTYPE   PORTS   ENDPOINTS                 AGE
# api-jdlhd   IPv4          8080    10.42.0.142,10.42.0.143   1s

Имя среза генерируется автоматически (api-jdlhd), поэтому искать их принято по лейблу kubernetes.io/service-name, а не по имени Service.

Что ещё умеет EndpointSlice, чего не умел Endpoints:

v1 Endpointsdiscovery.k8s.io/v1 EndpointSlice
Статусdeprecated с 1.33актуальный
Объектов на Serviceодинпо срезу на каждые 100 адресов
Изменился один Podпереписывается весь списокпереписывается один срез
Состояние адресаготов или нетready, serving, terminating
Читает kube-proxyнетда

Для быстрой ручной проверки «дошёл ли Pod до Service» короткая команда kubectl get endpoints всё ещё удобнее, поэтому в чужих гайдах она встречается постоянно. В новом коде и в скриптах опирайтесь на EndpointSlice: у Endpoints впереди только удаление из API.

Что попадает в Endpoints

Контроллер собирает IP Pod, если выполнено всё:

  1. Pod в том же namespace что и Service.
  2. Все labels из selector совпадают с labels Pod.
  3. Pod в фазе Running.
  4. У всех readinessProbe пройден последний check (или probe нет - тогда готов сразу). readinessProbe - проверка, по которой Kubernetes понимает, готов ли контейнер принимать трафик.
  5. Pod не в процессе удаления (у него не выставлен deletionTimestamp).

Стоит сломать любую галочку - IP уходит из Endpoints за пару секунд. kube-proxy перестаёт направлять туда трафик.

Что показывает kubectl

$ kubectl get endpoints api
NAME   ENDPOINTS                              AGE
api    10.42.0.5:8080,10.42.0.6:8080          2m

В EndpointSlice вывод подробнее: видно условия Ready/NotReady/Terminating для каждого адреса. Это полезно при отладке: если pod есть, а трафик не идёт, скорее всего у него readinessProbe красный.

kubectl describe endpoints api
kubectl get endpointslice -l kubernetes.io/service-name=api -o yaml

Как ломается селектор

Три классических варианта.

1. Опечатка в selector. Service ищет app: aip (вместо api). Endpoints пустые, запросы валятся в connection refused. Лечится правкой Service:

kubectl patch svc api --type=merge -p '{"spec":{"selector":{"app":"api"}}}'

2. Pod перевесили на другие метки. Кто-то прогнал kubectl label pod ... app=other --overwrite. Pod живы, но больше не матчат Service. Лечится возвратом метки.

3. Service в одном namespace, Pod в другом. Selector работает только внутри своего namespace. Никакой selector: { ns: other } его не расширит.

Selector матчит «слишком много»: попали Pod другого приложения с тем же ярлыком. Балансировка летит в чужие IP.

Headless Service

Если в Service выставить clusterIP: None, Kubernetes не выдаёт виртуальный IP. DNS возвращает напрямую адреса Pod. Endpoints всё равно собирает тот же контроллер по тому же selector, отличается только то, как клиент получает адрес.

Service без selector

Можно создать Service без selector и руками управлять Endpoints. Это способ направить трафик внутри кластера на внешний адрес (база на отдельном сервере, managed-инстанс, что угодно). Контроллер ничего сам не пишет, IP прописывают вручную:

apiVersion: v1
kind: Endpoints
metadata:
  name: external-db
subsets:
  - addresses: [{ ip: 10.0.0.42 }]
    ports: [{ port: 5432 }]

Эта схема показывает, что Endpoints - реально отдельный объект, который можно редактировать независимо. Просто обычно это делает контроллер.

Что обычно ломается

Домашка

Теорию закрепляет задание на своём кластере: 1 шаг с проверкой. Кластер поднимается по треку Kubernetes локально, задания лежат в репозитории k8s-practice.

./run.sh start k8s-endpoints-selector     # развернуть стартовое состояние
./run.sh verify k8s-endpoints-selector    # проверить текущий шаг

Коротко

Прошли материал до конца? Отметьте урок пройденным.