← Back to notes

Services


Pod эфемерны — создаются, уничтожаются и пересоздаются контроллерами. Каждый новый Pod получает новый IP. Обращаться к Pod по IP напрямую ненадёжно. Service решает эту проблему: даёт стабильный ClusterIP и DNS-имя, которые живут независимо от Pod.

source: kubernetes.io/docs/concepts/services-networking/service/

Что такое Service

Service — абстракция, которая предоставляет постоянную точку доступа к набору Pod. Service определяет набор Pod через Label Selector и обеспечивает балансировку нагрузки между ними.

Service получает:

  • Постоянный ClusterIP (не меняется за всё время жизни Service)
  • DNS-имя: <name>.<namespace>.svc.cluster.local
apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  selector:
    app: my-app
  ports:
    - port: 80          # порт Service
      targetPort: 8080  # порт контейнера
      protocol: TCP

port — порт, на котором Service принимает трафик. targetPort — порт контейнера, куда трафик перенаправляется. Если targetPort не указан, используется значение port.

Типы Service

ClusterIP (по умолчанию)

Доступен только внутри кластера. Подходит для внутренней коммуникации между сервисами.

apiVersion: v1
kind: Service
metadata:
  name: backend-service
spec:
  type: ClusterIP
  selector:
    app: backend
  ports:
    - port: 80
      targetPort: 8080

NodePort

Открывает порт на каждом узле кластера. Трафик на <NodeIP>:<NodePort> маршрутизируется к Service. Диапазон портов: 30000–32767.

apiVersion: v1
kind: Service
metadata:
  name: frontend-service
spec:
  type: NodePort
  selector:
    app: frontend
  ports:
    - port: 80
      targetPort: 8080
      nodePort: 30080    # необязательно  назначится автоматически

NodePort автоматически создаёт ClusterIP. Маршрут: клиент → NodeIP:30080ClusterIP:80Pod:8080.

LoadBalancer

Создаёт внешний балансировщик нагрузки у облачного провайдера (AWS, GCP, Azure). Автоматически создаёт NodePort и ClusterIP.

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

После создания в status.loadBalancer.ingress появится внешний IP или hostname:

kubectl get svc web-service
# NAME          TYPE           CLUSTER-IP      EXTERNAL-IP    PORT(S)        AGE
# web-service   LoadBalancer   10.96.12.34     203.0.113.50   80:31234/TCP   5m

На bare-metal кластерах LoadBalancer без cloud-провайдера зависнет в <pending>. Решение — MetalLB или аналоги.

ExternalName

Маппинг Service на внешнее DNS-имя (CNAME). Не создаёт прокси и ClusterIP.

apiVersion: v1
kind: Service
metadata:
  name: external-api
spec:
  type: ExternalName
  externalName: api.external-provider.com

Обращение к external-api.default.svc.cluster.local вернёт CNAME на api.external-provider.com. Используется для интеграции с внешними сервисами без хардкода URL в Pod.

Сводка типов

ТипДоступностьClusterIPNodePortВнешний LB
ClusterIPВнутри кластераДаНетНет
NodePortЧерез NodeIP:портДаДаНет
LoadBalancerЧерез внешний LBДаДаДа
ExternalNameCNAME aliasНетНетНет

Service без Selector

Используется для интеграции с внешними сервисами или ресурсами вне кластера. Endpoints задаются вручную через EndpointSlice (stable с Kubernetes 1.21; legacy Endpoints deprecated с 1.33, но ещё доступен для совместимости).

apiVersion: v1
kind: Service
metadata:
  name: external-db
spec:
  ports:
    - port: 5432
      targetPort: 5432
---
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: external-db-1
  labels:
    kubernetes.io/service-name: external-db
addressType: IPv4
ports:
  - port: 5432
endpoints:
  - addresses: ["10.0.0.100"]
  - addresses: ["10.0.0.101"]

Несколько портов

При объявлении нескольких портов каждый должен иметь имя:

apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  selector:
    app: my-app
  ports:
    - name: http
      port: 80
      targetPort: 8080
    - name: metrics
      port: 9090
      targetPort: 9090

DNS

source: kubernetes.io/docs/concepts/services-networking/dns-pod-service/

Каждый Service получает DNS-запись:

<service-name>.<namespace>.svc.cluster.local
# Внутри того же namespace  достаточно имени
curl http://backend-service
curl http://backend-service:8080

# Из другого namespace
curl http://backend-service.production.svc.cluster.local

# Краткая форма (без .svc.cluster.local) тоже работает
curl http://backend-service.production

DNS в кластере обеспечивает CoreDNS — он работает как Deployment в kube-system и обслуживает DNS-запросы Pod.

Роль kube-proxy

kube-proxy работает на каждом узле и реализует маршрутизацию трафика к Pod. Он watch-ит Service и EndpointSlice через API server и программирует правила ядра так, чтобы пакеты на ClusterIP:port попадали на один из backend-Pod. Важно: kube-proxy работает на уровне L4 (TCP/UDP/SCTP), балансировка — per-connection, а не per-request.

Режимы kube-proxy

РежимСтатусКак работаетОсобенности
iptablesdefault (Linux)цепочки NAT-правил, backend выбирается случайно (statistic module)O(n) правил на число Service; медленное обновление больших таблиц
ipvsdeprecated с 1.35IPVS (hash-таблицы в ядре) + алгоритмы балансировки (rr, lc, dh, sh…)O(1) lookup, задумывался для крупных кластеров
nftablesstable/GA с 1.33 (Linux)nftables — преемник iptables API, verdict mapsбыстрее обновляет endpoints и обрабатывает пакеты; требует kernel ≥ 5.13
kernelspaceWindowsправила в Windows-ядре

iptables остаётся режимом по умолчанию; nftables выбирается явно через --proxy-mode nftables (или mode: nftables в конфиге). nftables намеренно не 100% совместим с iptables-режимом — исправлены «плохие дефолты»: например, NodePort в nftables-режиме доступен только на дефолтных IP ноды, а не на всех адресах включая 127.0.0.1.

source: kubernetes.io/docs/reference/networking/virtual-ips/

При замене kube-proxy (например, Cilium eBPF mode, kube-proxy replacement) Service API остаётся неизменным — меняется только реализация dataplane. eBPF-режимы обходят iptables/nftables полностью и программируют балансировку в BPF-программах.

internalTrafficPolicy и externalTrafficPolicy

Два поля управляют тем, куда kube-proxy может слать трафик:

ПолеЗначенияЭффект при Local
externalTrafficPolicyCluster (default) / Localвнешний трафик идёт только на Pod этой же ноды; сохраняет client source IP, но нет второго hop
internalTrafficPolicyCluster (default) / Localвнутрикластерный трафик к Service идёт только на локальные Pod ноды-отправителя

externalTrafficPolicy: Local — стандартный способ сохранить реальный IP клиента для NodePort/LoadBalancer (иначе SNAT затирает его), ценой того, что нода без backend-Pod «чёрная дыра» и должна отсеиваться healthcheck-ом LB.

EndpointSlices: почему заменили Endpoints

За каждым Service с selector-ом стоит список готовых backend-адресов. Раньше это был единственный объект Endpoints, куда писались все IP:port всех Pod сервиса. Проблема масштабирования: при 5000 Pod любое изменение (один Pod стал ready) переписывало весь объект и рассылалось всем kube-proxy на всех нодах — гигабайты лишнего трафика на каждое мелкое изменение.

EndpointSlice разбивает список на куски: по умолчанию не более 100 endpoints на slice (настраивается флагом --max-endpoints-per-slice в kube-controller-manager, максимум 1000). Изменение одного Pod переписывает только его slice, а не весь список.

АспектEndpoints (legacy)EndpointSlice
APIv1 corediscovery.k8s.io/v1
Размеродин объект на все endpointsкуски ≤ 100 (до 1000)
Статусdeprecated с 1.33stable с 1.21
Topology / hintsнетzone, nodeName, hints — основа topology-aware routing
Dual-stackотдельные объектыaddressType IPv4 / IPv6 / FQDN

source: kubernetes.io/docs/concepts/services-networking/endpoint-slices/

Для обратной совместимости работает EndpointSlice mirroring: созданные вручную Endpoints автоматически зеркалируются в EndpointSlice. Но для selectorless-Service документация рекомендует создавать EndpointSlice напрямую (пример выше в разделе «Service без Selector»).

Session affinity

По умолчанию kube-proxy распределяет соединения к backend-ам случайно. sessionAffinity: ClientIP привязывает все соединения одного client IP к одному Pod на заданный таймаут:

spec:
  selector:
    app: my-app
  sessionAffinity: ClientIP
  sessionAffinityConfig:
    clientIP:
      timeoutSeconds: 10800   # default 3 часа
  ports:
    - port: 80
      targetPort: 8080

Это L4-affinity по IP, а не по cookie (L7 sticky sessions — задача Ingress/Gateway). За NAT все клиенты выглядят одним IP и попадут на один Pod — affinity по client IP не заменяет application-level session.

Topology aware routing и trafficDistribution

По умолчанию Service балансирует по всем готовым Pod в кластере независимо от зоны — это ровный трафик, но межзональный (в облаке — дороже и с большей latency). Есть два механизма предпочесть «ближние» endpoints.

trafficDistribution (поле spec Service, GA с 1.33) — явный hint для kube-proxy:

spec:
  selector:
    app: my-app
  trafficDistribution: PreferClose   # предпочесть endpoints в той же зоне
  ports:
    - port: 80
      targetPort: 8080
ЗначениеПоведениеСтатус
PreferCloseпредпочесть ту же зону; fallback на кластерGA с 1.33; с 1.35 — deprecated-алиас к PreferSameZone
PreferSameZoneто же, что PreferClosestable с 1.35
PreferSameNodeпредпочесть ту же ноду → зону → кластерstable с 1.35

Это preference, не гарантия: если локальных endpoints нет, трафик уходит в кластер, соединение не рвётся. Старый механизм — аннотация service.kubernetes.io/topology-mode: Auto (Topology Aware Routing, beta с 1.23; до 1.27 назывался Topology Aware Hints). trafficDistribution — более предсказуемая замена.

source: kubernetes.io/docs/concepts/services-networking/topology-aware-routing/

ClusterIP allocation

ClusterIP выделяется из диапазона --service-cluster-ip-range kube-apiserver. Обычно назначается динамически, но можно задать статически в spec.clusterIP (должен попадать в диапазон и быть свободным). Диапазон делится на две части: верхняя резервируется под динамические выдачи, нижняя — под статические, чтобы снизить конфликты.

Управление аллокацией доступно как API-объекты ServiceCIDR и IPAddress (группа networking.k8s.io) — это позволяет расширять диапазон Service IP без пересоздания кластера:

# Какие диапазоны Service IP активны
kubectl get servicecidr
# Выданные Service IP как объекты
kubectl get ipaddress

Попытка создать Service, когда диапазон исчерпан, вернёт ошибку Internal error ... range is full.

Основные команды

# Создать Service императивно
kubectl expose deployment nginx --port=80 --target-port=8080 --type=ClusterIP
kubectl expose deployment nginx --port=80 --type=NodePort

# Список Service
kubectl get services
kubectl get svc
kubectl get svc -o wide

# Подробности
kubectl describe svc my-service

# EndpointSlice (какие Pod стоят за Service)  stable API
kubectl get endpointslices -l kubernetes.io/service-name=my-service

# Legacy Endpoints (deprecated с 1.33, но всё ещё работает)
kubectl get endpoints my-service

# Создать из манифеста
kubectl apply -f service.yaml

Headless Service

clusterIP: None — Service без виртуального IP и без балансировки kube-proxy. Вместо одной A-записи на ClusterIP DNS возвращает адреса всех готовых Pod напрямую:

apiVersion: v1
kind: Service
metadata:
  name: my-headless
spec:
  clusterIP: None      # headless
  selector:
    app: my-app
  ports:
    - port: 80
  • С selector: my-headless.ns.svc.cluster.local резолвится в набор A/AAAA-записей Pod-ов. Клиент сам выбирает адрес — балансировку делает не kube-proxy, а клиент/библиотека.
  • Без selector: DNS отдаёт то, что указано в EndpointSlice (CNAME для ExternalName, адреса для ручных endpoints).

Основной потребитель — StatefulSet (глава 11): он даёт каждому Pod стабильное DNS-имя <pod>.<headless-svc>.<ns>.svc.cluster.local, что нужно для кластеров БД, где клиенту важно адресовать конкретный member, а не «любой Pod».

Типичные ошибки

Service создан, но EndpointSlice пуст. kubectl get endpointslices -l kubernetes.io/service-name=my-service пуст. Причина — selector не совпадает ни с одним Pod. Проверить: kubectl get pods --show-labels и сравнить с spec.selector Service.

Путаница port и targetPort. port — порт Service, куда стучится клиент. targetPort — порт контейнера. Ошибка: поставить одинаковые значения, а приложение слушает на другом порту.

LoadBalancer на bare-metal без MetalLB. Без cloud-провайдера или MetalLB EXTERNAL-IP зависнет в <pending>. Service работает через NodePort, но внешний IP не появится.

Использовать NodePort с фиксированным портом в production. Хардкод nodePort: 30080 конфликтует если порт уже занят другим Service. Лучше дать Kubernetes назначить порт автоматически.

Обращаться к Pod по IP напрямую. IP Pod меняется при перезапуске. Всегда ходить через Service.

Альтернативы

Ingress — L7-маршрутизация по hostname и пути для HTTP/HTTPS через единую точку входа. Поверх Service ClusterIP (глава 07).

Gateway API — современная замена Ingress с более богатой моделью маршрутизации (глава 07).

Service Mesh (Istio, Linkerd) — управление трафиком между сервисами с mTLS, circuit breaking, observability. Работает поверх Service, не заменяет его.


05: ReplicaSet и Deployment

07: Ingress

Services | Aleksandr Suprun