kube-proxy в режиме iptables создаёт правила iptables, которые направляют трафик Service к endpoint'ам через цепочки KUBE-SERVICES, KUBE-SVC-* и KUBE-SEP-*, выполняя DNAT к поду. Отдельный процесс пакеты не обрабатывает: трансляцию выполняет правило iptables в ядре. Такой подход даёт почти бесплатную диагностику: счётчики pkts и bytes в правилах показывают, сколько пакетов реально ушло на каждый под.
В режиме IPVS та же задача решается виртуальными сервисами ядра, и IPVS обеспечивает лучшую масштабируемость и производительность для больших кластеров по сравнению с режимом iptables. Разница проявляется на масштабе: в кластерах с сотней сервисов оба режима ведут себя похоже, а на тысячах сервисов iptables выполняет заметно больше работы при каждом обновлении endpoints.
Дальше разбираем путь пакета от ClusterIP до пода, механику обоих режимов, правила попадания подов в балансировку через readiness-пробы, настройку sessionAffinity и пошаговую диагностику неравномерной нагрузки. Команды рассчитаны на кластеры Kubernetes 1.28 и новее на Linux-нодах.
Как Kubernetes балансирует трафик: роль kube-proxy
Что такое kube-proxy и зачем он нужен
kube-proxy запускается на каждой ноде как DaemonSet (в managed-кластерах роль ноды может выполнять отдельный агент) и реализует абстракцию Service. Компонент следит за объектами Service и EndpointSlice через API-сервер и программирует правила в ядре, чтобы виртуальный IP сервиса превращался в реальный IP пода.
Классическим балансировщиком kube-proxy не назвать: в режимах iptables и IPVS он не принимает соединения и не трогает payload. Он только пишет правила ядра. Исключение - userspace-режим, который в 2026 году считается устаревшим и в продакшене не применяется.
В связке с CNI kube-proxy отвечает за адресацию в сервисной сети, а CNI обеспечивает связность подов и маршруты между нодами. Если kube-proxy не запустился, Service типа ClusterIP перестаёт отвечать, хотя поды живы и доступны по своим IP. Смежные сценарии, где адресация упирается в маршруты CNI, разобраны в материале про маршрутизацию между сервисами в Kubernetes.
Режим работы задаётся флагом --proxy-mode=iptables|ipvs в конфиге kube-proxy или через ConfigMap kube-proxy в namespace kube-system. Проверить текущий режим можно так:
kubectl -n kube-system get configmap kube-proxy -o yaml | grep mode kubectl -n kube-system logs -l k8s-app=kube-proxy --tail=50 | grep -i "proxy mode"
Путь пакета от клиента до пода
Шаги для режима iptables:
- Клиент отправляет пакет на ClusterIP сервиса и порт, например 10.96.0.10:80.
- Пакет попадает в PREROUTING (трафик извне ноды) или OUTPUT (трафик с самой ноды) и передаётся в цепочку KUBE-SERVICES.
- В KUBE-SERVICES находится правило для конкретного ClusterIP, которое переводит обработку в KUBE-SVC-XXXX.
- KUBE-SVC распределяет пакет по цепочкам KUBE-SEP-* с заданными вероятностями.
- KUBE-SEP выполняет DNAT: адрес назначения меняется на IP и порт пода.
- Ядро фиксирует трансляцию в conntrack, поэтому обратные пакеты вернутся клиенту с исходным ClusterIP.
Выбор endpoint происходит на ноде-источнике. Ответ идёт напрямую от пода, если он в той же подсети, или через маршруты CNI. Посмотреть правила для конкретного сервиса можно так:
iptables -t nat -L KUBE-SERVICES -n -v | grep 10.96.0.10 iptables -t nat -L KUBE-SVC-XXXXXXXXXXXXXXXX -n -v iptables -t nat -L KUBE-SEP-YYYYYYYYYYYYYYYY -n -v
В режиме IPVS путь короче по числу шагов: пакет попадает в виртуальный сервис ядра, планировщик выбирает real server, дальше работает обычная трансляция адресов. Список сервисов и бэкендов выводится одной командой:
ipvsadm -Ln ipvsadm -Ln --stats
Если нужен разбор HTTP-заголовков, cookies, retry и circuit breaking, смотрите маршрутизацию в Service Mesh на Istio и Linkerd: kube-proxy работает на L4 и о содержимом запроса ничего не знает.
Режимы kube-proxy: iptables против IPVS
iptables: как это работает под капотом
Для каждого сервиса kube-proxy создаёт цепочку KUBE-SVC-<hash>, для каждого endpoint - цепочку KUBE-SEP-<hash>. В KUBE-SVC пакет направляется в SEP по статистическому правилу: при двух endpoints вероятность 0.5 на каждый, при трёх - 0.33, последний endpoint в цепочке идёт без вероятности и получает остаток трафика.
Выглядит это примерно так:
Chain KUBE-SVC-ABC123XYZ (1 references) pkts bytes target prot opt source destination 120 7200 KUBE-SEP-EP01 tcp -- 0.0.0.0/0 10.96.0.10 statistic mode random probability 0.50000000000 98 5880 KUBE-SEP-EP02 tcp -- 0.0.0.0/0 10.96.0.10
При изменении набора endpoints kube-proxy пересобирает затронутые цепочки. В режиме iptables kube-proxy создаёт несколько правил на каждый Service и на каждый IP-адрес endpoint; в кластерах с десятками тысяч Pod и Service это означает десятки тысяч правил, и kube-proxy может долго обновлять правила в ядре при изменении Service или EndpointSlice.
Отдельная особенность режима: набор правил растёт линейно. Чем больше сервисов и endpoints, тем длиннее цепочки, а поиск нужного правила выполняется последовательным обходом. Статистику по конкретному сервису удобно смотреть с флагом -v, чтобы увидеть pkts и bytes и оценить, какой под реально получает трафик:
iptables -t nat -L KUBE-SVC-ABCDEFGH -n -v watch -n 5 'iptables -t nat -L KUBE-SEP- -n -v | grep -E "KUBE-SEP|pkts"'
IPVS: производительность и алгоритмы
IPVS - это балансировщик L4 внутри ядра Linux, который хранит виртуальные сервисы и real servers. В режиме ipvs kube-proxy создаёт для каждого Service виртуальный сервис, а для каждого endpoint - real server, и назначает планировщик. IPVS обеспечивает лучшую масштабируемость и производительность для больших кластеров по сравнению с режимом iptables, а для хранения адресов, которым нужен DROP или masquerade, IPVS-прокси использует ipset, чтобы число правил iptables оставалось постоянным независимо от количества сервисов.
Поддерживаемые алгоритмы планировщика включают rr (round-robin), wrr (взвешенный round-robin), lc (наименьшее число соединений), wlc (взвешенный lc), dh (хеш по IP назначения), sh (хеш по IP источника), sed и nq. Алгоритм задаётся в конфигурации kube-proxy:
--proxy-mode=ipvs --ipvs-scheduler=rr --ipvs-min-sync-period=5s --ipvs-sync-period=30s
Для работы IPVS-режима нужно обеспечить наличие необходимых модулей ядра IPVS. Проверьте и загрузите их заранее, иначе kube-proxy не поднимется:
modprobe ip_vs modprobe ip_vs_rr modprobe ip_vs_wrr modprobe ip_vs_sh modprobe nf_conntrack lsmod | grep ip_vs
Диагностика режима сводится к двум командам: ipvsadm -Ln показывает таблицу виртуальных сервисов и их real servers, ipvsadm -Ln --stats добавляет счётчики Connections, InPkts и OutPkts по каждому бэкенду. Именно эти счётчики помогают проверить, что нагрузка распределяется равномерно.
Сравнительная таблица и критерии выбора
| Параметр | iptables | IPVS |
|---|---|---|
| Механизм выбора бэкенда | последовательный обход цепочек nat и статистические правила | виртуальные сервисы и real servers ядра |
| Стоимость поиска при росте числа сервисов | растёт вместе с числом правил | ниже за счёт постоянного числа правил iptables (ipset) |
| Алгоритмы балансировки | случайный выбор по вероятностям, алгоритм не настраивается | rr, wrr, lc, wlc, dh, sh, sed, nq |
| Обновление при изменениях endpoints | пересборка затронутых цепочек, полная синхронизация больших наборов | обновление виртуальных сервисов и real servers |
| Нагрузка на CPU ноды | растёт при тысячах сервисов и частых событиях | ниже на тех же объёмах, зависит от числа real servers |
| Требования к ядру | модули iptables или nftables, nf_conntrack | необходимые модули ядра IPVS |
| Инструменты отладки | iptables -t nat -L -n -v, счётчики pkts и bytes по каждому SEP | ipvsadm -Ln, ipvsadm -Ln --stats |
| sessionAffinity | через conntrack, таймаут задаёт sessionAffinityConfig | через persistent connections |
Практические критерии: если в кластере меньше тысячи сервисов, нет жалоб на задержку и CPU нод не упирается в kube-proxy, режим iptables закрывает задачу без дополнительных модулей и с понятной отладкой по счётчикам. Если сервисов тысячи, нужны алгоритмы вроде lc или sh, а перезапуск kube-proxy из-за долгой синхронизации стал рутиной, переходите на IPVS и заранее проверьте наличие модулей ip_vs на всех нодах.
Сравнение выше описывает общее поведение обоих режимов; часть деталей зависит от версии Kubernetes и дистрибутива, поэтому перед сменой режима в продакшене сверяйтесь с документацией своей версии и прогоняйте изменения на staging. При переключении перезапустите kube-proxy и убедитесь, что старые правила в nat очищены: смешение цепочек iptables и таблиц IPVS даёт трудноуловимые ошибки маршрутизации.
Как Kubernetes выбирает endpoints для балансировки
Список endpoints складывается из двух условий: метки пода совпадают с селектором Service, а readiness-проба проходит. kube-proxy получает эти данные от API-сервера, фильтрует endpoints и маршрутизирует трафик только на актуальные адреса (политика трафика внутри сервиса, kubernetes.io).
Смотреть список endpoints нужно в EndpointSlice: объект Endpoints признан устаревшим и на больших кластерах раздувается до сотен килобайт на один сервис.
kubectl get endpoints my-service -o wide kubectl get endpointslice -l kubernetes.io/service-name=my-service -o yaml kubectl get pods -l app=my-app -o wide
В выводе EndpointSlice у готовых подов conditions.ready имеет значение true, а у неготовых поды либо отсутствуют, либо помечены ready: false. Если все поды не готовы, Service ведёт себя как сервис без endpoints: соединение к ClusterIP отклоняется или зависает до таймаута.
Роль readiness-проб в балансировке
readinessProbe определяет готовность пода принимать трафик. Пока проба не проходит, под исключается из endpoints, а kube-proxy перестаёт отправлять на него пакеты. livenessProbe решает другую задачу: она перезапускает контейнер, но не управляет попаданием в балансировку.
containers:
- name: api
image: example/api:1.4.2
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
successThreshold: 1
Проверка статуса:
kubectl describe pod api-7d9f8c6b5-x2k4m | grep -A5 Readiness
kubectl get pod api-7d9f8c6b5-x2k4m -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}'
kubectl get endpointslice -l kubernetes.io/service-name=api -o jsonpath='{range .items[*].endpoints[*]}{.addresses[0]}{" ready="}{.conditions.ready}{"\n"}{end}'
Слишком агрессивные параметры дают флаппинг: под то появляется в endpoints, то исчезает. На коротких интервалах это выглядит как неравномерная нагрузка, потому что часть трафика перенаправляется на оставшиеся поды. Ориентируйтесь на periodSeconds от 5 до 10, failureThreshold от 3 и endpoint, который отвечает быстрее timeoutSeconds без обращения к внешним зависимостям.
Service Internal Traffic Policy: локальные endpoints
Поле internalTrafficPolicy управляет трафиком, который идёт от подов внутри кластера. Со значением Local трафик направляется только на endpoints, расположенные на том же узле, что и под-источник. Это снижает количество переходов между нодами и расходы на межзонный трафик (Kubernetes, политика трафика внутри сервиса).
apiVersion: v1
kind: Service
metadata:
name: api
spec:
selector:
app: api
ports:
- port: 80
targetPort: 8080
internalTrafficPolicy: Local
Подводный камень описан в документации: для подов на узлах, где нет ни одного локального endpoint этого сервиса, Service ведёт себя так, как будто endpoints отсутствуют, даже если они есть на других узлах (kubernetes.io). Если поды распределены неравномерно, часть из них потеряет доступ к сервису. Проверить значение можно так:
kubectl get service api -o jsonpath='{.spec.internalTrafficPolicy}{"\n"}'
kubectl get pods -o wide --field-selector spec.nodeName=worker-1 | grep api
По умолчанию поле принимает значение Cluster, и трафик может уйти на любую ноду с готовым подом. Значение Local выбирайте для DaemonSet-подобных сценариев и сервисов, где задержка до бэкенда критична, а на каждом узле гарантированно есть локальный под.
sessionAffinity: привязка клиентских сессий к поду
По умолчанию sessionAffinity равно None, и каждый новый запрос уходит на произвольный готовый под. Для приложений, которые хранят состояние в памяти процесса, включают режим ClientIP: все запросы с одного IP клиента получают один и тот же бэкенд, пока не истечёт таймаут. sessionAffinity поддерживает значения "ClientIP" и "None".
Настройка sessionAffinity в Service
apiVersion: v1
kind: Service
metadata:
name: legacy-app
spec:
selector:
app: legacy-app
ports:
- port: 80
targetPort: 8080
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds: 10800
Проверить действующие настройки и убедиться, что привязка активна:
kubectl get service legacy-app -o yaml | grep -A3 sessionAffinity kubectl describe service legacy-app | grep -i session
В режиме iptables привязка опирается на таблицу conntrack: первый пакет создаёт запись, последующие пакеты с того же IP попадают в тот же бэкенд. В режиме IPVS используется механизм persistent connections: в выводе ipvsadm для виртуального сервиса отображается флаг persistent со значением 10800. Проверьте фактическое значение таймаута в выводе ipvsadm и при необходимости сверьтесь с конфигурацией kube-proxy.
Проверка и подводные камни sessionAffinity
Проверка простая: сделайте несколько запросов с одной машины и убедитесь, что отвечает один и тот же под. Если приложение возвращает имя пода, это видно в выводе сразу.
for i in $(seq 1 10); do curl -s http://legacy-app.default.svc.cluster.local/hostname; done conntrack -L -d 10.96.0.45 2>/dev/null | head ipvsadm -Ln | grep -A3 10.96.0.45
Главный риск: клиенты за NAT или корпоративным прокси выглядят для кластера как один IP, и весь их трафик идёт на единственный под. Второй нюанс: sessionAffinity не применяется к headless-сервисам, там клиент сам выбирает адрес из DNS-ответа. Третий: при удалении пода, к которому была привязана сессия, запросы переключатся на другой бэкенд, и приложение должно уметь работать с потерей состояния.
Диагностика балансировки: проверка правил и выявление неравномерной нагрузки
Порядок действий одинаков для обоих режимов: сначала проверяем, что трафик вообще есть куда направлять, потом смотрим статистику распределения, затем изучаем логи компонента.
Проверка правил iptables для сервиса
KUBE-SVC - это цепочка сервиса, KUBE-SEP - цепочка конкретного endpoint. Счётчики в них и показывают распределение.
iptables -t nat -L KUBE-SERVICES -n | grep 10.96.0.45 iptables -t nat -L KUBE-SVC-ABCDEF123456 -n -v iptables -t nat -L KUBE-SEP-111122223333 -n -v
Интерпретация: если на одном KUBE-SEP счётчик pkts равен нулю, а на других растёт, значит под либо не проходит readiness-пробу, либо выпал из селектора, либо трафик к нему блокирует NetworkPolicy. Если pkts растёт на всех, но распределение перекошено в разы, дело в статистике правил или в conntrack, который держит длинные соединения на уже выбранных бэкендах.
Выявление неравномерной нагрузки
Типовые причины неравномерности: активная sessionAffinity, неготовые поды, длинные TCP-соединения в conntrack и случайный выбор бэкенда в режиме iptables. Случайный выбор выравнивается только на больших объёмах запросов, на коротком окне перекос в 60 на 40 процентов - нормальная картина.
# распределение по подам через логи приложения kubectl logs -l app=api --since=10m | grep -c "request" # сброс залипших записей conntrack после изменения endpoints conntrack -D -p tcp --dport 8080 # статистика по real servers в IPVS ipvsadm -Ln --stats
Если нужен предсказуемо ровный трафик, переходите на IPVS с планировщиком rr или lc: round-robin раскладывает новые соединения по бэкендам по очереди, а lc отправляет запрос на под с наименьшим числом активных соединений, что заметно лучше работает при разной длительности запросов. Проверенный набор приёмов для L7-уровня, включая least_conn и ip_hash, собран в статье про балансировку нагрузки и маршрутизацию в Nginx и Kubernetes.
Логи и метрики kube-proxy
kube-proxy пишет в лог ошибки синхронизации правил и проблемы с доступом к API-серверу. Для разбора инцидента поднимите уровень детализации до --v=4 и повторите проблемный сценарий.
kubectl -n kube-system logs -l k8s-app=kube-proxy --tail=200 | grep -iE "error|sync|failed" journalctl -u kube-proxy -n 200 --no-pager curl -s http://localhost:10249/metrics | grep -E "kubeproxy_sync_proxy_rules_duration_seconds|kubeproxy_sync_partial_proxy_rules_duration_seconds|kubeproxy_sync_proxy_rules_last_timestamp_seconds"
Метрика kubeproxy_sync_proxy_rules_duration_seconds_count отражает задержку SyncProxyRules в секундах (count), а kubeproxy_sync_partial_proxy_rules_duration_seconds измеряет ту же задержку для частичных ресинков. Метрика kubeproxy_sync_proxy_rules_last_timestamp_seconds показывает время последней успешной синхронизации правил прокси. Рост этих значений вместе с числом сервисов стоит отслеживать, но однозначного порога, после которого нужно переходить на IPVS, документация не задаёт: решение принимайте по совокупности задержек, нагрузки на CPU и частоты событий в кластере.
Практические рекомендации и чек-лист
- Выберите режим kube-proxy: iptables для кластеров до тысячи сервисов без жалоб на задержки, IPVS для больших кластеров и сценариев, где нужны алгоритмы планирования.
- Перед переключением на IPVS проверьте наличие необходимых модулей ядра IPVS на всех нодах и перезапустите kube-proxy.
- Настройте readinessProbe для каждого сервиса: неготовые поды не должны получать трафик, а параметры проб не должны вызывать флаппинг.
- Используйте internalTrafficPolicy: Local только там, где на каждом узле гарантированно есть локальный под, иначе часть подов потеряет доступ к сервису.
- Включайте sessionAffinity: ClientIP только для приложений с состоянием в памяти и помните про клиентов за NAT.
- Регулярно сверяйте список endpoints через kubectl get endpointslice и статистику правил через iptables -t nat -L -n -v или ipvsadm -Ln --stats.
- Следите за метриками kubeproxy_sync_proxy_rules_duration_seconds_count, kubeproxy_sync_partial_proxy_rules_duration_seconds и kubeproxy_sync_proxy_rules_last_timestamp_seconds, а также за логами kube-proxy с уровнем --v=4 при разборе инцидентов.
- При неравномерной нагрузке последовательно проверьте endpoints, sessionAffinity, записи conntrack и выбранный планировщик.
- Любые изменения режима и таймаутов прогоняйте на staging: смена режима kube-proxy затрагивает сетевую связность всего кластера.
Начните с одного сервиса: выведите его цепочки командой iptables -t nat -L KUBE-SVC-XXXX -n -v или его real servers через ipvsadm -Ln --stats, зафиксируйте распределение pkts в моменте, а затем повторите замер через десять минут под нагрузкой. Такой замер даёт точку отсчёта, по которой видно эффект от смены режима, правки readiness-проб или включения internalTrafficPolicy: Local.