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

Автор: | 30/10/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.

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

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

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

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