Задался я вопросом — а как вообще в Kubernetes происходит балансировка нагрузки между подами?
Т.е, есть у нас внешний Load Balancer. За ним — Service. За ним — Pod.
Что происходит, когда мы из мира получаем пакет, а у нас несколько подов — как пакеты между ними распределяются?
Содержание
kube-proxy
За правила маршрутизации пакетов между Service и Pod у нас отвечает сервис kube-proxy, который может работать в одном из трёх режимов — user space proxy mode, iptables proxy mode и IPVS proxy mode.
User space proxy mode
Ссылки:
Устаревший режим, ранее был режимом по-умолчанию.
При использовании user space proxy mode, kube-proxy отслеживает изменения в кластере и для каждого нового Service открывает TCP-порт на WorkerNode.
Далее, iptables на этой WorkerNode роутит трафик с этого порта к kube-proxy, который собственно проксирует пакеты к подам, используя режим round-robin, т.е. отправляя трафик на следующий в списке под. При этом, kube-proxy имеет возможность отправить пакет к другому поду, если первый недоступен.
iptables proxy mode
Ссылки:
Наш случай, который и разберём в этом посте. Используется по-умолчанию.
При использовании iptables mode, kube-proxy отслеживает изменения в кластере и для каждого нового Service открывает TCP-порт на WorkerNode.
Далее, iptables на этой WorkerNode роутит трафик с этого порта к Kubernetes Service, который по сути является цепочкой в правилах iptables, а через неё — напрямую к подам, которые являются бекендами этого сервиса, при этом поды выбираются рандомно.
Этот режим менее ресурсоёмкий для системы, так как все операции выполняются в ядре в модуле netfilter, быстрее, и более надёжен, потому что нет «прослойки» в виде проксирующего сервиса — самого kube-proxy.
Но если под, на который был направлен пакет, не отвечает — то такое соединение разрывается, тогда как в user space proxy mode — прокси попробовал бы другой под.
Вот, почему важно использовать и правильно настроить Readiness Probes — что бы Kubernetes не отправлял паакеты на такие поды.
Кроме того — такой режим сложнее дебажить, т.к. при user space proxy mode kube-proxy пишет лог в /var/log/kube-proxy, а в случае с netfilter — придётся отслеживать работу самого ядра.
IPVS proxy mode
Ссылки:
И самый новый режим, использующий модуль ядра netlink, и при создании новых Service создаёт соответсвующие правила PVS.
Главное преимущество — разнообразие вариантов балансировки нагрузки:
rr: round-robinlc: least connection (smallest number of open connections)dh: destination hashingsh: source hashingsed: shortest expected delaynq: never queue
kube-proxy config
Проверим, какой режиму нас используется в AWS Elastic Kubernetes Service.
Находим поды:
[simterm]
$ kubectl -n kube-system get pod -l k8s-app=kube-proxy -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES kube-proxy-4prtt 1/1 Running 1 158d 10.3.42.245 ip-10-3-42-245.us-east-2.compute.internal <none> <none> kube-proxy-5b7pd 1/1 Running 0 60d 10.3.58.133 ip-10-3-58-133.us-east-2.compute.internal <none> <none> kube-proxy-66cm5 1/1 Running 0 92d 10.3.58.193 ip-10-3-58-193.us-east-2.compute.internal <none> <none> kube-proxy-8fdsv 1/1 Running 0 70d 10.3.39.145 ip-10-3-39-145.us-east-2.compute.internal <none> <none> kube-proxy-8wbj2 1/1 Running 1 158d 10.3.49.200 ip-10-3-49-200.us-east-2.compute.internal <none> <none> kube-proxy-cnd9c 1/1 Running 1 158d 10.3.47.58 ip-10-3-47-58.us-east-2.compute.internal <none> <none> kube-proxy-cwppt 1/1 Running 0 158d 10.3.48.124 ip-10-3-48-124.us-east-2.compute.internal <none> <none> kube-proxy-dd75p 1/1 Running 1 158d 10.3.43.168 ip-10-3-43-168.us-east-2.compute.internal <none> <none> kube-proxy-p6hb7 1/1 Running 0 158d 10.3.46.137 ip-10-3-46-137.us-east-2.compute.internal <none> <none> kube-proxy-pfjzt 1/1 Running 0 59d 10.3.62.200 ip-10-3-62-200.us-east-2.compute.internal <none> <none> kube-proxy-spckd 1/1 Running 0 70d 10.3.44.14 ip-10-3-44-14.us-east-2.compute.internal <none> <none> kube-proxy-tgl52 1/1 Running 0 59d 10.3.59.159 ip-10-3-59-159.us-east-2.compute.internal <none> <none>
[/simterm]
На каждой WorkerNode кластера запущен свой инстанс kube-proxy, к которым монтируется ConfigMap с именем kube-proxy-config:
[simterm]
$ kk -n kube-system get pod kube-proxy-4prtt -o yaml
apiVersion: v1
kind: Pod
...
spec:
...
containers:
...
volumeMounts:
...
- mountPath: /var/lib/kube-proxy-config/
name: config
...
volumes:
...
- configMap:
defaultMode: 420
name: kube-proxy-config
name: config
[/simterm]
Смотрим содержимое этого ConfigMap:
[simterm]
$ kk -n kube-system get cm kube-proxy-config -o yaml
apiVersion: v1
data:
config: |-
...
mode: "iptables"
...
[/simterm]
Теперь, когда мы рассмотрели режимы kube-proxy — проверим, как оно работает, и как тут задействован iptables.
Kubernetes Pod load-balancing
Для примера возьмём реальное приложение, у которого есть Ingress (AWS Application Load Balancer, ALB), который направляет трафик на Kubernetes Service:
[simterm]
$ kk -n eks-dev-1-appname-ns get ingress appname-backend-ingress -o yaml
...
- backend:
serviceName: appname-backend-svc
servicePort: 80
...
[/simterm]
Проверим сам Service:
[simterm]
$ kk -n eks-dev-1-appname-ns get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE appname-backend-svc NodePort 172.20.249.22 <none> 80:31103/TCP 63d
[/simterm]
Тип NodePort — слушает TCP порт на WorkerNode.
AWS ALB e172ad3e-eksdev1appname-abac направляет трафик от клиентов на AWS TargetGroup e172ad3e-4caa286edf23ff7e06d:
ЕС2 в этой TargetGroup прослушивают порт 31103, который мы и видим в описании Service выше:
AWS LoadBalancer traffic modes
ALB поддерживает два типа трафика — IP, и Instance Mode.
- Instance mode: режим по-умолчанию, требует от Kubernetes Service типа NodePort, и направляет трафик на TCP-порт рабочей ноды
- IP mode: при этом режиме таргетами для ALB являются сами Kubernetes Pods, а не Kubernetes Worker Node.
Далее нам потребуется доступ к одной из рабочих нод — подключаемся на Bastion хост, и с него — к рабочей ноде:
[simterm]
ubuntu@ip-10-3-29-14:~$ ssh [email protected] -i .ssh/bttrm-eks-nodegroup-us-east-2.pem Last login: Thu May 28 06:25:27 2020 from ip-10-3-29-32.us-east-2.compute.internal __| __|_ ) _| ( / Amazon Linux 2 AMI ___|\___|___| https://aws.amazon.com/amazon-linux-2/ 39 package(s) needed for security, out of 64 available Run "sudo yum update" to apply all updates. [ec2-user@ip-10-3-49-200 ~]$ sudo -s [root@ip-10-3-49-200 ec2-user]#
[/simterm]
kube-proxy и iptables
Итак, наш пакет от клиента пришёл на рабочую ноду.
На ноде kube-proxy биндит этот порт, что бы его не занял никакой другой сервис, и создаёт правила в iptables:
[simterm]
[root@ip-10-3-49-200 ec2-user]# netstat -anp | grep 31103 tcp6 0 0 :::31103 :::* LISTEN 4287/kube-proxy
[/simterm]
Пакет приходит на порт 31107, где начинают работать правила фильтрации iptables.
Правила iptables
Ссылки:
- https://www.frozentux.net/iptables-tutorial/chunkyhtml/c962.html
- https://upload.wikimedia.org/wikipedia/commons/3/37/Netfilter-packet-flow.svg
- https://www.digitalocean.com/community/tutorials/a-deep-dive-into-iptables-and-netfilter-architecture
Ядро принимает пакет, который попадает в цепочку (chain) PREROUTING таблицы nat:
См. describing kube-proxy iptables rules.
Проверяем правила в nat таблице и её цепочке PREROUTING:
[simterm]
[root@ip-10-3-49-200 ec2-user]# iptables -t nat -L PREROUTING | column -t Chain PREROUTING (policy ACCEPT) target prot opt source destination KUBE-SERVICES all -- anywhere anywhere /* kubernetes service portals */
[/simterm]
Тут у нас target == следующая цепочка, KUBE-SERVICES, в которой последним правилом идёт следующая цепочка — KUBE-NODEPORTS, в которую попадают пакеты для Service с типом NodePort:
[simterm]
[root@ip-10-3-49-200 ec2-user]# iptables -t nat -L KUBE-SERVICES -n | column -t ... KUBE-NODEPORTS all -- 0.0.0.0/0 0.0.0.0/0 /* kubernetes service nodeports; NOTE: this must be the last rule in th is chain */ ADDRTYPE match dst-type LOCAL ...
[/simterm]
Проверяем правила этой цепочки:
[simterm]
[root@ip-10-3-49-200 ec2-user]# iptables -t nat -L KUBE-NODEPORTS -n | column -t | grep 31103 KUBE-MARK-MASQ tcp -- 0.0.0.0/0 0.0.0.0/0 /* eks-dev-1-appname-ns/appnamed-backend-svc: */ tcp dpt:31103 KUBE-SVC-TII5GQPKXWC65SRC tcp -- 0.0.0.0/0 0.0.0.0/0 /* eks-dev-1-appname-ns/appname-backend-svc: */ tcp dpt:31103
[/simterm]
Тут ловятся пакеты для dpt:31103 (destination port 31103) и отправляются в следующую цепочку — KUBE-SVC-TII5GQPKXWC65SRC, проверяем её:
[simterm]
[root@ip-10-3-49-200 ec2-user]# iptables -t nat -L KUBE-SVC-TII5GQPKXWC65SRC | column -t Chain KUBE-SVC-TII5GQPKXWC65SRC (2 references) target prot opt source destination KUBE-SEP-N36I6W2ULZA2XU52 all -- anywhere anywhere statistic mode random probability 0.50000000000 KUBE-SEP-4NFMB5GS6KDP7RHJ all -- anywhere anywhere
[/simterm]
Тут следующие две цепочки, где собственно и происходит «магия» балансировки — пакет рандомно отправляется на одну из этих цепочек, 0.5 из 1.0 веса каждой — statistic mode random probability 0.5, как и сказано в документации:
By default, kube-proxy in iptables mode chooses a backend at random.
См. также Turning IPTables into a TCP load balancer for fun and profit.
Проверяем эти цепочки:
[simterm]
[root@ip-10-3-49-200 ec2-user]# iptables -t nat -L KUBE-SEP-N36I6W2ULZA2XU52 -n | column -t Chain KUBE-SEP-N36I6W2ULZA2XU52 (1 references) target prot opt source destination KUBE-MARK-MASQ all -- 10.3.34.219 0.0.0.0/0 DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp to:10.3.34.219:3001
[/simterm]
И вторая:
[simterm]
[root@ip-10-3-49-200 ec2-user]# iptables -t nat -L KUBE-SEP-4NFMB5GS6KDP7RHJ -n | column -t Chain KUBE-SEP-4NFMB5GS6KDP7RHJ (1 references) target prot opt source destination KUBE-MARK-MASQ all -- 10.3.57.124 0.0.0.0/0 DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp to:10.3.57.124:3001
[/simterm]
Где цепочка DNAT (Destination NAT) отправляет пакет на IP к порту 3001, который является нашим ContainerPort — проверим Deployment:
[simterm]
$ kk -n eks-dev-1-appname-ns get deploy appname-backend -o json | jq '.spec.template.spec.containers[].ports[].containerPort' 3001
[/simterm]
И проверим IP наших подов — находим поды:
[simterm]
$ kk -n eks-dev-1-appname-ns get pod NAME READY STATUS RESTARTS AGE appname-backend-768ddf9f54-2nrp5 1/1 Running 0 3d appname-backend-768ddf9f54-pm9bh 1/1 Running 0 3d
[/simterm]
Их IP, первый под:
[simterm]
$ kk -n eks-dev-1-appname-ns get pod appname-backend-768ddf9f54-2nrp5 --template={{.status.podIP}}
10.3.34.219
[/simterm]
И второй:
[simterm]
$ kk -n eks-dev-1-appname-ns get pod appname-backend-768ddf9f54-pm9bh --template={{.status.podIP}}
10.3.57.124
[/simterm]
Всё логично? 🙂
Попробуем заскейлить деплоймент и посмотрим, как изменится правило в цепочке KUBE-SVC-TII5GQPKXWC65SRC.
Находим деплоймент:
[simterm]
$ kk -n eks-dev-1-appname-ns get deploy NAME READY UP-TO-DATE AVAILABLE AGE appname-backend 2/2 2 2 64d
[/simterm]
Скейлим его до трёх подов:
[simterm]
$ kk -n eks-dev-1-appname-ns scale deploy appname-backend --replicas=3 deployment.extensions/appname-backend scaled
[/simterm]
Проверяем iptables:
[simterm]
[root@ip-10-3-49-200 ec2-user]# iptables -t nat -L KUBE-SVC-TII5GQPKXWC65SRC | column -t Chain KUBE-SVC-TII5GQPKXWC65SRC (2 references) target prot opt source destination KUBE-SEP-N36I6W2ULZA2XU52 all -- anywhere anywhere statistic mode random probability 0.33332999982 KUBE-SEP-HDIQCDRXRXBGRR55 all -- anywhere anywhere statistic mode random probability 0.50000000000 KUBE-SEP-4NFMB5GS6KDP7RHJ all -- anywhere anywhere
[/simterm]
Теперь у нас в цепочке KUBE-SVC-TII5GQPKXWC65SRC три правила — у первого рандом 0.33332999982, т.к. есть три правила, в следущем правиле уже срабывает условие 0.5, и последнее — без правил.
См. iptables statistics module.
В целом, на этом всё.
Ссылки по теме
- Kubernetes: ClusterIP vs NodePort vs LoadBalancer, Services и Ingress — обзор, примеры
- Kubernetes Networking Demystified: A Brief Guide
- A Deep Dive into Iptables and Netfilter Architecture
- Traversing of tables and chains
- Turning IPTables into a TCP load balancer for fun and profit
- Cracking kubernetes node proxy (aka kube-proxy)
- A minimal IPVS Load Balancer demo
- How to enable IPVS mode on AWS EKS?
- Load-Balancing in Kubernetes





