kube-proxy: iptables, IPVS, nftables
В прошлом уроке мы выяснили: ClusterIP — это виртуальный IP, реализованный правилами в netfilter. Теперь препарируем сам процесс генерации этих правил. Кто их пишет, какие они, как они работают на больших кластерах.
kube-proxy — это компонент, который превращает декларативные объекты Service/EndpointSlice в реальные данные в kernel: iptables-цепочки, IPVS-таблицы или nftables-правила. Без kube-proxy Services не работают (за исключением кластеров где его роль выполняет CNI типа Cilium через eBPF).
Алгоритмы балансировки нагрузки: round-robin, least-connections, consistent hashing
Что такое kube-proxy
kube-proxy — отдельный компонент, который запускается на каждой ноде (обычно как DaemonSet kube-proxy в namespace kube-system). Его job description прост:
- Watch на apiserver-е: все Services и EndpointSlices в кластере.
- На каждое изменение — пересобрать правила в netfilter (или IPVS/nftables) на локальной ноде.
- Эти правила реализуют: ClusterIP → DNAT на один из endpoint IPs, NodePort на каждой ноде, балансировку.
$ kubectl get ds -n kube-system kube-proxy
NAME DESIRED CURRENT READY AGE
kube-proxy 3 3 3 45d
$ kubectl get pods -n kube-system -l k8s-app=kube-proxy
NAME READY STATUS RESTARTS AGE
kube-proxy-abc12 1/1 Running 0 45d
kube-proxy-def34 1/1 Running 0 45d
kube-proxy-xyz56 1/1 Running 0 45d
Pod kube-proxy запускается с hostNetwork: true (использует netns ноды) и имеет permissions писать в iptables/IPVS — ему нужен NET_ADMIN.
В кластерах с Cilium с kubeProxyReplacement: true — kube-proxy не нужен, его роль играют eBPF-программы Cilium. Это уже не редкость в современных prod-кластерах. Но на CKAD считаем kube-proxy default.
Режимы работы
kube-proxy умеет три режима:
| Mode | Status (v1.35) | Default | Производительность |
|---|---|---|---|
| iptables | Stable | Default (исторически и в v1.35) | O(N) lookup на правилах |
| IPVS | GA с v1.11 | Нет | O(1) через hash table |
| nftables | alpha v1.29 → beta v1.31 → GA v1.33 | Нет (opt-in через --proxy-mode=nftables) | O(1) хешированием |
Выбирается через kube-proxy config:
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: "iptables" # или "ipvs", или "nftables"
На CKAD вас НЕ просят настраивать kube-proxy mode — это CKA-территория. Но знать что такое existует и в чём разница — обязательно. Особенно понимать, что Service “реализуется” правилами kube-proxy, а не магией.
iptables mode: цепочки правил
Самый известный режим (по умолчанию долгие годы). kube-proxy пишет правила в iptables (точнее в подсистему nat table) через цепочки KUBE-SERVICES, KUBE-SVC-XXXX, KUBE-SEP-XXXX.
Структура цепочек
PREROUTING / OUTPUT (kernel hooks)
↓
KUBE-SERVICES ← главная цепочка, по одному правилу на Service
↓
match dst=ClusterIP:port → jump to KUBE-SVC-XXXX
↓
KUBE-SVC-XXXX ← цепочка одного Service
↓
probability-based выбор → jump to одной из KUBE-SEP-YYYY
↓
KUBE-SEP-YYYY ← конечная точка, один endpoint
↓
DNAT to PodIP:targetPort
Пример из реального кластера
Service web (ClusterIP 10.96.0.10:80) с тремя endpoints: 10.244.1.5:8080, 10.244.2.7:8080, 10.244.3.9:8080.
$ iptables -t nat -L KUBE-SERVICES -n
Chain KUBE-SERVICES (2 references)
target prot source destination
KUBE-SVC-7XYZAB tcp -- anywhere 10.96.0.10 tcp dpt:80 /* default/web */
$ iptables -t nat -L KUBE-SVC-7XYZAB -n
Chain KUBE-SVC-7XYZAB (1 references)
target prot source destination
KUBE-SEP-AAAA all -- anywhere anywhere statistic mode random probability 0.33333
KUBE-SEP-BBBB all -- anywhere anywhere statistic mode random probability 0.50000
KUBE-SEP-CCCC all -- anywhere anywhere
$ iptables -t nat -L KUBE-SEP-AAAA -n
Chain KUBE-SEP-AAAA (1 references)
target prot source destination
DNAT tcp -- anywhere anywhere tcp to:10.244.1.5:8080
Probability-based load balancing
iptables не умеет round-robin нативно. Используется хак через модуль statistic:
правило 1: probability 0.33333 → 33% пакетов
правило 2: probability 0.50000 → 50% от ОСТАВШИХСЯ (33% от общего)
правило 3: без вероятности → все остальные (33% от общего)
Формула вероятности для k-го из N endpoints (1-индекс):
probability(k) = 1 / (N - k + 1)
Для 3 endpoints: 1/3 ≈ 0.333, 1/2 = 0.5, 1.0 (последний берёт остаток). Получается равномерное распределение.
Что такое KUBE-MARK-MASQ
Кроме DNAT, в цепочках есть KUBE-MARK-MASQ — маркирует пакеты, для которых нужен source NAT (MASQUERADE). Это нужно когда:
- Pod пишет на собственный Service ClusterIP (loopback hairpin) — без MASQUERADE backend отвергнет пакет.
- Трафик из external (NodePort) идёт на Pod на другой ноде — иначе client IP мог бы сбить routing назад.
Маркер MASQ обрабатывается в POSTROUTING цепочке: пакеты с этой меткой получат SNAT на NodeIP.
Проблема O(N) на больших кластерах
Каждый Service — это правила в KUBE-SERVICES (одно на Service) и в KUBE-SVC-XXXX (N правил на N endpoints). При N сервисах и M Pods за каждым:
KUBE-SERVICES: N правил- Все
KUBE-SVC-XXXX: суммарно N×M правил
iptables match — это linear scan: каждый пакет проходит правила по порядку. На 5000 services × 10 endpoints = 50000 правил. Это значительный latency на каждый сетевой connect.
Хуже того: при каждом изменении (Pod restart, новый Service) kube-proxy перегенерирует и применяет весь набор правил через iptables-restore. На больших кластерах это занимает секунды и блокирует netfilter. Поэтому iptables-mode не масштабируется выше ~5000 Services.
IPVS mode: kernel L4 load balancer
IPVS (IP Virtual Server) — это часть Linux kernel, специально разработанная для L4 load balancing. В отличие от iptables, IPVS хранит правила в hash table — O(1) lookup. Изначально использовался как фронтенд для веб-кластеров (LVS).
При mode: ipvs kube-proxy создаёт:
- Виртуальный интерфейс
kube-ipvs0на ноде, к которому привязывает ClusterIPs (ip addr add 10.96.0.10/32 dev kube-ipvs0). - IPVS виртуальные серверы (virtual server) — по одному на Service:port. Каждый имеет список real servers (Pod IPs).
- Iptables всё равно используется (только для
KUBE-MARK-MASQи base path), но без огромного дерева правил для каждого Service.
$ ipvsadm -L -n
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 10.96.0.10:80 rr
-> 10.244.1.5:8080 Masq 1 3 12
-> 10.244.2.7:8080 Masq 1 2 15
-> 10.244.3.9:8080 Masq 1 4 11
Алгоритмы балансировки
В отличие от iptables (только probability), IPVS поддерживает много алгоритмов:
- rr (round-robin) — default, циклически
- wrr (weighted round-robin) — с весами
- lc (least-connections) — на endpoint с наименьшим числом активных соединений
- wlc (weighted least-connections)
- sh (source hashing) — детерминированно по client IP (sticky без conntrack)
- dh (destination hashing)
Конфигурируется через kube-proxy config:
mode: ipvs
ipvs:
scheduler: rr
Преимущества и недостатки
Плюсы:
- O(1) lookup — масштабируется на 10000+ Services.
- Меньше CPU на каждый пакет.
- Больше алгоритмов LB.
Минусы:
- Сложнее debug — kernel state в отдельной таблице, не видно через
iptables-save. - Требует загруженных модулей kernel:
ip_vs,ip_vs_rr,ip_vs_wrr,nf_conntrack. - В bridge-mode конфигурациях нужны дополнительные sysctl (
bridge-nf-call-iptables=1).
nftables mode: современная альтернатива
С Linux kernel 3.13 разрабатывалась замена iptables — nftables. У него тот же концепт (правила в kernel netfilter), но:
- Hash-based matching для set-ов и map-ов — фактически O(log N) или O(1).
- Один унифицированный backend для NAT/filter/mangle вместо четырёх iptables-таблиц.
- Атомарные обновления —
nft -f rules.nftприменяет batch без race conditions. - Более выразительный синтаксис.
В Kubernetes nftables mode kube-proxy прошёл путь alpha v1.29 → beta v1.31 → GA v1.33. На v1.35 default остаётся iptables для обратной совместимости, nftables включается явно через --proxy-mode=nftables. Он генерирует nftables-правила вместо iptables, используя map-ы для hash-based lookup ClusterIP → endpoint.
# Пример nft правила kube-proxy:
table ip kube-proxy {
chain services {
ip daddr . tcp dport vmap @cluster-ips
}
map cluster-ips {
type ipv4_addr . inet_service : verdict
elements = {
10.96.0.10 . 80 : jump svc-web,
10.96.0.20 . 443 : jump svc-api,
}
}
}
Lookup через vmap — это hash, O(1). На больших кластерах nftables mode значительно быстрее iptables и сравним с IPVS, но без необходимости поддерживать отдельный модуль.
К моменту v1.35 nftables-mode стабилен и доступен как opt-in (--proxy-mode=nftables); default kube-proxy mode остаётся iptables. Команды начинают мигрировать большие кластеры на nftables/eBPF (Cilium kube-proxy replacement). На CKAD важно знать, что nftables-режим существует и решает O(N) проблему iptables.
Killer момент: посмотреть реальные правила
На любой ноде с iptables-mode kube-proxy можно увидеть всё это в живую:
# Все KUBE-SVC цепочки
sudo iptables -t nat -S | grep KUBE-SVC
# Цепочки для одного Service по имени (комментарий /* namespace/name */)
sudo iptables -t nat -S | grep "default/web"
# Полный путь от ClusterIP до DNAT для Service web
sudo iptables -t nat -L KUBE-SERVICES | grep "default/web"
# KUBE-SVC-7XYZAB tcp -- anywhere 10.96.0.10 tcp dpt:80 /* default/web */
sudo iptables -t nat -L KUBE-SVC-7XYZAB
# KUBE-SEP-AAAA all -- anywhere anywhere /* default/web */ statistic mode random probability 0.33333
# KUBE-SEP-BBBB all -- anywhere anywhere /* default/web */ statistic mode random probability 0.50000
# KUBE-SEP-CCCC all -- anywhere anywhere /* default/web */
sudo iptables -t nat -L KUBE-SEP-AAAA
# DNAT tcp -- anywhere anywhere tcp to:10.244.1.5:8080
Для IPVS:
ipvsadm -L -n -t 10.96.0.10:80
# Видим Service и его real servers (endpoints)
Для nftables:
nft list table ip kube-proxy
# Видим chains и maps
externalTrafficPolicy: Cluster vs Local
Для NodePort / LoadBalancer Service есть externalTrafficPolicy (default Cluster):
spec:
type: NodePort
externalTrafficPolicy: Local # или Cluster
Cluster (default): трафик пришёл на любую ноду → kube-proxy DNAT-ит на любой endpoint в кластере (даже на другой ноде). Плюс: равномерный LB. Минус: дополнительный hop, source IP клиента теряется (заменяется на NodeIP при SNAT).
Local: пакет обрабатывается только если на этой ноде есть endpoint. Если нет — пакет отброшен. Плюс: source IP сохраняется, нет лишнего hop. Минус: неравномерная нагрузка (если на ноде 1 endpoint, она получит больше трафика).
externalTrafficPolicy: Local
external client → NodeA:30080 → [есть Pod на NodeA? yes] → Pod
[no] → DROP
Local критично если приложению нужен real client IP (логи, rate limit, geo).
kube-proxy и Pods на той же ноде: short-circuit?
В iptables-mode пакет от Pod-а на ClusterIP того же Service-а может попасть на Pod на этой же ноде — но только если probability ему так выпала. Никакого “prefer local” в default-mode нет (это делает internalTrafficPolicy: Local — отдельная фича).
В Cilium с eBPF — там есть прямой short-circuit: пакет к локальному backend идёт минуя сетевой стек, через socket-level redirect.