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:30080 → ClusterIP:80 → Pod: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.
Сводка типов
| Тип | Доступность | ClusterIP | NodePort | Внешний LB |
|---|---|---|---|---|
| ClusterIP | Внутри кластера | Да | Нет | Нет |
| NodePort | Через NodeIP:порт | Да | Да | Нет |
| LoadBalancer | Через внешний LB | Да | Да | Да |
| ExternalName | CNAME 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
| Режим | Статус | Как работает | Особенности |
|---|---|---|---|
iptables | default (Linux) | цепочки NAT-правил, backend выбирается случайно (statistic module) | O(n) правил на число Service; медленное обновление больших таблиц |
ipvs | deprecated с 1.35 | IPVS (hash-таблицы в ядре) + алгоритмы балансировки (rr, lc, dh, sh…) | O(1) lookup, задумывался для крупных кластеров |
nftables | stable/GA с 1.33 (Linux) | nftables — преемник iptables API, verdict maps | быстрее обновляет endpoints и обрабатывает пакеты; требует kernel ≥ 5.13 |
kernelspace | Windows | правила в 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 |
|---|---|---|
externalTrafficPolicy | Cluster (default) / Local | внешний трафик идёт только на Pod этой же ноды; сохраняет client source IP, но нет второго hop |
internalTrafficPolicy | Cluster (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 |
|---|---|---|
| API | v1 core | discovery.k8s.io/v1 |
| Размер | один объект на все endpoints | куски ≤ 100 (до 1000) |
| Статус | deprecated с 1.33 | stable с 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 | то же, что PreferClose | stable с 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, не заменяет его.