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 |
| Под в другом namespace | users-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 форвардит внешние имена на апстрим-резолвер; его падение ломает обнаружение по всему кластеру.