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):
- EndpointController пересчитывает Endpoints.
- Целый объект записывается обратно в etcd.
- Все наблюдатели (kube-proxy на каждой из, скажем, 100 нод) получают полный список заново через watch.
- Каждая нода пересобирает правила в iptables.
5000 endpoints × 100 нод = огромный burst трафика и CPU. На больших кластерах эта схема не работает.
В 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).
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
Как это работает:
- EndpointSlice controller проставляет endpoints zone hints на основании Pod’s
topology.kubernetes.io/zonelabel. - В EndpointSlice появляется поле
hints.forZones: [{name: us-east-1a}]для каждого endpoint. - kube-proxy на ноде в
us-east-1aфильтрует endpoints — берёт только те, что имеют hintforZones: us-east-1a. - Если в этой зоне endpoints достаточно — балансит только между ними.
Topology mode "Auto" означает что controller сам решает hints на основании текущего распределения. Можно ставить и "Disabled" — выключить routing для конкретного Service.
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 Servicekube-dns.search— search-domains для resolution коротких имён.options ndots:5— если в имени меньше 5 точек, резолвер сначала пробует имя с каждым из search domains, и только если все не нашли — пробует как absolute (FQDN).
Search domains в действии
Pod пишет curl http://web:80. Что делает резолвер:
web— 0 точек, меньше 5 → пробуем search domains:web.default.svc.cluster.local→ CoreDNS →10.96.0.10.- (если не нашёл)
web.svc.cluster.local→ … - (если не нашёл)
web.cluster.local→ … - (если не нашёл)
webabsolute → external DNS.
Это даёт удобство: внутри namespace можно писать short names.
# Cross-namespace — нужен полный путь:
curl http://api.production.svc.cluster.local
# или короче (в одном кластере):
curl http://api.production
# второй вариант: search domain дойдёт до `.svc.cluster.local`
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: truePods. Используй 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 имена.