Learning Platform
Глоссарий Troubleshooting
Урок 09.02 · 25 мин
Продвинутый
ServiceClusterIPNodePortLoadBalancerExternalNameHeadlessSelectorEndpoints

Service: типы и базовые свойства

PodIP — ephemeral. При пересоздании Pod (rolling update, restart, eviction) IP меняется. Нельзя харкодить PodIP в конфигах клиентов. Нельзя ходить на конкретный Pod, потому что Pods приходят и уходят.

Нужна абстракция со стабильным адресом, которая прячет конкретные Pod-ы и распределяет нагрузку между ними. Это и есть Service. На уровне API — это объект kind: Service, на уровне сети — виртуальный IP (или DNS-имя) + правила kube-proxy, реализующие load balancing.


Bridge network и публикация портов в Docker

Что такое Service

Service — это API-объект группы v1 (core, не apps/v1). Минимальный пример:

apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  selector:
    app: web
  ports:
    - port: 80
      targetPort: 8080

Что произойдёт:

  1. K8s выделит из service CIDR диапазона ClusterIP (например, 10.96.0.10).
  2. Создаст объект Endpoints (или EndpointSlices) с IP всех Pods, у которых label app=web и они Ready.
  3. На каждой ноде kube-proxy подхватит изменения и пропишет правила в iptables/IPVS/nftables: пакеты на 10.96.0.10:80 будут DNAT-ться на один из endpoint IP:8080.
  4. CoreDNS добавит запись web.default.svc.cluster.local → 10.96.0.10.

С этого момента любой Pod в кластере может ходить на http://web:80 — и попадёт на один из живых backend-ов.


Selector — связь Service ↔ Pods

spec.selector — это label selector, который выбирает Pods для роли backend. Это не запрос к Deployment — Service знает только о Pods. Если labels Deployment-овских Pods матчатся — они становятся endpoints.

spec:
  selector:
    app: web
    tier: frontend

K8s контроллер EndpointSlice controller в kube-controller-manager:

  • Watch-ит Services и Pods.
  • Для каждого Service фильтрует Pods по spec.selector.
  • Из этих Pods берёт только Ready (по .status.conditions.Ready == True).
  • Складывает их IP:port в объекты EndpointSlice.
NOTE

Старый объект Endpoints (один на Service) всё ещё создаётся для backward compatibility. С v1.21 default — EndpointSlices (несколько slice-ов по 100 endpoints в каждом). Об этом подробнее в уроке 4.

Service связывает себя с Pods
ServiceОбъект Service с selector app=web. Создаёт ClusterIP 10.96.0.10.
EndpointSlice controller матчит
EndpointSlicediscovery.k8s.io/v1. Список endpoints с IP и Ready-флагом. Создаётся автоматически для каждого Service со selector.
kube-proxy watch
Pod 1Ready=True. Включён в endpoints.
Pod 2Ready=True. Включён в endpoints.
Pod 3Ready=True. Включён в endpoints.

Только Ready Pods попадают в endpoints. Если readinessProbe возвращает fail — kube-proxy исключит этот Pod из ротации load balancing. Это критичный механизм graceful rollout.


ports: port, targetPort, nodePort

В spec.ports три разных порта. Их часто путают.

spec:
  ports:
    - name: http
      port: 80           # порт самого Service (на ClusterIP)
      targetPort: 8080   # порт внутри backend Pod
      nodePort: 30080    # порт на каждой ноде (для типа NodePort)
      protocol: TCP
  • port — на каком порту слушает Service ClusterIP. Когда клиент пишет http://web:80 — он попадает сюда. Это “frontend” порт Service.
  • targetPort — куда Service форвардит трафик внутри backend Pod. Это containerPort из Pod-а. Может быть числом (8080) или именем (http — если в Pod описан ports: [{name: http, containerPort: 8080}]).
  • nodePort — порт на каждой ноде кластера, открытый для типа NodePort/LoadBalancer. Только в диапазоне 30000-32767 (configurable, но по умолчанию).
Client → web:80 (Service port)
       → DNAT → 10.244.1.5:8080 (Pod targetPort)

Один Service может exposить несколько портов (для приложений на разных портах, например metrics + main app):

ports:
  - name: http
    port: 80
    targetPort: 8080
  - name: metrics
    port: 9090
    targetPort: 9090

name обязательно если портов больше одного.


Headless Service: clusterIP None

Иногда ClusterIP не нужен — клиенту нужны IPs всех Pods (например, для StatefulSet или клиента, который сам делает load balancing). Тогда используют headless service: spec.clusterIP: None.

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

Что происходит:

  • K8s не выделяет ClusterIP.
  • kube-proxy не создаёт iptables/IPVS правила для этого Service.
  • Но CoreDNS всё равно создаёт DNS-записи — только теперь это несколько A-записей, по одной на каждый Pod в endpoints.
# Обычный (ClusterIP) Service:
$ dig +short web.default.svc.cluster.local
10.96.0.10            # один IP, ClusterIP

# Headless:
$ dig +short db.default.svc.cluster.local
10.244.1.5            # Pod IPs напрямую
10.244.2.7
10.244.3.9

Клиент получает список реальных Pod-ов, может балансировать сам, выбрать конкретный, держать stable connection к одному (например, для gRPC).

Для StatefulSet headless service обязательный: он обеспечивает каждому Pod-у stable DNS-имя <pod-name>.<service-name>.<namespace>.svc.cluster.local (например, db-0.db.default.svc.cluster.local).

TIP

Headless service — это “DNS as service discovery, without virtual IP”. Полезно когда клиент сам умнее kube-proxy. Главные случаи: StatefulSets (Cassandra, Elasticsearch, Kafka), gRPC clients (нужны Pod IPs, чтобы балансировать на L7), peer-to-peer apps.


Service types: ClusterIP

Default тип. ClusterIP — виртуальный IP, доступный только внутри кластера.

spec:
  type: ClusterIP        # можно не указывать — default

Это базовая форма: 99% internal services именно ClusterIP. Извне кластера такой Service недоступен.


Service types: NodePort

type: NodePort — открывает порт на КАЖДОЙ ноде кластера (включая control plane, если не disabled), форвардящий на Service.

spec:
  type: NodePort
  selector:
    app: web
  ports:
    - port: 80
      targetPort: 8080
      nodePort: 30080    # если не указать — K8s выделит из 30000-32767

Доступен извне через <любой-node-ip>:30080.

NodePort: трафик от внешнего клиента
External clientЛюбой клиент вне кластера. Бьёт на любой node IP на порту 30080.
TCP to node:30080
kube-proxy на нодеЛюбая нода кластера принимает пакеты на nodePort. iptables правила перехватывают и DNAT-ят.
DNAT
Backend PodМожет быть на той же ноде или другой. Если на другой — пакет уходит через CNI overlay/routing.

NodePort включает ClusterIP under the hood — то есть Service одновременно доступен и через ClusterIP (изнутри), и через NodePort (снаружи).

Подводные камни NodePort:

  • Порты 30000-32767 — это ограниченный диапазон. На больших кластерах может закончиться.
  • Открывать порты на всех нодах — security risk. Нужен firewall.
  • externalTrafficPolicy: Cluster (default) — пакет с одной ноды может уехать на Pod на другой, теряя client IP. externalTrafficPolicy: Local — обрабатывается только на той же ноде где Pod (сохраняет src IP, но даёт неравномерную нагрузку).

Service types: LoadBalancer

type: LoadBalancer — для cloud-инсталляций (AWS, GCP, Azure). K8s через cloud-controller-manager просит у cloud-провайдера внешний LB (ELB, GCLB, Azure LB), который терминирует external трафик и форвардит на NodePort всех нод.

spec:
  type: LoadBalancer
  selector:
    app: web
  ports:
    - port: 80
      targetPort: 8080

K8s сам выделяет nodePort, заполняет .status.loadBalancer.ingress адресом LB:

$ kubectl get svc web
NAME   TYPE           CLUSTER-IP    EXTERNAL-IP      PORT(S)        AGE
web    LoadBalancer   10.96.0.42    34.123.45.67     80:30080/TCP   2m

LoadBalancer = ClusterIP + NodePort + external LB. Все три слоя работают.

В bare-metal кластерах (без cloud) LoadBalancer не создастся автоматически — нужны решения как MetalLB, kube-vip, или Ingress поверх NodePort.


Service types: ExternalName

type: ExternalName — необычный тип. Без selector, без endpoints, без ClusterIP. Просто DNS CNAME alias.

apiVersion: v1
kind: Service
metadata:
  name: prod-db
spec:
  type: ExternalName
  externalName: my-db.example.com

Что произойдёт:

  • CoreDNS добавит CNAME: prod-db.default.svc.cluster.local → my-db.example.com.
  • Никаких ClusterIP/iptables правил — это чисто DNS-уровень.
  • Pod пишет prod-db:5432 → DNS возвращает my-db.example.com → дальше идёт через обычную DNS-цепочку к external адресу.

Полезно для:

  • Миграции: пока DB живёт извне кластера, потом её перевозят внутрь — меняется только externalName.
  • Отделение конфига от кода: код знает имя prod-db, Service-объект указывает на реальный адрес.
WARNING

ExternalName не делает port translation, не проксирует TCP, не имеет ничего кроме DNS. Это просто alias. Если нужен реальный proxy к external endpoint — используйте Service без selector + ручной Endpoints объект с IP внешнего хоста.


Service без selector

Иногда нужен Service, который указывает на не-Kubernetes backend (legacy сервер, external DB, что-то в другом кластере). Можно создать Service без selector и вручную создать Endpoints с нужными IP:

apiVersion: v1
kind: Service
metadata:
  name: legacy-api
spec:
  ports:
    - port: 80
      targetPort: 8080
---
apiVersion: v1
kind: Endpoints
metadata:
  name: legacy-api      # ИМЯ должно совпадать с Service
subsets:
  - addresses:
      - ip: 192.168.50.10
      - ip: 192.168.50.11
    ports:
      - port: 8080

K8s не будет автоматически менять этот Endpoints — управляет им человек/external оператор. Pod-ы внутри кластера могут писать http://legacy-api:80 и попадать на эти IP.


Killer момент: ClusterIP — это призрак

Давайте чётко: что такое 10.96.0.10 физически?

WARNING

ClusterIP не принадлежит ни одному network interface. Ни на одной ноде нет интерфейса с этим IP. Никакой Pod не отвечает на ARP-запрос с этим IP. Если запустить ping 10.96.0.10 с ноды — никто не ответит.

ClusterIP существует только в правилах netfilter (или эквиваленте в IPVS/nftables/eBPF). Это правило вида:

iptables -t nat -A KUBE-SERVICES -d 10.96.0.10/32 -p tcp --dport 80 \
  -j KUBE-SVC-XXXXXX

Когда пакет с dst=10.96.0.10:80 проходит через kernel netfilter — это правило перехватывает его и отправляет в цепочку KUBE-SVC-XXXXXX, которая делает DNAT на один из endpoint IPs.

Что это значит на практике:

  • ClusterIP работает только для протоколов, которые обрабатывает netfilter для NAT — TCP, UDP, SCTP.
  • ICMP не работает (ping не пройдёт).
  • tcpdump на интерфейсе ноды покажет, что пакет приходит с dst=ClusterIP, но сразу же DNAT-ится (видно в conntrack), и на eth0 уже уходит с dst=PodIP.
  • Если kube-proxy упал на ноде — Service перестаёт работать на этой ноде (старые правила могут остаться в iptables, но новые changes не применятся).
# Посмотреть, какой DNAT произошёл
ssh node1
conntrack -L | grep 10.96.0.10
# tcp 6 110 ESTABLISHED src=10.244.1.5 dst=10.96.0.10 sport=42124 dport=80
#                       src=10.244.2.7 dst=10.244.1.5 sport=8080 dport=42124 [ASSURED]
#                       ↑ reverse direction — обратный пакет приходит от PodIP, conntrack восстанавливает ClusterIP

В следующем уроке мы препарируем сами iptables-правила kube-proxy.


Жизненный цикл Service

# Создать
kubectl apply -f svc.yaml

# Посмотреть
kubectl get svc
# NAME   TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)   AGE
# web    ClusterIP   10.96.0.10    <none>        80/TCP    2m

# Endpoints
kubectl get endpoints web
# NAME   ENDPOINTS                                       AGE
# web    10.244.1.5:8080,10.244.2.7:8080,10.244.3.9:8080   2m

# EndpointSlices (modern)
kubectl get endpointslices -l kubernetes.io/service-name=web

# Describe — selector, ports, endpoints, sessionAffinity
kubectl describe svc web

# Кратко: создать ClusterIP Service для уже работающего Deployment
kubectl expose deployment web --port=80 --target-port=8080

kubectl expose — императивная команда, удобная для экзамена.


Session affinity

По умолчанию kube-proxy балансирует случайно — каждый новый TCP-connect может попасть на другой Pod. Если приложение не stateless — это проблема. Можно включить sticky sessions:

spec:
  sessionAffinity: ClientIP
  sessionAffinityConfig:
    clientIP:
      timeoutSeconds: 10800   # 3 часа, default

С ClientIP kube-proxy запоминает (через conntrack или IPVS hash table): один клиент IP → один backend, пока не истечёт timeout. Это L4 affinity, не cookie-based — не работает за NAT (когда много клиентов с одного IP).

Для L7 sticky sessions (cookie) — нужен Ingress controller.


Проверка знанийKnowledge check
Pod-клиент хочет общаться с gRPC сервисом, у которого 5 backend-Pods. Если использовать обычный ClusterIP Service, что произойдёт с load balancing на gRPC-трафике?
ОтветAnswer
gRPC использует HTTP/2 и держит долгоживущие TCP-соединения, через которые мультиплексирует множество RPC-вызовов. kube-proxy балансирует на L4 — выбирает backend при установке TCP-connect, а потом весь трафик этого соединения идёт на один Pod. В результате клиент откроет одно TCP-соединение, выберется один backend, и ВСЕ последующие gRPC-запросы пойдут на него — остальные 4 Pods будут простаивать. Это known issue gRPC over ClusterIP. Решение: использовать headless Service (clusterIP: None) — клиент получит все Pod IPs через DNS и сделает L7 load balancing сам (gRPC client-side LB). Альтернативно — поставить service mesh (Istio/Linkerd) для L7 балансировки через sidecar proxy.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 5. В Service spec поля port, targetPort и nodePort — какое из описаний верное?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5