Привет, Хабр!

Сегодня рассмотрим, как и почему в Kubernetes может сломаться service discovery — даже когда DNS вроде бы работает.

Что мы вообще называем service discovery

Под Kubernetes — это три независимых слоя:

  1. API-объекты (Service, Endpoints/EndpointSlice), где хранится «истина» о том, какой Pod где живёт.

  2. Прокси-плоскость (kube-proxy или eBPF-проксирование), раздающая IP-VS/iptables правила.

  3. 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".

Также рекомендуем ознакомиться с календарем открытых уроков.

Комментарии (0)