Задался я вопросом – а как вообще в 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