Что такое headless-сервис и зачем нужен clusterIP: None
Headless-сервис - это Service, у которого в спецификации указано clusterIP: None. Виртуальный адрес из service CIDR такому сервису не выделяется, селектор и порты продолжают работать, а имя в кластерном DNS начинает резолвиться в IP-адреса подов. Kubernetes не проксирует трафик: клиент получает список адресов и сам решает, с кем соединяться.
Разница проявляется на сетевом уровне. Для обычного сервиса kube-proxy пишет правила iptables или конфигурацию IPVS, и пакет на виртуальный IP уходит на один из подов. Для headless-сервиса cluster IP не выделяется, kube-proxy такие сервисы не обрабатывает, и платформа не выполняет для них балансировку или проксирование. Обнаружение берёт на себя DNS, а доставку до конкретного экземпляра обеспечивает клиент.
Прямой ответ на главный вопрос: clusterIP: None нужен там, где приложению важно видеть отдельные экземпляры, а не абстрактный сервис за одним адресом. StatefulSet, кластеры PostgreSQL и MongoDB, etcd, ZooKeeper и Kafka строятся именно на таком режиме. Headless - это режим ClusterIP, отдельного типа Service в API Kubernetes нет. Как устроены остальные типы, разбираем в руководстве по выбору типа Service.
Рабочая аналогия: обычный ClusterIP ведёт себя как виртуальный балансировщик перед группой подов, headless-сервис - как выданная клиенту адресная книга с именами. Первый скрывает топологию, второй её раскрывает, и это раскрытие бывает нужно самому приложению.
Как DNS работает в headless-режиме
Для headless-сервиса кластерный DNS (в большинстве установок это CoreDNS) создаёт A-записи на каждый под, попавший в EndpointSlice, то есть на каждый экземпляр, готовый принимать трафик. Имя сервиса резолвится не в один адрес, а в список адресов подов.
Второй слой - стабильные имена экземпляров. Если под управляется StatefulSet, ему задаются hostname и subdomain, поэтому для каждого пода появляется FQDN вида pod-name.service-name.namespace.svc.cluster.local. Для кластера PostgreSQL это postgres-0.postgres-headless.default.svc.cluster.local, postgres-1.postgres-headless.default.svc.cluster.local и так далее. Имя переживает перезапуск контейнера, а пересоздание пода с тем же порядковым номером возвращает тот же FQDN.
Именованные порты добавляют SRV-записи. Запрос _postgres._tcp.postgres-headless.default.svc.cluster.local отдаёт hostname каждого пода и номер порта; без поля name в описании порта SRV-записи для сервиса не появятся.
$ dig +short postgres-headless.default.svc.cluster.local 10.244.1.7 10.244.2.12 10.244.3.9
$ nslookup postgres-1.postgres-headless.default.svc.cluster.local Server: 10.96.0.10 Address: 10.96.0.10:53 Name: postgres-1.postgres-headless.default.svc.cluster.local Address: 10.244.2.12
Порядок адресов в ответе не гарантирован: CoreDNS может их перемешивать, а кэши резолвера и самого приложения держат старые записи после пересоздания подов. Список меняется, клиентская логика обязана это учитывать.
Чем headless-сервис отличается от обычного ClusterIP
| Параметр | ClusterIP | Headless (clusterIP: None) |
|---|---|---|
| Виртуальный IP | Выделяется из service CIDR | Не выделяется |
| Ответ DNS на имя сервиса | Один адрес, виртуальный IP | Список A-записей подов |
| Участие kube-proxy | Пишет правила iptables или IPVS | Сервис не обрабатывает |
| Балансировка | На уровне сервиса, под выбирается случайно | Отсутствует, выбор за клиентом |
| Имена подов | Не используются | FQDN на каждый под StatefulSet |
| Типовое применение | Stateless-сервисы, API, фронтенды | StatefulSet, БД, кворумные системы |
Ключевая разница: headless не балансирует трафик. Если нужен равномерный разброс запросов без клиентской логики, берите ClusterIP. Headless выбирают ради адресов конкретных экземпляров.
Когда headless-сервис действительно необходим
Критерий один: приложению нужны конкретные экземпляры, а не один общий вход. Практически это выражается в четырёх ситуациях.
- StatefulSet. Каждый под получает предсказуемый сетевой идентификатор, и это условие существования контроллера: StatefulSet требует headless-сервис, отвечающий за сетевую идентичность подов, а создание этого сервиса - ответственность пользователя. Поле serviceName должно ссылаться на этот headless-сервис (clusterIP: None), который управляет DNS и создаёт записи для отдельных подов, чтобы клиенты могли обращаться к database-0.database, а не к единому балансируемому виртуальному IP. Если serviceName указывает на несуществующий сервис или на сервис, чей селектор не совпадает с метками подов, DNS-записи подов не будут резолвиться как ожидается. Про устройство StatefulSet читайте в сравнении контроллеров Kubernetes.
- Базы данных. PostgreSQL с мастером и репликами, MongoDB с реплика-сетом, Cassandra с токен-кольцом: узлы должны знать друг друга по именам, а приложение - уметь обратиться к конкретному узлу.
- Peer-to-peer и кворум. etcd, ZooKeeper, Kafka, Consul обмениваются трафиком между всеми участниками и выбирают лидера, поэтому каждому узлу нужен собственный стабильный адрес для объявления его соседям.
- Клиенты, читающие SRV. Часть драйверов и библиотек (например, клиенты Cassandra и Consul) сами опрашивают DNS и распределяют соединения по полученному списку.
Для stateless-нагрузок headless обычно лишний. Фронтенд на nginx, пул API-подов за одним именем, воркеры очереди не выигрывают от списка адресов, зато требуют клиентской логики выбора и обработки отказов.
Быстрый чек-лист на этапе проектирования: приложению нужно адресовать конкретный экземпляр - headless; приложению достаточно абстрактного входа - ClusterIP; нужны оба поведения - заводите два сервиса с одинаковым селектором, обычный и headless.
Практические сценарии: StatefulSet, базы данных и peer-to-peer
StatefulSet. Поды postgres-0, postgres-1 и postgres-2 получают FQDN вида postgres-N.postgres-headless.default.svc.cluster.local. Клиент подключается к конкретному узлу и после пересоздания пода попадает на тот же идентификатор. ClusterIP в этой роли неудобен: он скрывает, к какому экземпляру ушло соединение, а мастер и реплика требуют разного обращения.
Базы данных. В схеме с мастером и репликами запись идёт на постgres-0, чтение - на остальные адреса из DNS-ответа. В MongoDB реплика-сет собирается так: каждый узел знает своё имя и адреса соседей, выбирает primary, а драйвер получает список участников и подключается к нужному. Имя мастера меняется при failover, поэтому клиент должен перечитывать DNS и переподключаться, а не держать один адрес в конфиге навсегда.
Peer-to-peer. etcd, ZooKeeper и Kafka используют headless-сервис для внутреннего обмена: узлы находят друг друга по стабильным именам, голосуют за лидера, синхронизируют журналы. Тут важно, чтобы адрес участника не менялся при перезапуске, иначе кворум распадётся. Если строите отказоустойчивый stateful-контур вне Kubernetes, пригодятся подходы из обзора решений для кластеризации.
Общий знаменатель всех трёх сценариев: приложению нужен сетевой идентификатор экземпляра, а не абстрактный сервис. Именно эту задачу решает clusterIP: None.
Как настроить headless-сервис: манифесты и команды проверки
Манифест headless-сервиса отличается от обычного одной строкой. Порты именуйте: это включает SRV-записи и упрощает диагностику.
apiVersion: v1
kind: Service
metadata:
name: postgres-headless
labels:
app: postgres
spec:
clusterIP: None
publishNotReadyAddresses: true
selector:
app: postgres
ports:
- name: postgres
port: 5432
targetPort: 5432
StatefulSet ссылается на этот сервис через serviceName. Имя в serviceName должно в точности совпадать с именем headless-сервиса, иначе поды не получат FQDN вида pod-name.service-name.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres-headless
replicas: 3
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16
ports:
- name: postgres
containerPort: 5432
Флаг publishNotReadyAddresses: true для управляющего сервиса StatefulSet полезен тем, что адреса подов попадают в DNS до прохождения readinessProbe. По умолчанию DNS сервиса публикует только готовые эндпоинты, а приложениям, которым нужно обнаружение пиров до прохождения готовности, может потребоваться publishNotReadyAddresses на headless-сервисе; это поле предназначено в первую очередь для обнаружения пиров StatefulSet через headless-сервисы. Участники кластера видят друг друга на этапе инициализации, а не после того, как все одновременно станут готовы. Учитывайте и известное ограничение: в одном из issue проекта Kubernetes описана задержка - адрес пода не публикуется во внутреннем Kube-DNS как минимум 30 секунд после создания пода, даже если у headless-сервиса явно задан publishNotReadyAddresses.
Проверка начинается с самого сервиса: убедитесь, что CLUSTER-IP равен None, а адреса подов попали в EndpointSlice.
kubectl get svc postgres-headless -o wide NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE postgres-headless ClusterIP None 5432/TCP 5m kubectl get endpointslice -l kubernetes.io/service-name=postgres-headless NAME ADDRESSTYPE PORTS ENDPOINTS AGE postgres-headless-abc12 IPv4 5432 10.244.1.7,10.244.2.12,10.244.3.9 5m
Дальше проверяем DNS. Удобно запустить временный под с утилитами или отладочный контейнер рядом с рабочим подом.
kubectl run dnstest --rm -it --restart=Never --image=busybox:1.36 -- \ sh -c 'nslookup postgres-headless; nslookup postgres-0.postgres-headless' kubectl debug -it postgres-0 --image=busybox:1.36 --target=postgres -- \ nslookup postgres-1.postgres-headless.default.svc.cluster.local
Типичные причины пустого ответа: в сервисе нет селектора или он не совпадает с метками подов; поды не проходят проверку готовности, а publishNotReadyAddresses не включён; targetPort не совпадает с портом контейнера. Во всех этих случаях EndpointSlice остаётся пустым, и DNS отвечает NXDOMAIN. Ещё один источник расхождений - кэш CoreDNS и клиентский резолвер: после изменения состава подов ответ обновляется не мгновенно.
Ограничения и подводные камни headless-сервисов
Главное ограничение: балансировки нет. Приложение само решает, к какому адресу из списка подключаться, само отслеживает отказы и само переподключается. Если такой логики нет, headless создаёт проблему там, где ClusterIP её не создавал.
- Долгоживущие соединения. gRPC и HTTP keep-alive держат канал с одним подом, поэтому новые экземпляры не получают трафик, а ушедший оставляет клиента с мёртвым соединением. Нужны переподключение, health-check и повторный резолв имени.
- DNS-кэш и ndots. Кэшируют CoreDNS, системный резолвер и рантайм приложения. Плюс в Kubernetes параметры ndots и search domains в /etc/resolv.conf часто замедляют внешние домены: при ndots и нескольких search domains резолвер перебирает суффиксы, делает A/AAAA-запросы, ловит SERVFAIL и таймауты, а таймаут может произойти ещё до того, как резолвер попробует «правильное» имя. Точные FQDN с завершающей точкой помогают сократить лишние попытки.
- Нестабильные имена вне StatefulSet. У подов Deployment нет порядковых номеров, поэтому имена вида pod-name.service-name не годятся как идентификаторы: при пересоздании под получит новое имя.
- Внешний доступ. Headless-режим рассчитан на внутренний трафик: ClusterIP такому сервису не выделяется, и DNS возвращает A-записи подов напрямую. Прямого внешнего входа у headless-сервиса нет; чтобы открыть поды наружу, сервис нужно опубликовать или добавить Ingress.
Смежная настройка, которую часто путают с headless-режимом, - поле spec.internalTrafficPolicy. Значение Local ограничивает внутренний трафик эндпоинтами на том же узле, откуда пришёл запрос, и это помогает снизить задержку и затраты на межзональные пересылки (Service Internal Traffic Policy). Значение по умолчанию Cluster и отсутствие настройки означают, что Kubernetes учитывает все эндпоинты сервиса.
Считайте риски заранее: если на узле нет эндпоинтов нужного сервиса, для подов этого узла сервис ведёт себя так, как будто эндпоинтов нет вообще, хотя на других узлах они есть. Включайте Local только там, где на каждом рабочем узле гарантированно есть локальная реплика. В headless-режиме адресную выдачу формирует DNS, и фильтрацию по узлу выполняет клиент, поэтому Local не заменяет клиентскую логику. Для промышленных кластеров с несколькими зонами пригодятся общие правила из руководства по проектированию кластера.
Сравнительная таблица: headless-сервис vs ClusterIP
| Параметр | ClusterIP | Headless (clusterIP: None) | Что выбирать |
|---|---|---|---|
| Виртуальный IP | Есть | Нет | Нужен стабильный вход для внешних клиентов - ClusterIP |
| Ответ DNS | Один A-запись с адресом сервиса | Список A-записей подов | Нужен перечень экземпляров - headless |
| kube-proxy | Правила iptables или IPVS | Сервис не обрабатывает | Headless обходит проксирование целиком |
| Балансировка | Распределение внутри кластера | Клиентская | Нет кода выбора пода - ClusterIP |
| Стабильные имена подов | Не предоставляет | FQDN на каждый под StatefulSet | Кластеры БД и кворум - headless |
| Поведение при отказе пода | Трафик уходит на оставшиеся поды | Клиент видит недоступный адрес | Нужна отказоустойчивость без клиентского кода - ClusterIP |
| Доступ извне кластера | Через NodePort, LoadBalancer, Ingress | Прямого входа нет, нужна публикация сервиса или Ingress | Публикация наружу - ClusterIP плюс Ingress |
Пример для наглядности: три реплики PostgreSQL за headless-сервисом позволяют клиенту обратиться точно к мастеру и отдельно к репликам для чтения. Тот же набор подов за ClusterIP разложит запросы случайно, и запись может уйти на реплику.
Чек-лист: когда выбирать headless-сервис
- Нагрузка управляется StatefulSet и требует стабильных сетевых идентификаторов подов.
- Приложению нужны FQDN отдельных экземпляров, а не общее имя сервиса.
- Есть peer-to-peer обнаружение или выбор лидера: etcd, ZooKeeper, Kafka, Consul.
- Клиент умеет читать список адресов или SRV-записи и сам выбирает узел.
- Балансировка на уровне Kubernetes не нужна, либо она реализована в клиенте.
- Готовы предусмотреть переподключение и повторный резолв DNS для долгоживущих соединений.
- Проверьте, не поможет ли вместо этого spec.internalTrafficPolicy: Local оставить трафик внутри узла и снизить задержку.
Если сомневаетесь, начните с обычного ClusterIP: он закрывает большинство задач и не требует кода выбора пода. Headless добавляйте тогда, когда появилась конкретная потребность адресовать отдельный экземпляр, и сразу проверяйте результат через kubectl get endpointslice, dig и nslookup, чтобы DNS-ответы совпадали с ожидаемым списком подов.