Service и порты

Уровень: beginner ~55 мин Практика: Linkding (AGPL-3.0)

Что нужно знать перед уроком

  • M1.02 — Deployment и ReplicaSet — нужно понимать label selector

Что нужно знать перед уроком

M1 урок 2 — Deployment создаёт и заменяет Pod’ы, у каждого свой IP-адрес, который меняется при пересоздании. Как же клиенту находить актуальный набор Pod’ов? Это и есть задача Service.

Теория

Зачем нужен Service

Pod’ы — эфемерны: пересоздаются при обновлении, масштабировании, сбое узла, каждый раз получая новый IP-адрес. Если клиент запомнит конкретный IP Pod’а — он рано или поздно обратится в никуда. Service — объект, который даёт стабильную точку доступа (виртуальный IP и/или DNS-имя) к динамически меняющемуся набору Pod’ов, определяемому через selector (по тому же принципу label selector, что и у Deployment/ReplicaSet, но независимо от них — Service не знает о существовании Deployment, он видит только Pod’ы, подходящие под свой selector).

Endpoints/EndpointSlice — механизм связи Service↔Pod

Service не хранит список Pod’ов напрямую — он декларирует selector, а отдельный контроллер непрерывно синхронизирует список подходящих IP:port в объекты Endpoints (устаревающий формат) или EndpointSlice (современный, масштабируемый на большие наборы Pod’ов за счёт разбиения на несколько объектов). Именно EndpointSlice/Endpoints, а не сам Service, содержит актуальный список адресов — Service — это, по сути, «указатель» плюс правило балансировки на уровне kube-proxy.

1
kubectl get endpointslices -l kubernetes.io/service-name=linkding

Четыре типа Service — каждый следующий технически расширяет предыдущий

ТипЧто делаетРасширяет
ClusterIP (по умолчанию)стабильный виртуальный IP, доступный только внутри кластера
NodePortто же, что ClusterIP, плюс статический порт (диапазон 30000–32767 по умолчанию), открытый на каждом узле кластераClusterIP
LoadBalancerто же, что NodePort, плюс запрос внешнего балансировщика у облачного провайдера/платформы, который направляет трафик на этот NodePortNodePort
ExternalNameне создаёт виртуальный IP вообще — просто CNAME-запись, перенаправляющая DNS-имя сервиса на внешний DNS-адресотдельный случай, без Endpoints
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
apiVersion: v1
kind: Service
metadata:
  name: linkding
spec:
  type: ClusterIP
  selector:
    app: linkding
  ports:
    - port: 80
      targetPort: 9090

port — порт, на котором Service доступен внутри кластера; targetPort — порт, который реально слушает контейнер. Они могут (и часто должны) отличаться.

trafficDistribution — предпочтение локальных Pod’ов

Начиная с Kubernetes 1.31 (beta), а с Kubernetes 1.35 — стабильно (GA), у Service есть поле spec.trafficDistribution, которое подсказывает kube-proxy, каким Pod’ам отдавать предпочтение при балансировке:

ЗначениеПоведение
(не задано)балансировка без явных предпочтений локальности — трафик может уйти на Pod в любой зоне/узле
PreferSameZoneпредпочитать Pod’ы в той же availability zone, что и узел-отправитель, с fallback на любые остальные (в версиях до 1.35 это же поведение называлось PreferClose — имя сохранено для обратной совместимости, но PreferSameZone — актуальное явное название)
PreferSameNodeпоявилось в Kubernetes 1.35 — строго предпочитать Pod’ы на том же узле, что и узел-отправитель, и только при их отсутствии переключаться на удалённые

Это не замена topologyKeys (устаревшего механизма, удалённого ранее) и не гарантия, а именно предпочтение — если подходящих локальных Pod’ов нет, трафик всё равно уйдёт на удалённые.

Headless Service: clusterIP: None

Обычный Service выдаёт один виртуальный IP, скрывающий за собой множество Pod’ов (балансировка на уровне kube-proxy, клиент не видит отдельные Pod’ы). Headless Service (spec.clusterIP: None) убирает этот виртуальный IP — вместо него DNS-запрос к имени сервиса возвращает A-записи сразу на все IP подходящих Pod’ов. Это нужно, когда клиенту важно самому видеть и выбирать конкретный Pod (классический случай — клиентские библиотеки БД, работающие со StatefulSet, где важно различать реплики).

Если у Pod’ов заданы subdomain и hostname — headless Service дополнительно создаёт именованные SRV-записи по каждому Pod’у отдельно (<hostname>.<subdomain>.<service>.<namespace>.svc.cluster.local), что используется StatefulSet для предсказуемого сетевого имени каждой реплики.

DNS и FQDN сервиса

Любой Service (кроме ExternalName, где это CNAME) получает полное доменное имя вида:

<service-name>.<namespace>.svc.cluster.local

Из того же namespace достаточно короткого имени (linkding), из другого namespace обязательно указывать хотя бы <service-name>.<namespace> — это разбирается подробно в разделе Linux/Сети ниже.

spec.externalIPs — deprecated с Kubernetes 1.36
Отдельно от четырёх основных типов Service существует малоизвестное поле spec.externalIPs — список произвольных внешних IP, на которые Kubernetes принимает трафик для этого Service, минуя LoadBalancer/NodePort. Начиная с Kubernetes 1.36, это поле официально deprecated (полное удаление запланировано на v1.43) — оно годами было источником проблем с безопасностью (в частности, CVE-2020-8554, MITM-атаки через перехват трафика на произвольный IP). Если нужен внешний доступ — используйте LoadBalancer (облачный/платформенный балансировщик), NodePort или Gateway API/Ingress, а не externalIPs.

Частые ошибки и подводные камни

  • Путать port и targetPort. Ошибка «Service отвечает, но всегда Connection Refused» почти всегда означает, что targetPort не совпадает с портом, который реально слушает контейнер.
  • Ожидать, что headless Service балансирует трафик как обычный. Headless Service не балансирует — клиент получает список всех IP и сам решает, к какому обращаться (или использует round-robin на уровне DNS-резолвера, что не то же самое, что kube-proxy).
  • Забыть про EndpointSlice/Endpoints при диагностике «Service не работает». Если selector не совпадает ни с одним Pod’ом (опечатка в лейбле) — Service будет существовать, но Endpoints окажется пустым; сам Service при этом не покажет явную ошибку.

Linux / Сети

Как Pod резолвит DNS

Внутри каждого Pod’а kubelet прописывает /etc/resolv.conf, указывающий на DNS-сервер кластера (обычно CoreDNS) и search-домены:

nameserver 10.96.0.10
search course-m1-service.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

Search-домены — причина, почему внутри своего namespace достаточно написать просто linkding (резолвер попробует linkding.course-m1-service.svc.cluster.local первым из списка search-доменов и найдёт запись), а обращаясь из другого namespace, нужно писать хотя бы linkding.<namespace> — короткое имя linkding там просто не резолвится ни в одну из ваших записей.

1
kubectl run debug --rm -it --image=busybox -n course-m1-service -- sh -c "cat /etc/resolv.conf; nslookup linkding"

Практика в кластере

🧩 Практика урока: Linkding · лицензия AGPL-3.0

Linkding — минималистичный self-hosted менеджер закладок на Python (Django) + SQLite внутри контейнера — чуть сложнее Homer за счёт того, что реально отвечает на HTTP-запросы динамическим контентом, удобно для проверки Service end-to-end.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
kubectl create namespace course-m1-service
kubectl apply -n course-m1-service -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: linkding
spec:
  replicas: 2
  selector:
    matchLabels: { app: linkding }
  template:
    metadata:
      labels: { app: linkding }
    spec:
      containers:
        - name: linkding
          image: sissbruecker/linkding:latest
          ports: [{ containerPort: 9090 }]
          resources:
            requests: { cpu: "100m", memory: "128Mi" }
            limits: { cpu: "300m", memory: "256Mi" }
---
apiVersion: v1
kind: Service
metadata:
  name: linkding
spec:
  selector: { app: linkding }
  ports:
    - port: 80
      targetPort: 9090
EOF
  1. Проверьте Service и его Endpoints:
1
2
kubectl get svc,endpointslices -n course-m1-service
kubectl describe svc linkding -n course-m1-service
  1. Проверьте DNS-резолвинг и доступ изнутри кластера:
1
2
kubectl run debug --rm -it --image=busybox -n course-m1-service -- sh -c \
  "nslookup linkding; wget -qO- http://linkding.course-m1-service.svc.cluster.local/ | head -5"
  1. Смоделируйте headless Service и посмотрите на разницу в DNS-ответе:
1
2
3
kubectl patch svc linkding -n course-m1-service --type=merge -p '{"spec":{"clusterIP":"None"}}'
kubectl run debug2 --rm -it --image=busybox -n course-m1-service -- nslookup linkding
# теперь в ответе — IP всех Pod'ов по отдельности, а не один виртуальный IP
  1. Сломайте selector намеренно и убедитесь, что Endpoints опустели, а сам Service «выглядит» рабочим:
1
2
3
kubectl patch svc linkding -n course-m1-service --type=merge -p '{"spec":{"selector":{"app":"typo"}}}'
kubectl get endpointslices -n course-m1-service   # пусто!
kubectl patch svc linkding -n course-m1-service --type=merge -p '{"spec":{"selector":{"app":"linkding"}}}'   # вернуть

Практика разработки

Используется официальный образ sissbruecker/linkding, собственная сборка появится в M3.

Шпаргалка команд урока

1
2
3
4
kubectl get svc [-n <ns>]
kubectl get endpointslices [-l kubernetes.io/service-name=<svc>]
kubectl describe svc <name> [-n <ns>]
kubectl run debug --rm -it --image=busybox -- sh

Вопросы для самопроверки

Что реально хранит актуальный список IP-адресов Pod'ов, подходящих под Service?

Service декларирует правило (selector), а актуальный список адресов поддерживается в EndpointSlice/Endpoints.

Источник: Kubernetes — service

Какой тип Service технически включает в себя ClusterIP и добавляет статический порт на каждом узле кластера?

NodePort — это ClusterIP плюс открытый порт на каждом узле; LoadBalancer, в свою очередь, расширяет NodePort внешним балансировщиком.

Источник: Kubernetes — service

Чем отличается headless Service (clusterIP: None) от обычного?

Headless Service убирает единый виртуальный IP, отдавая клиенту список всех адресов Pod'ов напрямую — используется, когда клиенту важно различать конкретные реплики.

Источник: Kubernetes — service

Как правильно обратиться к Service из другого namespace по короткому имени?

Search-домены в /etc/resolv.conf Pod'а покрывают только собственный namespace; из другого namespace нужно хотя бы <service>.<namespace>.

Источник: Kubernetes — dns pod service

Service существует, describe не показывает явных ошибок, но приложение не отвечает через него. Какая проверка наиболее вероятно выявит проблему?

Самая частая причина «немого» Service — опечатка в selector, из-за которой EndpointSlice остаётся пустым при формально валидном Service.

Источник: Рекомендуемая литература

Рекомендуемая литература

Официальная документация

Статьи и блоги

Книги

  • Marko Lukša. Kubernetes in Action. 2nd ed., 2022. ISBN 978-1617297618.
  • Kelsey Hightower, Brendan Burns, Joe Beda. Kubernetes: Up and Running. 3rd ed., 2022. ISBN 978-1098110201.
  • Michael Hausenblas, Stefan Schimanski. Programming Kubernetes. 2019. ISBN 978-1492047107.

Связанные материалы