Сервисы и kube-proxy глубже: типы, IPVS, EndpointSlices

Рейтинг: 70.1% · 9 голосов
Kubernetes для разработчиков: поды, деплойменты, сервисы, ingress, конфиги и отладка. Уроки по главам с обсуждением.
Ответить
Аватара пользователя
anton_k8s
Сообщения: 55
Зарегистрирован: 12 май 2026, 03:23

Сервисы и kube-proxy глубже: типы, IPVS, EndpointSlices

Сообщение anton_k8s »

Оглавление курса (48)
  1. Зачем нужен Kubernetes и из чего состоит кластер
  2. Поднимаем локальный кластер: minikube и kind
  3. Поды: базовая единица запуска
  4. Deployment и ReplicaSet: управляем репликами
  5. Service: сетевой доступ к подам
  6. ConfigMap и Secret: выносим конфигурацию
  7. Ingress: пускаем трафик снаружи
  8. Хранилище: Volumes и PersistentVolumeClaim
  9. Namespaces, requests и limits
  10. Health checks: liveness и readiness пробы
  11. Отладка: почему под не стартует
  12. Helm: пакетный менеджер для Kubernetes
  13. Базовая безопасность: RBAC и доступы
  14. Job и CronJob: разовые и периодические задачи
  15. StatefulSet и DaemonSet: stateful-нагрузки и системные агенты
  16. Стратегии обновления и планирование: rollout и rollback, graceful shutdown, nodeSelector, affinity, taints
  17. Автомасштабирование: HPA по метрикам, обзор VPA и Cluster Autoscaler
  18. Наблюдаемость: логи, метрики, events, обзор Prometheus и Grafana
  19. Безопасность глубже: securityContext, Pod Security Standards, NetworkPolicy, шифрование секретов
  20. Архитектура control plane: api-server, etcd, scheduler
  21. Узел кластера: kubelet, kube-proxy и container runtime
  22. Декларативная модель: reconciliation, контроллеры, CRD
  23. kubectl профессионально: get, describe, explain, jsonpath
  24. Под глубже: init, sidecar, lifecycle hooks и QoS
  25. Метки, селекторы и организация ресурсов
  26. Сервисы и kube-proxy глубже: типы, IPVS, EndpointSlices (вы здесь)
  27. Ingress, ingress-контроллеры и Gateway API
  28. Секреты в кластере: шифрование, External Secrets, Vault
  29. Хранилище глубже: PV, StorageClass, CSI и StatefulSet
  30. Сеть кластера: CNI, NetworkPolicy и CoreDNS
  31. Планировщик: affinity, taints, topology spread
  32. Ресурсы, QoS и вытеснение: requests, limits, eviction
  33. Helm глубже: шаблоны, хуки, зависимости, OCI
  34. Kustomize и управление конфигурацией без шаблонов
  35. RBAC и аутентификация глубже
  36. Admission и Pod Security: контроль на входе
  37. Policy as code: Kyverno и OPA Gatekeeper
  38. GitOps: Argo CD и Flux
  39. Операторы и CRD: расширяем Kubernetes
  40. Сервис-меш: Istio, Linkerd и когда он нужен
  41. Эксплуатация кластера: апгрейд, узлы, бэкап etcd
  42. Где запускать кластер: managed, self-hosted, k3s, Deckhouse
  43. Траблшутинг кластера: Pending, CrashLoop, узлы NotReady
  44. Стоимость и эффективность кластера: FinOps
  45. CI/CD в Kubernetes: сборка, деплой, прогрессивные релизы
  46. Лучшие практики и антипаттерны Kubernetes
  47. Сквозной проект и путь дальше: от манифеста до прода
  48. Деплой приложений через Argo CD: Application, sync и App-of-Apps
Зачем вообще нужен Service, если у пода есть IP

У каждого пода в кластере есть собственный 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
Разберём вывод. ADDRESSTYPE может быть IPv4, IPv6 или FQDN - слайсы изначально спроектированы под dual-stack, чего старый Endpoints толком не умел. PORTS - порт контейнера. ENDPOINTS - адреса готовых подов. Заглянем глубже в один слайс.

Код: Выделить всё

$ 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
Ключевое - блок conditions. ready - под прошёл readinessProbe, на него можно слать новый трафик. serving - под отвечает, но может уже быть terminating; это разделение позволяет дренировать соединения корректно. terminating - под получил SIGTERM и доживает grace period. Поле zone - топологическая метка ноды, она понадобится для topology-aware routing ниже. apiVersion тут discovery.k8s.io/v1 - стабильный с 1.21, на нём всё и строится.

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"
Отдельно: многие современные кластеры вообще выкидывают kube-proxy и реализуют сервисы через eBPF (Cilium в режиме kube-proxy replacement). Там DNAT делается в eBPF-программах на сетевых хуках ядра - ещё быстрее и без таблиц вовсе. Если у тебя Cilium - проверь, не отключён ли kube-proxy совсем, иначе будешь дебажить правила, которых нет.

Типы 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   # порт на каждой ноде (можно не задавать - выберется сам)
LoadBalancer - надстройка над NodePort. Облачный cloud-controller-manager видит такой сервис и идёт в API провайдера: поднимает managed-балансировщик и направляет его на NodePort всех нод. В Yandex Managed Kubernetes так появляется L4-балансировщик Yandex Cloud, в VK Cloud - свой, в EKS - NLB/ELB. On-prem облачного API нет, поэтому LoadBalancer там по умолчанию висит в Pending - и тут на сцену выходит MetalLB: он раздаёт внешние IP из заданного пула либо через ARP/NDP (L2-режим), либо анонсами BGP. Deckhouse тащит свой аналог из коробки.

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
Source IP, externalTrafficPolicy и topology-aware routing

Самая частая боль в проде - потеря 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 клиента
Запомни как мантру: externalTrafficPolicy: Local - стандартный способ сохранить реальный IP клиента. Есть и брат для внутреннего трафика - internalTrafficPolicy: Local, который держит запрос на той же ноде (полезно для node-local кешей и снижения cross-AZ трафика).

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   # держать трафик в своей зоне
sessionAffinity. По умолчанию запросы одного клиента раскидываются по разным подам. Если нужна липкость (клиент должен попадать в тот же под) - sessionAffinity: ClientIP. Тогда kube-proxy привязывает source IP к поду на timeoutSeconds (по умолчанию 10800 = 3 часа). Это L3-липкость по IP, не по cookie - за NAT-ом сотни клиентов с одним IP прилипнут к одному поду, так что для веба это костыль, лучше внешний ingress с cookie-affinity.

Код: Выделить всё

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).
👍3 ❤️2 🔥1 😄 🤔1
Аватара пользователя
s08qqr
Сообщения: 1
Зарегистрирован: 24 май 2026, 19:34

Re: Сервисы и kube-proxy глубже: типы, IPVS, EndpointSlices

Сообщение s08qqr »

А я думал kube-proxy реально гоняет трафик через себя, как nginx. То есть он просто пишет правила в ядро, а пакеты ходят мимо него? Тогда понятно почему сервисы работают даже когда kube-proxy перезапускается.
👍 ❤️ 🔥 😄 🤔
Аватара пользователя
skifast
Сообщения: 1
Зарегистрирован: 03 июн 2026, 10:42

Re: Сервисы и kube-proxy глубже: типы, IPVS, EndpointSlices

Сообщение skifast »

Поймал ту самую граблю с gRPC: после скейла новые поды стояли пустые, весь трафик висел на старых соединениях. Теперь ясно - это conntrack держит установленные коннекты на одном поде. externalTrafficPolicy: Local тоже наконец встало на место, спасибо за разбор про SNAT.
👍2 ❤️ 🔥 😄 🤔1
Ответить
← Предыдущая глава
Метки, селекторы и организация ресурсов
Следующая глава →
Ingress, ingress-контроллеры и Gateway API

Все главы курса «Kubernetes: оркестрация контейнеров от основ до продакшена»

Поделиться темой: ✈ Telegram VK

Вернуться в «Kubernetes на практике»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость