У каждого пода в кластере есть собственный IP. Казалось бы, бери да обращайся напрямую. Но поды смертны: деплой выкатил новую версию - старые поды удалили, новые подняли с другими адресами. ReplicaSet отскейлил - адресов стало больше или меньше. HPA дёрнул нагрузку - набор IP меняется каждые несколько секунд. Если твой фронтенд хардкодит IP бэкенда, он сломается при первом же рестарте.
Service - это стабильная точка входа поверх изменчивого множества подов. Ты получаешь один неизменный виртуальный IP (ClusterIP) и DNS-имя, а за ним прячется живой, постоянно обновляемый список здоровых эндпоинтов. Это базовый кирпич сервис-дискавери, и понимать, что такое kubernetes service и как он работает под капотом, обязательно для любого, кто эксплуатирует кластер всерьёз. В этом уроке разберём не "что такое clusterip nodeport", а механику: откуда берётся виртуальный IP, кто и как переписывает пакеты, почему обычные Endpoints заменили на EndpointSlices и чем iptables-режим отличается от ipvs kubernetes на больших масштабах.
Сразу важная мысль: ClusterIP - это не адрес какого-то процесса. На этот IP никто не слушает. Это просто число, которое ловит сетевой стек ядра на каждой ноде и подменяет на адрес реального пода. Service - чистая абстракция уровня данных, а не сервер. Понял это - половина магии исчезла.

Кто кого выбирает: selector, Endpoints и EndpointSlices
Service не знает про конкретные поды. Он знает про label selector. Контроллер endpoint-slice (часть kube-controller-manager) непрерывно смотрит, какие поды попадают под selector сервиса И прошли readiness-проба, и складывает их адреса в объекты. Раньше это был один объект Endpoints на сервис - и тут пряталась серьёзная проблема масштаба.
Представь сервис с 5000 подов. В старой модели весь список из 5000 адресов лежал в одном объекте Endpoints. Меняется один под (rolling update переехал на новую реплику) - и API-сервер обязан разослать весь объект целиком всем подписчикам: каждому kube-proxy на каждой ноде. Один чих - мегабайты трафика по всему кластеру. На крупных кластерах это душило etcd и control plane.
EndpointSlices решают это шардированием. Вместо одного гигантского объекта - набор слайсов, по умолчанию до 100 эндпоинтов в каждом. Меняется один под - перерассылается только тот слайс (100 записей), а не весь список. Это и есть смысл endpointslices: гранулярность обновлений и линейная масштабируемость control plane. Посмотрим вживую.
Код: Выделить всё
$ kubectl get endpointslices -l kubernetes.io/service-name=web
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
web-7p4k2 IPv4 8080 10.244.1.7,10.244.2.3 4h
web-x9m1q IPv4 8080 10.244.3.5,10.244.1.9 4h
Код: Выделить всё
$ kubectl get endpointslice web-7p4k2 -o yaml
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
addressType: IPv4
endpoints:
- addresses:
- 10.244.1.7
conditions:
ready: true
serving: true
terminating: false
nodeName: node-a
zone: ru-central1-a
ports:
- name: http
port: 8080
protocol: TCP
kube-proxy под капотом: iptables, IPVS и nftables
Теперь главное действующее лицо - kube-proxy. Это демон (обычно DaemonSet) на каждой ноде. Он подписан на Service и EndpointSlices через informer-кэш, при каждом изменении диффает желаемое состояние с тем, что сейчас в ядре, и программирует датаплейн ядра так, чтобы пакет на ClusterIP подменялся на адрес живого пода. Сам kube-proxy не проксирует трафик через себя (вопреки названию) - он только пишет правила, а пакеты гоняет ядро. Это критично: упал kube-proxy - существующие правила продолжают работать, новые просто не появляются.
Исторически было три режима, и понимать разницу - значит понимать производительность сети на масштабе.
Режим iptables. Классика по умолчанию много лет. kube-proxy для каждого сервиса генерирует цепочку правил DNAT (Destination NAT): пришёл пакет на ClusterIP:port - подменяем destination на IP:port случайного пода, дальше conntrack ядра запоминает соединение и держит его на том же поде. Балансировка делается через statistic-модуль с вероятностями: первое правило ловит пакет с вероятностью 1/N, следующее 1/(N-1) и так далее. Проблема в природе iptables: это линейный список правил, который ядро обходит последовательно. На 5000 сервисов и десятки тысяч эндпоинтов таблица раздувается до сотен тысяч правил, а каждое обновление - это перезапись большого куска таблицы под глобальным локом. Латентность добавления правила растёт нелинейно, и на больших кластерах синхронизация начинает занимать секунды.
Режим IPVS. IPVS (IP Virtual Server) - это L4-балансировщик прямо в ядре Linux, построенный на хеш-таблицах, а не на линейном списке. kube-proxy в этом режиме создаёт виртуальный сервер на ClusterIP и привязывает к нему real servers (поды). Поиск нужного бэкенда - O(1) по хешу, а не O(N) по списку. Плюс IPVS даёт настоящие алгоритмы балансировки: rr (round-robin), wrr (взвешенный), lc (least connection), sh (source hashing). На кластерах с тысячами сервисов ipvs kubernetes исторически выигрывал у iptables и по латентности добавления правил, и по пропускной способности. Важная деталь: IPVS всё равно подтягивает немного iptables-правил для маскарадинга и фильтрации, так что полностью от iptables он не уходит.
Режим nftables - актуальный выбор на 2026. И вот ключевое обновление, которое надо знать. nftables-режим kube-proxy стал GA в Kubernetes 1.33 и официально рекомендован как замена и iptables, и IPVS на нодах с современным ядром. nftables - это преемник iptables в самом ядре Linux: вместо линейных цепочек он использует maps и verdict-карты, что даёт O(1)-поиск бэкенда и инкрементальные обновления без перезаписи всей таблицы. По сути он закрывает главную боль iptables-режима (масштаб) штатными средствами ядра, без отдельной подсистемы вроде IPVS. Сам IPVS-режим в свежих релизах помечен как deprecated - не "удалён завтра", но новых внедрений на нём начинать не стоит, целься в nftables. Включается режим флагом kube-proxy.
Код: Выделить всё
# фрагмент конфига kube-proxy (ConfigMap kube-proxy в namespace kube-system)
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: "nftables" # варианты: "iptables", "ipvs", "nftables"
Типы Service: ClusterIP, NodePort, LoadBalancer, headless
ClusterIP - дефолт. Виртуальный IP из service CIDR, доступный только внутри кластера. 90% сервисов - именно такие: бэкенды, базы, внутренние API.
NodePort - надстройка над ClusterIP. Кроме виртуального IP, Kubernetes открывает один и тот же порт на каждой ноде кластера из диапазона по умолчанию 30000-32767. Стучишь на IP-любой-ноды:30080 - попадаешь в сервис. Диапазон задаётся флагом --service-node-port-range у API-сервера. Граблю запомни: порт занят на всех нодах, даже на тех, где нет ни одного пода этого сервиса, а сам диапазон узкий, так что на NodePort внешний прод обычно не строят - это сырьё для внешних балансировщиков.
Код: Выделить всё
apiVersion: v1
kind: Service
metadata:
name: web
spec:
type: NodePort
selector:
app: web
ports:
- port: 80 # порт ClusterIP
targetPort: 8080 # порт контейнера
nodePort: 30080 # порт на каждой ноде (можно не задавать - выберется сам)
Headless - особый случай: clusterIP: None. Виртуального IP нет вообще, DNAT не происходит. Вместо одного A-записи на ClusterIP DNS отдаёт A-записи всех подов сразу. Зачем? Когда клиенту нужны конкретные поды, а не безликая балансировка. Канонический случай - StatefulSet: каждому поду нужно стабильное DNS-имя вида web-0.web.default.svc.cluster.local, чтобы реплики базы находили друг друга по именам. Headless - это discovery без проксирования.
Код: Выделить всё
apiVersion: v1
kind: Service
metadata:
name: db
spec:
clusterIP: None # делает сервис headless
selector:
app: db
ports:
- port: 5432
Код: Выделить всё
$ kubectl run -it --rm dnsutils --image=registry.k8s.io/e2e-test-images/jessie-dnsutils:1.7 -- nslookup db
Server: 10.96.0.10
Address: 10.96.0.10#53
Name: db.default.svc.cluster.local
Address: 10.244.1.7 # сразу адреса подов, а не ClusterIP
Address: 10.244.2.3
Address: 10.244.3.5
Самая частая боль в проде - потеря IP клиента. Сценарий: внешний запрос пришёл на NodePort ноды A, но под живёт на ноде B. Нода A форвардит пакет на B и при этом делает SNAT - подменяет source IP на свой собственный, иначе ответный пакет не вернётся правильным маршрутом. Итог: твой под видит source IP ноды, а не реального клиента. Логи, гео, rate limit, бан по IP - всё врёт.
Лечится полем externalTrafficPolicy:
- Cluster (по умолчанию) - трафик размазывается по всем подам кластера, балансировка ровная, но source IP теряется из-за SNAT и появляется лишний межнодовый хоп.
- Local - нода отправляет трафик только в локальные поды на этой же ноде, без SNAT. Source IP клиента сохраняется. Цена: если на ноде нет пода сервиса, трафик на ней дропается, а балансировка становится неравномерной (нода с 3 подами получит столько же внешнего трафика, сколько нода с 1 подом). Облачный LB при этом опрашивает healthCheckNodePort и не шлёт трафик на ноды без локальных подов.
Код: Выделить всё
spec:
type: LoadBalancer
externalTrafficPolicy: Local # сохранить source IP клиента
Topology-aware routing. В мультизональном кластере дефолтная балансировка размазывает трафик по всем зонам - а cross-AZ трафик в облаке стоит денег и добавляет латентность. Раньше это решал annotation service.kubernetes.io/topology-aware-hints, теперь штатное поле spec.trafficDistribution. Значение PreferClose говорит kube-proxy: предпочитай эндпоинты в той же зоне, что и клиент, а в другую зону уходи только если локальных не осталось. В 1.33 добавились более явные PreferSameZone и PreferSameNode (пока alpha).
Код: Выделить всё
spec:
trafficDistribution: PreferClose # держать трафик в своей зоне
Код: Выделить всё
spec:
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds: 10800
- "Балансировка не round-robin". В iptables-режиме это не RR, а случайный выбор по вероятностям + липкость conntrack: установленное TCP-соединение живёт на одном поде до закрытия. Долгие keep-alive соединения (gRPC, HTTP/2, пулы к БД) прилипают намертво и распределяются неравномерно - новый под после скейла может вообще не получить трафик. Лечат разрывом соединений на уровне приложения или L7-прокси.
- Source IP потерян, хотя ничего не настраивал. Дефолт - Cluster, значит SNAT. Хочешь IP клиента - ставь externalTrafficPolicy: Local и закладывай неравномерность.
- NodePort "занят". Диапазон узкий (около 2700 портов), порт глобален на все ноды. Не раздавай NodePort руками без учёта - кончатся.
- Сервис есть, эндпоинтов ноль. kubectl get endpointslices пуст - значит selector не матчит ни один под ИЛИ поды не прошли readinessProbe. Первым делом сверь labels пода и selector сервиса, потом смотри readiness.
- targetPort перепутан с port. port - это порт ClusterIP, targetPort - порт контейнера. Перепутал - сервис резолвится, но connection refused.
- Подними деплой из 3 реплик nginx и сервис ClusterIP к нему. Посмотри kubectl get endpointslices -l kubernetes.io/service-name=ИМЯ - убедись, что в слайсе ровно 3 адреса.
- Прибей один под (kubectl delete pod). Сразу снова глянь слайс: адрес терминирующего пода уйдёт, появится новый. Поймай момент с conditions.terminating.
- Сделай сервис headless (clusterIP: None), из временного пода выполни nslookup на его имя - убедись, что DNS отдаёт IP подов, а не один ClusterIP.
- Пересоздай как NodePort, найди nodePort в выводе kubectl get svc, постучи curl на IP-ноды:nodePort.
- Добавь externalTrafficPolicy: Local и сравни, что увидит приложение в X-Forwarded-For / source IP до и после.
- Почему EndpointSlices масштабируются лучше одного объекта Endpoints при частых изменениях подов?
- Чем поиск бэкенда в IPVS/nftables принципиально выгоднее линейного списка iptables на больших кластерах?
- Что именно делает externalTrafficPolicy: Local и какой ценой сохраняет source IP клиента?
- Зачем нужен headless-сервис и почему он незаменим для StatefulSet?
Service - стабильная абстракция поверх смертных подов: один ClusterIP и DNS-имя вместо ловли изменчивых адресов. EndpointSlices шардируют список эндпоинтов и снимают нагрузку с control plane на масштабе. kube-proxy не проксирует, а программирует ядро: iptables (линейно, тяжело на масштабе), IPVS (хеши, deprecated) и актуальный на 2026 nftables-режим (GA в 1.33, рекомендованная замена обоим). Типы покрывают все сценарии: ClusterIP внутри, NodePort как сырьё, LoadBalancer в облаке и MetalLB on-prem, headless для discovery. А ключ к проду - помнить про source IP (externalTrafficPolicy: Local), неравномерность балансировки и стоимость cross-AZ трафика (trafficDistribution: PreferClose).