Headless-сервис в Kubernetes: когда нужен clusterIP: None | AdminWiki

Headless-сервис в Kubernetes: когда нужен clusterIP: None

30 сентября 2026 10 мин. чтения

Что такое 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

ПараметрClusterIPHeadless (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

ПараметрClusterIPHeadless (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-сервис

  1. Нагрузка управляется StatefulSet и требует стабильных сетевых идентификаторов подов.
  2. Приложению нужны FQDN отдельных экземпляров, а не общее имя сервиса.
  3. Есть peer-to-peer обнаружение или выбор лидера: etcd, ZooKeeper, Kafka, Consul.
  4. Клиент умеет читать список адресов или SRV-записи и сам выбирает узел.
  5. Балансировка на уровне Kubernetes не нужна, либо она реализована в клиенте.
  6. Готовы предусмотреть переподключение и повторный резолв DNS для долгоживущих соединений.
  7. Проверьте, не поможет ли вместо этого spec.internalTrafficPolicy: Local оставить трафик внутри узла и снизить задержку.

Если сомневаетесь, начните с обычного ClusterIP: он закрывает большинство задач и не требует кода выбора пода. Headless добавляйте тогда, когда появилась конкретная потребность адресовать отдельный экземпляр, и сразу проверяйте результат через kubectl get endpointslice, dig и nslookup, чтобы DNS-ответы совпадали с ожидаемым списком подов.

Поделиться: