DNS и обнаружение сервисов

Как поды находят друг друга по имени, а не по эфемерному IP — и кто этим именам отвечает.

Service discovery в Kubernetes — это возможность обратиться к другому сервису по стабильному DNS-имени; за разрешение имён в кластере отвечает CoreDNS.

IP-адреса подов эфемерны: под пересоздали — адрес сменился. Жёстко прописывать их в конфигах нельзя. Service даёт стабильный виртуальный IP (ClusterIP), но и его помнить наизусть неудобно. Решение — DNS: каждому сервису автоматически назначается имя, и приложение ходит к http://users-svc вместо http://10.96.0.42.

CoreDNS — DNS-сервер кластера

За имена в кластере отвечает CoreDNS — пара подов в namespace kube-system, за которыми стоит свой Service (обычно ClusterIP 10.96.0.10). Когда kubelet запускает контейнер, он прописывает этот адрес в /etc/resolv.conf пода как nameserver. Поэтому любой DNS-запрос изнутри пода уходит в CoreDNS, а тот знает про все Service кластера и отдаёт их ClusterIP.

kubectl get pods -n kube-system -l k8s-app=kube-dns
kubectl get svc -n kube-system kube-dns

Как устроены имена: service.namespace.svc.cluster.local

Полное (FQDN) имя сервиса собирается по шаблону <service>.<namespace>.svc.cluster.local. Например, сервис users-svc в namespace shop имеет FQDN users-svc.shop.svc.cluster.local. Разберём части: users-svc — имя Service; shop — namespace; svc — что это именно сервис (а не под); cluster.local — доменный суффикс кластера.

Писать FQDN целиком приходится редко благодаря search-доменам в /etc/resolv.conf. kubelet прописывает туда строку вроде search shop.svc.cluster.local svc.cluster.local cluster.local. Поэтому из пода в namespace shop работают короткие формы:

Откуда зовёмКак обратиться к users-svc в shop
Под в том же namespace (shop)users-svc
Под в другом namespaceusers-svc.shop
Откуда угодно, без неоднозначностиusers-svc.shop.svc.cluster.local

Отсюда практическое правило: внутри одного namespace зовите сервис коротким именем, между namespace — добавляйте .<namespace>.

Кроме A-записей (имя → IP) CoreDNS отдаёт и SRV-записи для именованных портов сервиса. Если в Service порт назван, скажем, http, то запись _http._tcp.users-svc.shop.svc.cluster.local вернёт и номер порта, и хост — удобно, когда клиент не хочет хардкодить номер порта. На практике этим пользуются реже A-записей, но знать про существование полезно: это часть того же автоматического DNS-механизма. Сами поды по умолчанию A-записей по имени не получают (адресуют их через Service), а вот headless-сервис, как увидим ниже, такие записи подам как раз создаёт.

Headless service: DNS без виртуального IP

Обычный Service даёт один ClusterIP и балансирует запросы по подам. Но иногда нужно адресовать конкретные поды — например, у баз данных и кластеров с лидером и репликами. Для этого есть headless service: у него clusterIP: None. Виртуального IP нет — вместо одной A-записи DNS отдаёт адреса всех подов сервиса.

apiVersion: v1
kind: Service
metadata:
  name: db
spec:
  clusterIP: None
  selector:
    app: db
  ports:
    - port: 5432

Запрос db вернёт список IP всех подов под этим селектором. А в связке со StatefulSet каждый под получает ещё и собственное стабильное имя: db-0.db.shop.svc.cluster.local, db-1.db... — так можно постучаться именно в нулевую реплику.

Когда что-то не резолвится, удобнее всего проверить DNS изнутри кластера: запускают временный под с утилитами и руками спрашивают CoreDNS. Так сразу видно, отвечает ли DNS вообще и какой адрес отдаёт:

kubectl run dnstest --rm -it --image=busybox:1.36 --restart=Never -- \
  nslookup users-svc.shop.svc.cluster.local

Если короткое имя не находится, а полный FQDN находится — проблема в search-доменах или в том, что вы зовёте из чужого namespace. Если не находится даже FQDN — стоит проверить, жив ли CoreDNS и совпадает ли имя Service с тем, что вы спрашиваете.

Как это работает под капотом

CoreDNS — это плагинный DNS-сервер; его поведение задаёт конфиг Corefile. Главный плагин — kubernetes: он подписан на API-сервер и держит в памяти карту «имя Service → ClusterIP» и «имя → Endpoints». Запрос из пода приходит на ClusterIP CoreDNS, плагин смотрит в эту карту и отдаёт ответ; для обычного Service — его ClusterIP, для headless — адреса подов из Endpoints. Имена вне кластера (например, github.com) CoreDNS форвардит на внешний резолвер плагином forward. Так одна и та же цепочка обслуживает и внутренние сервисы, и выход в интернет.

Частые ошибки

Самая частая — обращаться к сервису в другом namespace коротким именем: users-svc из namespace payments не найдётся, нужно users-svc.payments-namespace-сервиса, то есть users-svc.shop. Вторая — путать имя Service с именем Deployment/пода: DNS-имя берётся от Service, а не от Deployment. Третья — ждать DNS-имя от headless-сервиса, забыв, что у него нет ClusterIP, и потом удивляться, что приложение получает сразу несколько адресов. Ещё одна — проблемы с задержками DNS из-за агрессивного ndots:5 в resolv.conf: короткое имя пробуется по всем search-доменам, давая лишние запросы; лечится FQDN с точкой на конце или настройкой DNS-политики пода. И помните: если CoreDNS лежит, по именам перестаёт работать вообще всё — это критичный компонент.

Итоги

  • Поды находят друг друга по DNS-именам сервисов, а не по эфемерным IP; имена резолвит CoreDNS.
  • FQDN сервиса: service.namespace.svc.cluster.local; внутри namespace хватает короткого имени, между namespace — service.namespace.
  • CoreDNS живёт в kube-system, его ClusterIP прописан в resolv.conf каждого пода как nameserver.
  • Headless service (clusterIP: None) не даёт виртуального IP — DNS отдаёт адреса всех подов; со StatefulSet у каждого пода своё стабильное имя.
  • CoreDNS форвардит внешние имена на апстрим-резолвер; его падение ломает обнаружение по всему кластеру.
Проверьте себя
1. Какое DNS-имя гарантированно разрешит Service users-svc из namespace shop при обращении из пода в другом namespace?
Ausers-svc
Busers-svc.shop
Cshop.users-svc
Dusers-svc.cluster.local
2. Чем headless service (clusterIP: None) отличается от обычного Service в плане DNS?
AОн вообще не регистрируется в CoreDNS
BDNS отдаёт не один виртуальный ClusterIP, а список адресов всех подов сервиса
CОн работает только для внешнего трафика через Ingress
DОн даёт каждому поду публичный IP