Привет, Хабр!
Сегодня рассмотрим, как и почему в Kubernetes может сломаться service discovery — даже когда DNS вроде бы работает.
Что мы вообще называем service discovery
Под Kubernetes — это три независимых слоя:
API-объекты (
Service
,Endpoints
/EndpointSlice
), где хранится «истина» о том, какой Pod где живёт.Прокси-плоскость (kube-proxy или eBPF-проксирование), раздающая IP-VS/iptables правила.
DNS-плоскость (CoreDNS +
/etc/resolv.conf
внутри Pod’ов), которая лишь превращаетmy-svc.default.svc.cluster.local
в10.96.42.17
.
Сломать можно любой из трёх слоёв — и вот почему «просто перезапустите CoreDNS» не обязательно поможет.
Короткий счастливый путь
flowchart TD
A[Client Pod] -->|TCP/UDP 53| B(CoreDNS)
B -->|API: get EndpointsSlice| C[apiserver]
C --> D[etcd]
C -->|watch| B
A -->|10.96.42.17:8080| E[kube-proxy rule]
E --> F[Backend Pod]
Когда всё здорово:
EndpointsSlice
обновляется < 100 мс после масштабирования Deployment.kube-proxy успвает вставить IPVS-entry до того, как придёт первый трафик.
В
resolv.conf
стоитndots:5
, и лишние четыре запроса CoreDNS выдерживает без ноострих.
Но мы живём в ином мире.
Где ломается слой API
EndpointSlice теряет Pod
EndpointsSlice
застревает в статусе stale
по-прежнему ловят кластеры -> 3000 Service’ов при апгрейде 1.29 -> 1.31. Симптом: kubectl get ep
пустой, а Pods живы, DNS отдаёт NXDOMAIN
. CoreDNS тут ни при чём – ему просто нечего отдавать.
Как лечить
kubectl patch endpointslice -n default my-svc-abc \
--type=json -p='[{"op":"replace","path":"/metadata/finalizers","value":null}]'
Руками выбиваем finalizer, потом kube-controller-manager
пересоздаёт актуальный slice.
Большие списки Endpoints > медленный apiserver
До 1.28 один сервис с 10 000 Pods означал эндпоинт-объект на ≈ 5 МБ. Чтение такого ConfigMap
убивает cache-layer apiserver — все kubectl get для CPU-bound контроллеров подпадают под throttle. EndpointSlice
частично решает проблему, дробя список на куски по 100 адресов. Но если CRI-runtime медленно умирает, старые Podы могут провисеть по 30 минут.
DNS-плоскость: CoreDNS не панацея
Нагрузка на CoreDNS из-за ndots:5
Каждый lookup без точек приводит к 5 последовательным запросам. Для микросервиса, делающего 1 000 RPS и вызывающего три внуренних сервиса по коротким именам, это лишние 15 000 QPS на CoreDNS.
Патчим kubelet:
# /etc/default/kubelet
KUBELET_EXTRA_ARGS="--resolv-conf=/run/systemd/resolve/resolv.conf"
или per-Pod:
dnsConfig:
options:
- name: ndots
value: "1"
DNS-кэш на каждой ноде
NodeLocal DNSCache DaemonSet шорт-катит 53-й порт к 169.254.20.10
на узле, кешируя успешные и отрицательные ответы. На кластере с 8k Podов это даёт 6× меньше median-latency CoreDNS-пода.
Установка в два шага:
kubectl apply -f https://k8s.io/examples/admin/nodelocaldns/nodelocaldns.yaml
kubectl -n kube-system set env ds/nodelocaldns \
LOCAL_DNS=169.254.20.10 \
PROMETHEUS_PORT=9253
Когда DNS вовсе не виноват
NetworkPolicy режет egress -> 10.96.0.10:53. Отладка: kubectl run --rm -it debug --image=busybox --restart=Never -- nslookup kubernetes.default
. Если timeout
, смотрим kc get netpol
.
В Podе сделан chroot
-> /etc/resolv.conf
превратился в пустой файл. Go-приложение падает на no such host
.
kube-proxy и сетевой слой
iptables-режим и xtables-lock
На узлах с AWS VPC-CNI lock contention доходит до 30 с на одну операцию iptables, kube-proxy выставляет netfilter
chain с задержкой — а запросы уже летят и теряются.
Лечим — IPVS-режим или eBPF вместо iptables:
kube-proxy --proxy-mode=ipvs \
--ipvs-scheduler=rr --ipvs-sync-period=5s
IPVS держит state-table в /var/lib/ipvs и не блокируется на файловой системе.
Когда классический DNS — уже вчерашний день
Классический DNS-слой в Kubernetes неплохо отрабатывает среднюю температуру по палате, но как только кластер раздувается до тысяч Podов, требования к p99-латентности ужимаются до однозначных миллисекунд, а SLA грозят штрафами, старая схема «CoreDNS → kube-proxy → Pod» перестаёт быть серебряной пулей. Service-объекту приходится одновременно играть роли балансировщика, компонента безопасности и источника discovery, и каждая из этих задач в крайних случаях тянет одеяло на себя, оставляя другие без ресурсов.
Первый очевидный ход — Service Mesh в side-car-режиме (старые версии Istio, Linkerd). Envoy перехватывает трафик на уровне Podа, кеширует DNS-ответы локально и развешивает ретраи с экспонентой, спасая приложение от внезапных 503. Цена вопроса — лишние 30–50 МБ RSS на Pod и плюс пара миллисекунд к p95-латентности, что для high-freq API уже больно. Ради устранения overhead в Istio придумали Ambient Mesh: вместо сотни side-car’ов на узле живёт один ztunnel
, который шифрует и маршрутизирует L4-пакеты. CPU и память падают почти вчетверо, но DNS-плоскость по-прежнему остаётся за CoreDNS — резолв имён случается раньше, чем трафик протекает через ztunnel
.
Если хочется радикального DNS-less-подхода, смотрим в сторону Cilium ClusterMesh или SRV-over-IP. Здесь карта сервисов хранится прямо в eBPF, а клиенты стучатся по IP, не подозревая о существовании CoreDNS. На сетевом уровне это значит zero-copy forwarding и минимальный jitter; на уровне безопасности — та же L7-Policy и mTLS, но уже без dance-floor из PTR/A-запросов. Минус ровно один: чтобы жить без DNS, придётся обучить ваши SDK или service-brokerы разговаривать с eBPF-API, иначе discovery вернётся на круги своя.
DNS в Kubernetes — всего лишь первая половина уравнения. Пока API не переписан, именно вы отвечаете за то, чтобы EndpointsSlice, kube-proxy и CoreDNS обновлялись быстрее, чем бизнес-логика успевает уронить продажи.
Всех заинтересовавшихся приглашаем посетить бесплатный открытый урок «Стили взаимодействия микросервисов: 5 секретов, которые изменят ваш подход к backend-разработке», который пройдет 11 июня. Записаться можно на странице курса "Microservice Architecture".
Также рекомендуем ознакомиться с календарем открытых уроков.