Kubernetes: Service, балансировка нагрузки, kube-proxy и iptables

Автор: | 10/30/2020
 

Задался я вопросом — а как вообще в 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-robin
  • lc: least connection (smallest number of open connections)
  • dh: destination hashing
  • sh: source hashing
  • sed: shortest expected delay
  • nq: never queue

kube-proxy config

Проверим, какой режиму нас используется в AWS Elastic Kubernetes Service.

Находим поды:

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>

На каждой WorkerNode кластера запущен свой инстанс kube-proxy, к которым монтируется ConfigMap с именем kube-proxy-config:

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

Смотрим содержимое этого ConfigMap:

kk -n kube-system get cm kube-proxy-config -o yaml
apiVersion: v1
data:
config: |-
...
mode: "iptables"
...

Теперь, когда мы рассмотрели режимы kube-proxy — проверим, как оно работает, и как тут задействован iptables.

Kubernetes Pod load-balancing

Для примера возьмём реальное приложение, у которого есть Ingress  (AWS Application Load Balancer, ALB), который направляет трафик на Kubernetes Service:

kk -n eks-dev-1-appname-ns get ingress appname-backend-ingress -o yaml
...
- backend:
serviceName: appname-backend-svc
servicePort: 80
...

Проверим сам Service:

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

Тип 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 хост, и с него — к рабочей ноде:

ubuntu@ip-10-3-29-14:~$ ssh ec2-user@10.3.49.200 -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]#

kube-proxy и iptables

Итак, наш пакет от клиента пришёл на рабочую ноду.

На ноде kube-proxy биндит этот порт, что бы его не занял никакой другой сервис, и создаёт правила в iptables:

[root@ip-10-3-49-200 ec2-user]# netstat -anp | grep 31103
tcp6       0      0 :::31103                :::*                    LISTEN      4287/kube-proxy

Пакет приходит на порт 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:

[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  */

Тут у нас target == следующая цепочка, KUBE-SERVICES, в которой последним правилом идёт следующая цепочка — KUBE-NODEPORTS, в которую попадают пакеты для Service с типом NodePort:

[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
...

Проверяем правила этой цепочки:

[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

Тут ловятся пакеты для dpt:31103 (destination port 31103) и отправляются в следующую цепочку — KUBE-SVC-TII5GQPKXWC65SRC, проверяем её:

[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

Тут следующие две цепочки, где собственно и происходит «магия» балансировки — пакет рандомно отправляется на одну из этих цепочек, 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.

Проверяем эти цепочки:

[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

И вторая:

[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

Где цепочка DNAT (Destination NAT) отправляет пакет на IP к порту 3001, который является нашим ContainerPort — проверим Deployment:

kk -n eks-dev-1-appname-ns get deploy appname-backend -o json | jq '.spec.template.spec.containers[].ports[].containerPort'
3001

И проверим IP наших подов — находим поды:

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

Их IP, первый под:

kk -n eks-dev-1-appname-ns get pod appname-backend-768ddf9f54-2nrp5 --template={{.status.podIP}}
10.3.34.219

И второй:

kk -n eks-dev-1-appname-ns get pod appname-backend-768ddf9f54-pm9bh --template={{.status.podIP}}
10.3.57.124

Всё логично? 🙂

Попробуем заскейлить деплоймент и посмотрим, как изменится правило в цепочке KUBE-SVC-TII5GQPKXWC65SRC.

Находим деплоймент:

kk -n eks-dev-1-appname-ns get deploy
NAME                          READY   UP-TO-DATE   AVAILABLE   AGE
appname-backend   2/2     2            2           64d

Скейлим его до трёх подов:

kk -n eks-dev-1-appname-ns scale deploy appname-backend --replicas=3
deployment.extensions/appname-backend scaled

Проверяем iptables:

[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

Теперь у нас в цепочке KUBE-SVC-TII5GQPKXWC65SRC три правила — у первого рандом 0.33332999982, т.к. есть три правила, в следущем правиле уже срабывает условие 0.5, и последнее — без правил.

См. iptables statistics module.

В целом, на этом всё.

Ссылки по теме