Learning Platform
Глоссарий Troubleshooting
Урок 09.04 · 22 мин
Продвинутый
EndpointsEndpointSlicesCoreDNSDNSTopology aware routingresolv.conf

Endpoints, EndpointSlices и DNS

Service — это абстракция, но за ней живут конкретные Pod-ы. Чтобы kube-proxy знал, куда DNAT-ить, ему нужны актуальные endpoints — список IP:port всех Ready Pods, матчинг Service selector. Эти данные хранятся в API server в специальных объектах.

Поверх endpoints живёт DNS: CoreDNS превращает имена Service-ов в их ClusterIP, чтобы Pods могли писать на читаемые адреса. В этом уроке — как устроены оба слоя и как они эволюционировали для больших кластеров.


DNS-резолюция: путь от example.com до IP

Endpoints (legacy)

Старая API: kind: Endpoints, group v1 (core). Создаётся автоматически EndpointController в kube-controller-manager для каждого Service со selector. Имя совпадает с именем Service.

apiVersion: v1
kind: Endpoints
metadata:
  name: web                  # имя = имя Service
  namespace: default
subsets:
  - addresses:
      - ip: 10.244.1.5
        targetRef:
          kind: Pod
          name: web-abc12
      - ip: 10.244.2.7
        targetRef:
          kind: Pod
          name: web-def34
      - ip: 10.244.3.9
        targetRef:
          kind: Pod
          name: web-xyz56
    ports:
      - port: 8080
        protocol: TCP

Все Ready Pods Service-а в одном объекте. Это одно поле субсетов для всех endpoints.

$ kubectl get endpoints web
NAME   ENDPOINTS                                          AGE
web    10.244.1.5:8080,10.244.2.7:8080,10.244.3.9:8080   5m

Проблема Endpoints на больших Services

Представьте Service с 5000 Pods (большой Deployment). Все они в одном Endpoints-объекте. При каждом изменении (один Pod рестартует, один Pod становится Ready):

  1. EndpointController пересчитывает Endpoints.
  2. Целый объект записывается обратно в etcd.
  3. Все наблюдатели (kube-proxy на каждой из, скажем, 100 нод) получают полный список заново через watch.
  4. Каждая нода пересобирает правила в iptables.

5000 endpoints × 100 нод = огромный burst трафика и CPU. На больших кластерах эта схема не работает.

WARNING

В Endpoints v1 был жёсткий лимит — 1000 endpoints на объект. Если их больше, kube-controller-manager обрезал список. Это была одна из причин разработки EndpointSlices.


EndpointSlices: разбиение на слайсы

С v1.16 (beta) и v1.21 (GA, default) появился новый API: EndpointSlices, группа discovery.k8s.io/v1.

Главная идея: вместо одного огромного объекта — несколько маленьких (“slices”) по умолчанию по 100 endpoints в каждом. У каждого slice свой watch stream.

apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: web-abc12          # автогенерированное имя
  labels:
    kubernetes.io/service-name: web    # ссылка на Service
addressType: IPv4
endpoints:
  - addresses: ["10.244.1.5"]
    conditions:
      ready: true
      serving: true
      terminating: false
    nodeName: node-1
    zone: us-east-1a       # ← topology hint
    targetRef:
      kind: Pod
      name: web-pod-1
  - addresses: ["10.244.2.7"]
    conditions:
      ready: true
    nodeName: node-2
    zone: us-east-1b
    targetRef:
      kind: Pod
      name: web-pod-2
ports:
  - name: http
    port: 8080
    protocol: TCP

При 250 endpoints — будут 3 EndpointSlice: 100, 100, 50. При изменении одного Pod — обновляется только тот slice, в котором он лежит. Watch-traffic снижается на порядок.

$ kubectl get endpointslices -l kubernetes.io/service-name=web
NAME            ADDRESSTYPE   PORTS   ENDPOINTS                                           AGE
web-abc12       IPv4          8080    10.244.1.5,10.244.2.7,10.244.3.9,...               5m
web-def34       IPv4          8080    10.244.4.1,10.244.4.2,10.244.4.3,...               5m

Дополнительные поля у EndpointSlice

EndpointSlice богаче Endpoints:

  • addressType: IPv4, IPv6, или FQDN.
  • conditions: ready, serving, terminating. Старый Endpoints различал только Ready/NotReady — а EndpointSlice добавляет понятие terminating (Pod в процессе graceful shutdown, ещё может обслуживать) и serving (готов принимать трафик).
  • zone: topology hint, AZ Pod-а. Используется topology-aware routing.
  • nodeName: имя ноды, на которой работает Pod.

kube-proxy в новых версиях слушает EndpointSlices, а не Endpoints (--feature-gates=EndpointSliceProxying=true, default true с v1.22).

NOTE

Endpoints всё ещё создаётся K8s для backward compat — old controllers и tools могут на него смотреть. Но “источник правды” — EndpointSlice. С v1.33 Endpoints API формально помечен как deprecated (но не удалён). На v1.35 ещё работает.


Topology aware routing

Cross-AZ трафик стоит денег у cloud-провайдеров. Если на ноде в AZ us-east-1a есть backend Pod того же Service — было бы хорошо предпочитать его, не гоняя трафик в us-east-1b.

С v1.27 (GA) это умеет topology aware routing. Включается аннотацией на Service:

apiVersion: v1
kind: Service
metadata:
  name: web
  annotations:
    service.kubernetes.io/topology-mode: "Auto"
spec:
  selector:
    app: web
  ports:
    - port: 80
      targetPort: 8080

Как это работает:

  1. EndpointSlice controller проставляет endpoints zone hints на основании Pod’s topology.kubernetes.io/zone label.
  2. В EndpointSlice появляется поле hints.forZones: [{name: us-east-1a}] для каждого endpoint.
  3. kube-proxy на ноде в us-east-1a фильтрует endpoints — берёт только те, что имеют hint forZones: us-east-1a.
  4. Если в этой зоне endpoints достаточно — балансит только между ними.
Topology aware routing: cross-AZ economy
Pod в us-east-1aКлиент пишет на Service. kube-proxy локально на ноде в AZ-1a.
curl http://web:80
kube-proxy (zone 1a)Видит EndpointSlice с hints. Фильтрует endpoints: берёт только те, у которых forZones содержит 1a.
prefer 1a endpoints
Pod backend (zone 1a)Локальный backend, без cross-AZ hop. Cloud-billing не зачисляет cross-AZ.
fallback если 1a недостаточно
Pod (zone 1b)Endpoints в других зонах используются если в локальной мало или их нет. Controller балансит hints чтобы избежать гoroch endpoints в одной зоне.

Topology mode "Auto" означает что controller сам решает hints на основании текущего распределения. Можно ставить и "Disabled" — выключить routing для конкретного Service.

TIP

Topology hints — heuristic, не жёсткое правило. Если в одной зоне endpoints мало, controller всё равно расширит hints на другие зоны, чтобы избежать overloading немногих backend-ов. Это балансирующая ось между in-zone предпочтением и равномерной нагрузкой.

spec.trafficDistribution (GA с v1.31) — современная замена аннотации

С v1.31 в Service.spec появилось нативное поле trafficDistribution, которое предпочтительнее старой аннотации service.kubernetes.io/topology-mode:

apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  trafficDistribution: PreferClose  # явное "предпочитай in-zone"
  selector:
    app: web
  ports:
    - port: 80
      targetPort: 8080

Значения:

  • PreferClose — предпочитать endpoints в той же топологической зоне; fallback на cross-zone при дефиците.
  • (поле не задано) — обычный random/round-robin без topology preference.

Разница с аннотацией: trafficDistribution — полноценное API-поле в spec, проще обнаруживается через kubectl explain service.spec.trafficDistribution и валидируется API server. На v1.35 для нового кода используйте trafficDistribution: PreferClose вместо service.kubernetes.io/topology-mode: Auto.


CoreDNS: DNS внутри кластера

В namespace kube-system живёт Deployment coredns (обычно 2-3 replicas), expose-нутый как Service kube-dns (имя историческое — старый kube-dns daemon заменили на CoreDNS в v1.11). ClusterIP этого Service-а прописывается в каждый Pod как nameserver.

resolv.conf в Pod-е

kubelet генерирует /etc/resolv.conf для каждого Pod-а:

nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

Разбор:

  • nameserver — ClusterIP Service kube-dns.
  • search — search-domains для resolution коротких имён.
  • options ndots:5 — если в имени меньше 5 точек, резолвер сначала пробует имя с каждым из search domains, и только если все не нашли — пробует как absolute (FQDN).

Search domains в действии

Pod пишет curl http://web:80. Что делает резолвер:

  1. web — 0 точек, меньше 5 → пробуем search domains:
  2. web.default.svc.cluster.local → CoreDNS → 10.96.0.10.
  3. (если не нашёл) web.svc.cluster.local → …
  4. (если не нашёл) web.cluster.local → …
  5. (если не нашёл) web absolute → external DNS.

Это даёт удобство: внутри namespace можно писать short names.

# Cross-namespace — нужен полный путь:
curl http://api.production.svc.cluster.local
# или короче (в одном кластере):
curl http://api.production
# второй вариант: search domain дойдёт до `.svc.cluster.local`
WARNING

ndots:5 — частый источник проблем. Для внешних доменов (например, api.github.com — 2 точки) резолвер сначала пробует ВСЕ search domains: api.github.com.default.svc.cluster.local, api.github.com.svc.cluster.local, api.github.com.cluster.local, и только потом настоящий internet DNS. 4 лишних DNS-запроса на каждый внешний хост. На high-throughput Pod-ах это даёт заметный CPU и latency на CoreDNS. Решение: задавать FQDN с trailing dot (api.github.com.) или менять ndots.


DNS-записи для Services

CoreDNS watch-ит Services и EndpointSlices, генерирует DNS-записи.

Обычный Service (ClusterIP)

A     web.default.svc.cluster.local   → 10.96.0.10
AAAA  web.default.svc.cluster.local   → (IPv6 ClusterIP если есть)
SRV   _http._tcp.web.default.svc.cluster.local   → 10 100 80 web.default.svc.cluster.local

A-запись возвращает ClusterIP, не Pod IPs. SRV (Service Record) описывает port и protocol — полезно для приложений типа Consul, которые умеют SRV для service discovery.

Headless Service

clusterIP: None — нет ClusterIP. CoreDNS возвращает A-записи на каждый Pod IP:

A   db.default.svc.cluster.local   → 10.244.1.5
                                      10.244.2.7
                                      10.244.3.9

Клиент получает все Pod IPs в response. Дальше его дело — балансить.

StatefulSet Pods

В headless service StatefulSet-а каждый Pod получает индивидуальное DNS-имя:

A   web-0.web.default.svc.cluster.local   → 10.244.1.5
A   web-1.web.default.svc.cluster.local   → 10.244.2.7
A   web-2.web.default.svc.cluster.local   → 10.244.3.9

Это позволяет писать на конкретный Pod (например, primary в database cluster), не зависая от его IP.

Pod IP records

Менее известно: CoreDNS создаёт A-записи для самих Pods:

A   10-244-1-5.default.pod.cluster.local   → 10.244.1.5

Имя — это PodIP с точками заменёнными на дефисы. Это для случаев, когда нужен stable hostname для Pod, но без Service.


CoreDNS Corefile

CoreDNS конфигурируется через Corefile в ConfigMap coredns в kube-system:

.:53 {
    errors
    health {
       lameduck 5s
    }
    ready
    kubernetes cluster.local in-addr.arpa ip6.arpa {
       pods insecure
       fallthrough in-addr.arpa ip6.arpa
       ttl 30
    }
    prometheus :9153
    forward . /etc/resolv.conf {
       max_concurrent 1000
    }
    cache 30
    loop
    reload
    loadbalance
}

Ключевые блоки:

  • kubernetes cluster.local — main plugin: резолвит все *.cluster.local через apiserver watch.
  • forward . /etc/resolv.conf — fallback для всех остальных доменов: проксирует на upstream DNS (берёт его из resolv.conf хоста).
  • cache 30 — кешировать ответы 30 секунд.
# Изменить Corefile (например, добавить stub для external домена):
kubectl edit configmap coredns -n kube-system

# Перезапустить CoreDNS чтобы перечитал:
kubectl rollout restart deployment coredns -n kube-system

dnsPolicy в Pod

У Pod-а есть поле spec.dnsPolicy, определяющее как kubelet генерирует resolv.conf:

  • ClusterFirst (default) — nameserver kube-dns ClusterIP, search domains кластера. CoreDNS форвардит non-cluster запросы на upstream.
  • Default — копирует resolv.conf хоста (ноды). Cluster-DNS не используется → нельзя резолвить Service имена.
  • ClusterFirstWithHostNet — для hostNetwork: true Pods. Используй cluster DNS даже хотя Pod в hostnetns.
  • None — никакого default. Должны прописать свой spec.dnsConfig ручками.
spec:
  dnsPolicy: None
  dnsConfig:
    nameservers:
      - 8.8.8.8
    searches:
      - my.custom.local
    options:
      - name: ndots
        value: "2"

Это редкая, но иногда нужная штука: например, если нужен только external DNS, не cluster.


hostAliases: /etc/hosts override

Если нужно прописать конкретное имя → IP в Pod (для тестов, mock-ов), есть hostAliases:

spec:
  hostAliases:
    - ip: "10.1.2.3"
      hostnames:
        - "legacy-api.local"
        - "internal-api"

Эти записи добавятся в /etc/hosts Pod-а. Резолвер пытается их до DNS (стандартный nsswitch).


DNS troubleshooting

Главная мантра при сетевых проблемах: проверь DNS первым.

# Запустить utility-Pod для отладки
kubectl run -i --tty --rm debug --image=nicolaka/netshoot -- bash

# Внутри:
nslookup web                           # короткое имя
nslookup web.default.svc.cluster.local # full FQDN
dig web.default.svc.cluster.local
dig +trace web.default.svc.cluster.local

# Проверить /etc/resolv.conf
cat /etc/resolv.conf

# Проверить health CoreDNS
kubectl get pods -n kube-system -l k8s-app=kube-dns
kubectl logs -n kube-system deployment/coredns

Типичные проблемы:

  • CoreDNS Pod-ы не Ready → DNS лежит, все Service queries timeout.
  • Неправильный ndots → лишние queries, latency.
  • NetworkPolicy блокирует Pod → CoreDNS port 53.
  • dnsPolicy: Default в Pod → не резолвятся Service имена.

Проверка знанийKnowledge check
Чем EndpointSlices (discovery.k8s.io/v1) принципиально отличаются от старого Endpoints (v1), и какие проблемы они решают на больших кластерах?
ОтветAnswer
Старый Endpoints хранит ВСЕ endpoints одного Service в одном объекте. На больших Services (1000+ Pods) это вызывает три проблемы: (1) жёсткий лимит на размер объекта (1000 endpoints — лишние обрезались); (2) при изменении одного endpoint весь объект перезаписывается в etcd; (3) каждый kube-proxy на каждой ноде получает полный список через watch — burst трафика и CPU при rolling update. EndpointSlices разбивают endpoints на **слайсы по 100 в каждом** — при изменении обновляется только тот slice, в котором лежит endpoint. Watch-traffic снижается на порядок. Плюс EndpointSlice богаче полями: `addressType` (IPv4/v6/FQDN), `conditions` с granular `ready/serving/terminating`, `zone` для topology-aware routing, `nodeName`. С v1.21 EndpointSlices — default, kube-proxy слушает их. Endpoints оставлен для backward compat, deprecated с v1.33.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 5. Почему EndpointSlices (discovery.k8s.io/v1) лучше масштабируются, чем старый Endpoints API?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5