Вообще, этот пост планировался в виде небольшой заметки о том, как использовать NodeAffinity
для Kubernetes Pod:
Но, как это часто бывает – за одним потянулось другое, за другим третье – и в результате вышел очередной длиннопост в свободном стиле.
Итак, собирался я написать про NodeAffinity, как вдруг подумал – а как будет себя вести Kubernetes cluster-autoscaler
– будет ли он учитывать NodeAffinity при создании новых нод?
Что бы проверить это – решил провести небольшой нагрузочный тест с помощью Apach Benchmark, что бы затриггерить Kubernetes HorizontalPodAutoscaler, который должен создать поды, а поды – затриггерить cluster-autoscaler
на создание новых AWS ЕС2, которые будут подключены к Kubernetes-кластеру в роли WorkerNodes.
Начал самое первое нагрузочное тестирование, и столкнулся с проблемой, когда поды перестали скейлится.
А потом решил, что раз уж тестирую – то можно попробовать разные типы инстансов. Ну и об этом тоже написать.
А потом мы начали уже “полномастабное” нагрузочное тестирование – и проявилось ещё несколько проблем, и, конечно – надо же записать, как их решал.
В результате – получился пост с описанием и процесса нагрузочного тестирования, и про типы инстансов, и про DNS, и про другие нюансы работы с нагруженным приложением в Kubernetes.
kk
тут: alias kk="kubectl" > ~/.bashrc
Содержание
Задача
Итак, у нас имеется приложение, которое прям ну очень любит CPU.
PHP, Laravel. Сейчас крутится в DigitalOcean, на 50-ти одновременно запущенных дроплетах, плюс NFS-шара между ними для кода (отдельный NFS-сервер), плюс memcache, Redis и MySQL.
Хотим вынести приложение в Kubernetes-кластер с автоскейлингом и сэкономить на инфрастуктуре, т.к. за нынешние сервера в DigitalOcean мы платим в районе 4000 долларов/мес, а за один кластер AWS EKS – сейчас уходит порядка 500-600 долларов (сам кластер + по 4 штуки AWS t3.medium EC2 серверов для WorkerNodes в двух независимых AWS AvailabilityZone, итого 8 серверов под весь кластер).
При этом приложение падало на 12.000 одновременных пользователей (48.000/час). Мы хотим добить до 15.000 одновременных (60.000 час, 1.440.000/сутки), при максимуме, который был с момента запуска проекта осенью 2019, в 600.000/сутки. При этом текущий Production раздаётся через Cloudflare CDN, который из кеша отдаёт до 90% запросов.
Проект будем выносить на отдельную группу рабочих нод, что бы не затрагивать работу остальных приложений в кластере. Для того, что бы поды приложения запускались только на этих серверах – используем NodeAffinity
.
Кроме того, проведём нагрузочное тестирование с тремя типами AWS EC2 инстансов – t3, m5, c5, что бы определиться с тем, какой тип лучше подходит, а потом ещё одно нагрузочное – что бы проверить работу автоскейлинга и самого приложения.
Выбор EC2 type
Что выбрать?
- Т3? Burstable процессоры, хорошее соотношение цены/ЦПУ/памяти, подходят для большинства задач:
T3 instances are the next generation burstable general-purpose instance type that provide a baseline level of CPU performance with the ability to burst CPU usage at any time for as long as required. - М5? Заточены под память – меньше ЦПУ/больше памяти:
M5 instances are the latest generation of General Purpose Instances powered by Intel Xeon® Platinum 8175M processors. This family provides a balance of compute, memory, and network resources, and is a good choice for many applications. - С5? Заточены под ЦПУ – больше ядер процессора, сам процессор немного лучше, но памяти, по сравнению с М5, меньше:
C5 instances are optimized for compute-intensive workloads and deliver cost-effective high performance at a low price per compute ratio.
Давайте начнём с t3a – будет и немного дешевле, чем обычный Т3, и бурст CPU есть, а нагрузки у нас не линейные, а спорадические.
EC2 AMD instances
Имеются так же инстансы на процессорах AMD – t3a, m5a, c5a – при тех же показателях CPU/memory/network они выходят немного дешевле, но доступны не во всех регионах и даже не во всех AvailabilityZone одного и того же региона.
Например, в AWS регионе us-east-2 c5a доступны в зонах доступности us-east-2b и us-east-2c – но недоступны в us-east-2a. Менять сейчас автоматизацию (а AZ выбирается автоматически, см. AWS: CloudFormation — использование lists в Parameters) не хочется, поэтому AMD оставим в стороне.
EC2 Graviton instances
Кроме того, у AWS появились инстансы m6g и c6g с AWS Graviton2 processors, но для их использования есть определённые требования к AWS EKS кластеру, см. тут>>>, а заниматься сейчас апдейтом кластеров возможности нет.
Далее – создадим три WorkerNode Group с типами инстансов t3, m5 и c5, и проверим работу приложения и потребление CPU на каждом из них.
eksctl
и Kubernetes WorkerNode Groups
Конфиг для создаваемых нод-групп выглядит так:
--- apiVersion: eksctl.io/v1alpha5 kind: ClusterConfig metadata: name: "{{ eks_cluster_name }}" region: "{{ region }}" version: "{{ k8s_version }}" nodeGroups: - name: "eat-test-t3-{{ item }}" instanceType: "t3.xlarge" privateNetworking: true labels: { role: eat-workers } volumeSize: 50 volumeType: gp2 desiredCapacity: 1 minSize: 1 maxSize: 1 availabilityZones: ["{{ item }}"] ssh: publicKeyName: "bttrm-eks-nodegroup-{{ region }}" iam: withAddonPolicies: autoScaler: true cloudWatch: true albIngress: true efs: true securityGroups: withShared: true withLocal: true attachIDs: [ {{ worker_nodes_add_sg }} ] - name: "eat-test-m5-{{ item }}" instanceType: "m5.xlarge" privateNetworking: true labels: { role: eat-workers } volumeSize: 50 volumeType: gp2 desiredCapacity: 1 minSize: 1 maxSize: 1 availabilityZones: ["{{ item }}"] ssh: publicKeyName: "bttrm-eks-nodegroup-{{ region }}" iam: withAddonPolicies: autoScaler: true cloudWatch: true albIngress: true efs: true securityGroups: withShared: true withLocal: true attachIDs: [ {{ worker_nodes_add_sg }} ] - name: "eat-test-c5-{{ item }}" instanceType: "c5.xlarge" privateNetworking: true labels: { role: eat-workers } volumeSize: 50 volumeType: gp2 desiredCapacity: 1 minSize: 1 maxSize: 1 availabilityZones: ["{{ item }}"] ssh: publicKeyName: "bttrm-eks-nodegroup-{{ region }}" iam: withAddonPolicies: autoScaler: true cloudWatch: true albIngress: true efs: true securityGroups: withShared: true withLocal: true attachIDs: [ {{ worker_nodes_add_sg }} ]
Тут описываются три Worker Node группы с тремя различными типами инстансов.
Деплоится всё Ансиблом и eksctl
, см. AWS Elastic Kubernetes Service: — автоматизация создания кластера, часть 2 — Ansible, eksctl, в двух различных AvailabilityZone.
minSize
и maxSize
заданы в 1 и 1, что бы наш Cluster AutoScaler не начал их скейлить – в начале тестирования хочется увидеть нагрузку на один сервер одним подом, и посмотреть на результаты kubectl top
для подов и нод.
После того, как определимся с типом инстансов – оставим одну нод-группу, и включим скейлинг.
Тестирование – план
Что и как будем тестировать:
- PHP, Laravel, упакованы в Docker-образы
- все сервера 4 ядра, 16 гиг памяти
- в Deployment проекта с помощью
requests
опишем запуск по одному поду на 1 сервер (будем “просить” чуть больше, чем половина доступных ядер – и Kubernetes Scheduler вынужден будет запускать поды на выделенных нодах) - с помощью
NodeAffinity
опишем запуск подов только на нужных нам серверах - автоскейлинг подов и кластера пока отключен
Создадим три WorkerNode Group с тремся различными типами ЕС2 инстансов, и задеплоим четыре неймспейса – один “дефолтный”, и три – под разные нод-группы с разными типами серверов – в каждом таком неймспейсе в Deployment будет прописан свой NodeAffinity
, который будет запускать поды только на нужных нам серверах.
Соответственно, у нас будет создано 4 Ingress
ресурса с AWS LoadBalancer, см. Kubernetes: ClusterIP vs NodePort vs LoadBalancer, Services и Ingress — обзор, примеры, и будет 4 ендпоинта для тестирования.
Kubernetes NodeAffinity && nodeSelector
Документация – Assigning Pods to Nodes.
Для выбора того, на каких именно WorkerNode запускать поды с приложением можно использовать два варианта – либо собственные лейблы, либо лейблы, добавляемые самим Кубером автоматически.
В нашем конфиге нод групп добавляется такая лейбла:
... labels: { role: eat-workers } ...
Которая будет прикреплена к каждому ЕС2.
Обновляем кластер:
И все теги инстанса:
Проверим WorkerNode Groups в eksctl
:
[simterm]
$ eksctl --profile arseniy --cluster bttrm-eks-dev-1 get nodegroups CLUSTER NODEGROUP CREATED MIN SIZE MAX SIZE DESIRED CAPACITY INSTANCE TYPE IMAGE ID bttrm-eks-dev-1 eat-test-c5-us-east-2a 2020-08-20T09:29:28Z 1 1 1 c5.xlarge ami-0f056ad53eddfda19 bttrm-eks-dev-1 eat-test-c5-us-east-2b 2020-08-20T09:34:54Z 1 1 1 c5.xlarge ami-0f056ad53eddfda19 bttrm-eks-dev-1 eat-test-m5-us-east-2a 2020-08-20T09:29:28Z 1 1 1 m5.xlarge ami-0f056ad53eddfda19 bttrm-eks-dev-1 eat-test-m5-us-east-2b 2020-08-20T09:34:54Z 1 1 1 m5.xlarge ami-0f056ad53eddfda19 bttrm-eks-dev-1 eat-test-t3-us-east-2a 2020-08-20T09:29:27Z 1 1 1 t3.xlarge ami-0f056ad53eddfda19 bttrm-eks-dev-1 eat-test-t3-us-east-2b 2020-08-20T09:34:54Z 1 1 1 t3.xlarge ami-0f056ad53eddfda19
[/simterm]
Проверяем созданные WorkerNode ЕС2-инстансы, с помощью -l
выбрав сервера с нашей кастомной лейблой “role: eat-workers” и отсортировав по типам инстансов:
[simterm]
$ kk -n eks-dev-1-eat-backend-ns get node -l role=eat-workers -o=json | jq -r '[.items | sort_by(.metadata.labels["beta.kubernetes.io/instance-type"])[] | {name:.metadata.name, type:.metadata.labels["beta.kubernetes.io/instance-type"], region:.metadata.labels["failure-domain.beta.kubernetes.io/zone"]}]' [ { "name": "ip-10-3-47-253.us-east-2.compute.internal", "type": "c5.xlarge", "region": "us-east-2a" }, { "name": "ip-10-3-53-83.us-east-2.compute.internal", "type": "c5.xlarge", "region": "us-east-2b" }, { "name": "ip-10-3-33-222.us-east-2.compute.internal", "type": "m5.xlarge", "region": "us-east-2a" }, { "name": "ip-10-3-61-225.us-east-2.compute.internal", "type": "m5.xlarge", "region": "us-east-2b" }, { "name": "ip-10-3-45-186.us-east-2.compute.internal", "type": "t3.xlarge", "region": "us-east-2a" }, { "name": "ip-10-3-63-119.us-east-2.compute.internal", "type": "t3.xlarge", "region": "us-east-2b" } ]
[/simterm]
См. форматирование аутпута kubectl
тут>>>.
Deployment update
nodeSelector
by a custom label
Сначала задеплоим на сервера с лейблой labels: { role: eat-workers }
– Kubernetes должен будет запустить поды на всех 6-ти серверах – по 2 на каждый тип инстанса.
Обновляем деплоймент, добавляем nodeSelector
по лейбле role со значением “eat-workers“:
apiVersion: apps/v1 kind: Deployment metadata: name: {{ .Chart.Name }} annotations: reloader.stakater.com/auto: "true" spec: replicas: {{ .Values.replicaCount }} strategy: type: RollingUpdate selector: matchLabels: application: {{ .Chart.Name }} template: metadata: labels: application: {{ .Chart.Name }} version: {{ .Chart.Version }}-{{ .Chart.AppVersion }} managed-by: {{ .Release.Service }} spec: containers: - name: {{ .Chart.Name }} image: {{ .Values.image.registry }}/{{ .Values.image.repository }}/{{ .Values.image.name }}:{{ .Values.image.tag }} imagePullPolicy: Always ... ports: - containerPort: {{ .Values.appConfig.port }} livenessProbe: httpGet: path: {{ .Values.appConfig.healthcheckPath }} port: {{ .Values.appConfig.port }} initialDelaySeconds: 10 readinessProbe: httpGet: path: {{ .Values.appConfig.healthcheckPath }} port: {{ .Values.appConfig.port }} initialDelaySeconds: 10 resources: requests: cpu: {{ .Values.resources.requests.cpu | quote }} memory: {{ .Values.resources.requests.memory | quote }} nodeSelector: role: eat-workers volumes: imagePullSecrets: - name: gitlab-secret
replicaCount
задаём в 6, по числу серверов
Деплоим:
[simterm]
$ helm secrets upgrade --install --namespace eks-dev-1-eat-backend-ns --set image.tag=179217391 --set appConfig.appEnv=local --set appConfig.appUrl=https://dev-eks.eat.example.com/ --atomic eat-backend . -f secrets.dev.yaml --debug
[/simterm]
Проверяем:
[simterm]
$ kk -n eks-dev-1-eat-backend-ns get pod -o=custom-columns=NAME:.metadata.name,STATUS:.status.phase,NODE:.spec.nodeName,TYPE:.spec.nodeSelector NAME STATUS NODE TYPE eat-backend-57b7b54d98-7m27q Running ip-10-3-63-119.us-east-2.compute.internal map[role:eat-workers] eat-backend-57b7b54d98-7tvtk Running ip-10-3-53-83.us-east-2.compute.internal map[role:eat-workers] eat-backend-57b7b54d98-8kphq Running ip-10-3-47-253.us-east-2.compute.internal map[role:eat-workers] eat-backend-57b7b54d98-l24wr Running ip-10-3-61-225.us-east-2.compute.internal map[role:eat-workers] eat-backend-57b7b54d98-ns4nr Running ip-10-3-45-186.us-east-2.compute.internal map[role:eat-workers] eat-backend-57b7b54d98-sxzk4 Running ip-10-3-33-222.us-east-2.compute.internal map[role:eat-workers] eat-backend-memcached-0 Running ip-10-3-63-119.us-east-2.compute.internal <none>
[/simterm]
Хорошо – 6 подов на 6 серверах.
nodeSelector
by Kuber label
Теперь обновим наш деплоймент, и используем лейблы, которые создаются Kubernetes автоматически, например beta.kubernetes.io/instance-type, используя который зададим деплоймент подов только на сервера определённого типа.
replicaCount
задаём в 2, по числу серверов каждого типа – получим по одному поду на каждый из двух серверов.
Удаляем текущий деплоймент:
[simterm]
$ helm --namespace eks-dev-1-eat-backend-ns uninstall eat-backend release "eatt-backend" uninstalled
[/simterm]
Обновляем манифест деплоймента, – добавляем выбор t3, будут работать оба условия – и role
, и instance-type
:
... nodeSelector: beta.kubernetes.io/instance-type: t3.xlarge role: eat-workers ...
Деплоим в новые неймспейсы – добавим постфикс t3, m5, c5, т.е. для t3 группы имя нейспейса будет “eks-dev-1-eat–backend-ns-t3“, при вызове Helm добавляем --create-namespace
:
[simterm]
$ helm secrets upgrade --install --namespace eks-dev-1-eat-backend-ns-t3 --set image.tag=180029557 --set appConfig.appEnv=local --set appConfig.appUrl=https://t3-dev-eks.eat.example.com/ --atomic eat-backend . -f secrets.dev.yaml --debug --create-namespace
[/simterm]
Повторяем для m5, c5, проверяем.
t3:
[simterm]
$ kk -n eks-dev-1-eat-backend-ns-t3 get pod -o=custom-columns=NAME:.metadata.name,STATUS:.status.phase,NODE:.spec.nodeName,TYPE:.spec.nodeSelector NAME STATUS NODE TYPE eat-backend-cc9b8cdbf-tv9h5 Running ip-10-3-45-186.us-east-2.compute.internal map[beta.kubernetes.io/instance-type:t3.xlarge role:eat-workers] eat-backend-cc9b8cdbf-w7w5w Running ip-10-3-63-119.us-east-2.compute.internal map[beta.kubernetes.io/instance-type:t3.xlarge role:eat-workers] eat-backend-memcached-0 Running ip-10-3-53-83.us-east-2.compute.internal <none>
[/simterm]
m5:
[simterm]
$ kk -n eks-dev-1-eat-backend-ns-m5 get pod -o=custom-columns=NAME:.metadata.name,STATUS:.status.phase,NODE:.spec.nodeName,TYPE:.spec.nodeSelector NAME STATUS NODE TYPE eat-backend-7dfb56b75c-k8gt6 Running ip-10-3-61-225.us-east-2.compute.internal map[beta.kubernetes.io/instance-type:m5.xlarge role:eat-workers] eat-backend-7dfb56b75c-wq9n2 Running ip-10-3-33-222.us-east-2.compute.internal map[beta.kubernetes.io/instance-type:m5.xlarge role:eat-workers] eat-backend-memcached-0 Running ip-10-3-47-253.us-east-2.compute.internal <none>
[/simterm]
И c5:
[simterm]
$ kk -n eks-dev-1-eat-backend-ns-c5 get pod -o=custom-columns=NAME:.metadata.name,STATUS:.status.phase,NODE:.spec.nodeName,TYPE:.spec.nodeSelector NAME STATUS NODE TYPE eat-backend-7b6778c5c-9g6st Running ip-10-3-47-253.us-east-2.compute.internal map[beta.kubernetes.io/instance-type:c5.xlarge role:eat-workers] eat-backend-7b6778c5c-sh5sn Running ip-10-3-53-83.us-east-2.compute.internal map[beta.kubernetes.io/instance-type:c5.xlarge role:eat-workers] eat-backend-memcached-0 Running ip-10-3-47-58.us-east-2.compute.internal <none>
[/simterm]
Всё готово к тестированию.
Тестирование – AWS EC2 t3 vs m5 vs c5
Запускаем тесты, один и тот же набор на все группы.
Следим за потреблением CPU каждым подом.
t3
Поды:
[simterm]
$ kk top nod-n eks-dev-1-eat-backend-ns-t3 top pod NAME CPU(cores) MEMORY(bytes) eat-backend-79cfc4f9dd-q22rh 1503m 103Mi eat-backend-79cfc4f9dd-wv5xv 1062m 106Mi eat-backend-memcached-0 1m 2Mi
[/simterm]
Ноды:
[simterm]
$ kk top node -l role=eat-workers,beta.kubernetes.io/instance-type=t3.xlarge NAME CPU(cores) CPU% MEMORY(bytes) MEMORY% ip-10-3-45-186.us-east-2.compute.internal 1034m 26% 1125Mi 8% ip-10-3-63-119.us-east-2.compute.internal 1616m 41% 1080Mi 8%
M5
Поды:
[simterm]
$ kk -n eks-dev-1-eat-backend-ns-m5 top pod NAME CPU(cores) MEMORY(bytes) eat-backend-6f5d68778d-484lk 1039m 114Mi eat-backend-6f5d68778d-lddbw 1207m 105Mi eat-backend-memcached-0 1m 2Mi
[/simterm]
Ноды:
[simterm]
$ kk top node -l role=eat-workers,beta.kubernetes.io/instance-type=m5.xlarge NAME CPU(cores) CPU% MEMORY(bytes) MEMORY% ip-10-3-33-222.us-east-2.compute.internal 1550m 39% 1119Mi 8% ip-10-3-61-225.us-east-2.compute.internal 891m 22% 1087Mi 8%
C5
Поды:
[simterm]
$ kk -n eks-dev-1-eat-backend-ns-c5 top pod NAME CPU(cores) MEMORY(bytes) eat-backend-79b947c74d-mkgm9 941m 103Mi eat-backend-79b947c74d-x5qjd 905m 107Mi eat-backend-memcached-0 1m 2Mi
[/simterm]
Ноды:
[simterm]
$ kk top node -l role=eat-workers,beta.kubernetes.io/instance-type=c5.xlarge NAME CPU(cores) CPU% MEMORY(bytes) MEMORY% ip-10-3-47-253.us-east-2.compute.internal 704m 17% 1114Mi 19% ip-10-3-53-83.us-east-2.compute.internal 1702m 43% 1122Mi 19%
Собственно – с этим закончили.
Резюмируя:
- t3: 1000-1500 mCPU, 385 ms responce
- m5: 1000-1200 mCPU, 371 ms responce
- c5: 900-1000 mCPU, 370 ms responce
Остановимся пока на С5 – они вроде себя лучше всего показали.
Kubernetes NodeAffinity vs Kubernetes ClusterAutoscaler
Один из вопросов, который волновал с самого начала – а будет ли Cluster AutoScaler учитывать NodeAffinity
?
Забегая наперёд – да, учитывает.
Наш HorizontalPodAutoscaler
выглядит так:
--- apiVersion: autoscaling/v2beta2 kind: HorizontalPodAutoscaler metadata: name: {{ .Chart.Name }}-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: {{ .Chart.Name }} minReplicas: {{ .Values.hpa.minReplicas }} maxReplicas: {{ .Values.hpa.maxReplicas }} metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: {{ .Values.hpa.cpuUtilLimit }}
cpuUtilLimit
задаём в 30%, с той идеей, что как только PHP начнёт активно использовать своих FPM-воркеров – вырастет нагрузка на процессор, а лимит в 30% даст нам время на запуск новых ЕС2 и подов в них, пока текущие поды вытягивают имеющуюся, но растущую нагрузку.
См. Kubernetes: HorizontalPodAutoscaler — обзор и примеры.
nodeSelector
описываем уже используя шаблонизатор Helm и его values.yaml
, см. Helm: Kubernetes package manager — обзор, начало работы:
... nodeSelector: beta.kubernetes.io/instance-type: {{ .Values.nodeSelector.instanceType | quote }} role: {{ .Values.nodeSelector.role | quote }} ...
Сам values.yaml
:
... nodeSelector: instanceType: "c5.xlarge" role: "eat-workers ...
Пересоздаём всё, и начинаем полноценное тестирование.
$ kk -n eks-dev-1-eat-backend-ns top pod NAME CPU(cores) MEMORY(bytes) eat-backend-b8b79574-8kjl4 50m 55Mi eat-backend-b8b79574-8t2pw 39m 55Mi eat-backend-b8b79574-bq8nw 52m 68Mi eat-backend-b8b79574-swbvq 40m 55Mi eat-backend-memcached-0 2m 6Mi
$ kk top node -l role=eat-workers NAME CPU(cores) CPU% MEMORY(bytes) MEMORY% ip-10-3-34-151.us-east-2.compute.internal 105m 2% 1033Mi 18% ip-10-3-39-132.us-east-2.compute.internal 110m 2% 1081Mi 19% ip-10-3-54-32.us-east-2.compute.internal 166m 4% 1002Mi 17% ip-10-3-56-98.us-east-2.compute.internal 106m 2% 1010Mi 17%
HorizontalPodAutoscaler
на 30% реквестов CPU:$ kk -n eks-dev-1-eat-backend-ns get hpa NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE eat-backend-hpa Deployment/eat-backend 1%/30% 4 40 4 6m27s
[/simterm]
Нагрузочное тестирование
День первый
В двух словах – тестирование, день первый. Сам пост начал писать на второй день, так что тут мы немного вернёмся во вчера.
Тестировали на четырёх t3a.medium инстансах, с теми же 1 под на 1 ноду, с включенным HPA и Cluster AutoScaler.
И всё шло хорошо, пока мы не превысили порог в 8000 одновременных юзеров – резко выросло время ответа:
И поды перестали скейлится:
Так как перестали генерить нагрузку выше 30%.
Первое предположение оказалось и правильным: в подах для PHP-FPM был задан OnDemand
, и максимум 5 воркеров, см. PHP-FPM: Process Manager — dynamic vs ondemand vs static.
FPM запускал 5 процессов, которые не нагружали ЦПУ выше 30% от реквестов, и поды переставали скейлиться.
На второй день мы переключили их в Dynamic
(на третий скорее переключим в Static
), и максимум 50 процессов – после этого нагрузка генерировалась постоянно, и поды скейлились.
Хотя, разумеется, можно просто добавить ещё одно условие для HPA, например – кол-во запросов к LoadBalancer в секунду, и позже именно так и сделаем, см. Kubernetes: мониторинг кластера с Prometheus Operator.
День второй
Итак – грузим JMeter, те же тесты, что вчера (и завтра).
Начинаем с 1, разгоняем до 15000 одновременных пользователей.
Инфрастуктура текущего Production в DigitalOcean падала на 12000 – мы хотим вытащить 15000.
Поехали:
На 3300 юзеров – начал триггерится скейлинг подов:
[simterm]
... 0s Normal SuccessfulRescale HorizontalPodAutoscaler New size: 5; reason: cpu resource utilization (percentage of request) above target 0s Normal ScalingReplicaSet Deployment Scaled up replica set eat-backend-b8b79574 to 5 0s Normal SuccessfulCreate ReplicaSet Created pod: eat-backend-b8b79574-l68vq 0s Warning FailedScheduling Pod 0/12 nodes are available: 12 Insufficient cpu, 8 node(s) didn't match node selector. 0s Warning FailedScheduling Pod 0/12 nodes are available: 12 Insufficient cpu, 8 node(s) didn't match node selector. 0s Normal TriggeredScaleUp Pod pod triggered scale-up: [{eksctl-bttrm-eks-dev-1-nodegroup-eat-us-east-2b-NodeGroup-1N0QUROWQ8K2Q 2->3 (max: 20)}] ...
[/simterm]
И для них начали скейлится новые ЕС2-ноды:
[simterm]
$ kk -n eks-dev-1-eat-backend-ns top pod NAME CPU(cores) MEMORY(bytes) eat-backend-b8b79574-8kjl4 968m 85Mi eat-backend-b8b79574-8t2pw 1386m 85Mi eat-backend-b8b79574-bq8nw 737m 71Mi eat-backend-b8b79574-l68vq 0m 0Mi eat-backend-b8b79574-swbvq 573m 71Mi eat-backend-memcached-0 20m 15Mi [/simterm] [simterm] $ kk -n eks-dev-1-eat-backend-ns get hpa NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE eat-backend-hpa Deployment/eat-backend 36%/30% 4 40 5 37m [/simterm] [simterm] $ kk top node -l role=eat-workers NAME CPU(cores) CPU% MEMORY(bytes) MEMORY% ip-10-3-34-151.us-east-2.compute.internal 662m 16% 1051Mi 18% ip-10-3-39-132.us-east-2.compute.internal 811m 20% 1095Mi 19% ip-10-3-53-136.us-east-2.compute.internal 2023m 51% 567Mi 9% ip-10-3-54-32.us-east-2.compute.internal 1115m 28% 1032Mi 18% ip-10-3-56-98.us-east-2.compute.internal 1485m 37% 1040Mi 18%
[/simterm]
5500 – полёт нормальный, скейлимся дальше:
net/http: request canceled (Client.Timeout exceeded while awaiting headers)
Где-то на 7-8 тысячах начались проблемы – поды отваливались по Liveness и Readiness проверкам с ошибкой “Client.Timeout exceeded while awaiting headers“:
[simterm]
0s Warning Unhealthy Pod Liveness probe failed: Get http://10.3.38.7:80/: net/http: request canceled (Client.Timeout exceeded while awaiting headers) 1s Warning Unhealthy Pod Readiness probe failed: Get http://10.3.44.96:80/: net/http: request canceled (Client.Timeout exceeded while awaiting headers) 0s Normal MODIFY Ingress rule 1 modified with conditions [{ Field: "path-pattern", Values: ["/*"] }] 0s Warning Unhealthy Pod Liveness probe failed: Get http://10.3.44.34:80/: net/http: request canceled (Client.Timeout exceeded while awaiting headers)
[/simterm]
И чем дальше – тем хуже, на 10.000:
Поды начали падать вообще, а самое “весёлое”, что у нас даже не было нормального вывода логов приложения – оно по-прежнему писало в лог-файл в контейнерах, поправили это только на третий день.
Нагрузка выглядела так:
[simterm]
$ kk -n eks-dev-1-eat-backend-ns get hpa NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE eat-backend-hpa Deployment/eat-backend 60%/30% 4 40 15 63m [/simterm] [simterm] $ kk top node -l role=eat-workers NAME CPU(cores) CPU% MEMORY(bytes) MEMORY% ip-10-3-33-155.us-east-2.compute.internal 88m 2% 951Mi 16% ip-10-3-34-151.us-east-2.compute.internal 1642m 41% 1196Mi 20% ip-10-3-39-128.us-east-2.compute.internal 67m 1% 946Mi 16% ip-10-3-39-132.us-east-2.compute.internal 73m 1% 1029Mi 18% ip-10-3-43-76.us-east-2.compute.internal 185m 4% 1008Mi 17% ip-10-3-47-243.us-east-2.compute.internal 71m 1% 959Mi 16% ip-10-3-47-61.us-east-2.compute.internal 69m 1% 945Mi 16% ip-10-3-53-124.us-east-2.compute.internal 61m 1% 955Mi 16% ip-10-3-53-136.us-east-2.compute.internal 75m 1% 946Mi 16% ip-10-3-53-143.us-east-2.compute.internal 1262m 32% 1110Mi 19% ip-10-3-54-32.us-east-2.compute.internal 117m 2% 985Mi 17% ip-10-3-55-140.us-east-2.compute.internal 992m 25% 931Mi 16% ip-10-3-55-208.us-east-2.compute.internal 76m 1% 942Mi 16% ip-10-3-56-98.us-east-2.compute.internal 1578m 40% 1152Mi 20% ip-10-3-59-239.us-east-2.compute.internal 1661m 42% 1175Mi 20% [/simterm] [simterm] $ kk -n eks-dev-1-eat-backend-ns top pod NAME CPU(cores) MEMORY(bytes) eat-backend-b8b79574-5d6zl 0m 0Mi eat-backend-b8b79574-7n7pq 986m 184Mi eat-backend-b8b79574-8t2pw 709m 135Mi eat--backend-b8b79574-bq8nw 0m 0Mi eat-backend-b8b79574-ds68n 0m 0Mi eat-backend-b8b79574-f4qcm 0m 0Mi eat-backend-b8b79574-f6wfj 0m 0Mi eat-backend-b8b79574-g7jm7 842m 165Mi eat-backend-b8b79574-ggrdg 0m 0Mi eat-backend-b8b79574-hjcnh 0m 0Mi eat-backend-b8b79574-l68vq 0m 0Mi eat-backend-b8b79574-mlpqs 0m 0Mi eat-backend-b8b79574-nkwjc 2882m 103Mi eat-backend-b8b79574-swbvq 2091m 180Mi eat-backend-memcached-0 31m 54Mi
[/simterm]
И рестарты подов:
[simterm]
$ kk -n eks-dev-1-eat-backend-ns get pod NAME READY STATUS RESTARTS AGE eat-backend-b8b79574-5d6zl 0/1 CrashLoopBackOff 6 17m eat-backend-b8b79574-7n7pq 1/1 Running 5 9m13s eat-backend-b8b79574-8kjl4 0/1 CrashLoopBackOff 7 64m eat-backend-b8b79574-8t2pw 0/1 CrashLoopBackOff 6 64m eat-backend-b8b79574-bq8nw 1/1 Running 6 64m eat-backend-b8b79574-ds68n 0/1 CrashLoopBackOff 7 17m eat-backend-b8b79574-f4qcm 1/1 Running 6 9m13s eat-backend-b8b79574-f6wfj 0/1 Running 6 9m13s eat-backend-b8b79574-g7jm7 0/1 CrashLoopBackOff 5 25m eat-backend-b8b79574-ggrdg 1/1 Running 6 9m13s eat-backend-b8b79574-hjcnh 0/1 CrashLoopBackOff 6 25m eat-backend-b8b79574-l68vq 1/1 Running 7 29m eat-backend-b8b79574-mlpqs 0/1 CrashLoopBackOff 6 21m eat-backend-b8b79574-nkwjc 0/1 CrashLoopBackOff 5 9m13s eat-backend-b8b79574-swbvq 0/1 CrashLoopBackOff 6 64m eat-backend-memcached-0 1/1 Running 0 64m
[/simterm]
На 12-13 тысячах стало совсем печально – в живых оставался один под:
[simterm]
$ kk -n eks-dev-1-eat-backend-ns top pod NAME CPU(cores) MEMORY(bytes) eat-backend-b8b79574-7n7pq 0m 0Mi eat-backend-b8b79574-8kjl4 0m 0Mi eat-backend-b8b79574-8t2pw 0m 0Mi eat--backend-b8b79574-bq8nw 0m 0Mi eat-backend-b8b79574-ds68n 0m 0Mi eat-backend-b8b79574-f4qcm 0m 0Mi eat-backend-b8b79574-f6wfj 0m 0Mi eat-backend-b8b79574-g7jm7 0m 0Mi eat-backend-b8b79574-ggrdg 0m 0Mi eat-backend-b8b79574-hjcnh 0m 0Mi eat-backend-b8b79574-l68vq 0m 0Mi eat-backend-b8b79574-mlpqs 0m 0Mi eat-backend-b8b79574-nkwjc 3269m 129Mi eat-backend-b8b79574-swbvq 0m 0Mi eat-backend-memcached-0 23m 61Mi [/simterm] [simterm] $ kk -n eks-dev-1-eat-backend-ns get pod NAME READY STATUS RESTARTS AGE eat-backend-b8b79574-5d6zl 1/1 Running 7 20m eat-backend-b8b79574-7n7pq 0/1 CrashLoopBackOff 6 12m eat-backend-b8b79574-8kjl4 0/1 CrashLoopBackOff 7 67m eat-backend-b8b79574-8t2pw 0/1 CrashLoopBackOff 7 67m eat-backend-b8b79574-bq8nw 0/1 CrashLoopBackOff 6 67m eat-backend-b8b79574-ds68n 0/1 CrashLoopBackOff 8 20m eat-backend-b8b79574-f4qcm 0/1 CrashLoopBackOff 6 12m eat-backend-b8b79574-f6wfj 0/1 CrashLoopBackOff 6 12m eat-backend-b8b79574-g7jm7 0/1 CrashLoopBackOff 6 28m eat-backend-b8b79574-ggrdg 0/1 Running 7 12m eat-backend-b8b79574-hjcnh 0/1 CrashLoopBackOff 7 28m eat-backend-b8b79574-l68vq 0/1 CrashLoopBackOff 7 32m eat-backend-b8b79574-mlpqs 0/1 CrashLoopBackOff 7 24m eat-backend-b8b79574-nkwjc 1/1 Running 7 12m eat-backend-b8b79574-swbvq 0/1 CrashLoopBackOff 7 67m eat-backend-memcached-0 1/1 Running 0 67m
[/simterm]
И вот только тут я вспомнил про логи Laravel в контейнерах, и полез их глянуть – оказалось, что сервер баз данных начал отклонять новые подключения:
[simterm]
bash-4.4# cat ./new-eat-backend/storage/logs/laravel-2020-08-20.log [2020-08-20 16:53:25] production.ERROR: SQLSTATE[HY000] [2002] Connection refused {"exception":"[object] (Doctrine\\DBAL\\Driver\\PDOException(code: 2002): SQLSTATE[HY000] [2002] Connection refused at /var/www/new-eat-backend/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOConnection.php:31, PDOException(code: 2002): SQLSTATE[HY000] [2002] Connection refused at /var/www/new-eat-backend/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOConnection.php:27)
[/simterm]
AWS RDS – “Connection refused”
В качестве сервера баз данных используется RDS Aurora MySQL, у которой есть свой скейлинг для слейвов.
Проблема в том, что во-первых – тестирование проводится на Dev-окружении, у которого сервера db.t2.medium с 4 гигами памяти, а во-вторых – снова-таки, из-за спешки и неподготовленности – приложение все запросы слало на Мастер-хост, Слейвы не использовались вообще. На мастер уходило порядка 155 запросов/секунду.
Воообще, главная идея использования Aurora RDS как раз разделение запросов: все запросы, которые модифицируют данные (UPDATE
, CREATE
, etc) должны идти на master-инстанс, а все SELECT
– на его slave.
При этом слейвы могут скейлится по собственным политикам:
Кстати – мы тут неправильно делаем: надо вместо нагрузки на ЦПУ скейлить по кол-ву коннектов к слейву, тогда для слейвов можно вообще какой-то t3.medium использовать. Потом изменим.
AWS RDS max connections
Вообще, судя по документации – лимит должен быть в 90 подключений, а нас начало отбрасывать на 50-60 подключениях:
Говорил потом с ребятами-архитекторами из Амазона, спрашивал – почему так, где наш лимит в 90?
Сказали – хз, посмотрим в документации 🙂 “Может 90 – это up to”.
Ну и в целом картина такая получилась:
52% запросов отвалились с ошибками, что, разумеется, очень плохо:
Но лично мне был важен другой момент – сам кластер, его Control Plane и сеть работали без проблем.
А вопрос с базой будем решать на день третий – увеличим тип инстанса, и включим в приложении работу со слейвами.
День третий
Самый получился интересный день.
Первое – девелоперы включили работу со слейвами, теперь заодно потестируем автоскейлинг Авроры слейвов (забегая наперёд – не потестировали, потому что нагрузка на слейвы была минимальная).
Кстати, вчера вечером общался с архитекторами Амазона – подсказали про RDS Proxy – надо будет потом тоже добавить.
И ещё надо проверить стаус OpCache – он сильно снижает нагрузку на ЦПУ, см. PHP: кеширование PHP-скриптов — настройка и тюнинг OpCache.
Пока девелоперы вносят свои изменения – давайте глянем на наши пробки 😉
Kubernetes Liveness и Readiness probes
Нашёл неплохих пару статей – Kubernetes Liveness and Readiness Probes: How to Avoid Shooting Yourself in the Foot и Liveness and Readiness Probes with Laravel.
Разработчики уже добавили два новых ендпоинта:
... $router->get('healthz', 'HealthController@phpCheck'); $router->get('readiness', 'HealthController@dbReadCheck'); ...
А сам HealthController
выглядит так:
<?php namespace App\Http\Controllers; class HealthController extends Controller { public function phpCheck() { return response('ok'); } public function dbReadCheck() { try { $rows = \DB::select('SELECT 1 AS ok'); if ($rows && $rows[0]->ok == 1) { return response('ok'); } } catch (\Throwable $err) { // ignore } return response('err', 500); } }
По /healthz
– проверяем, что сам под запустился, РНР в нём работает.
По /readiness
– проверяем, что приложение в поде запустилось, и готово принимать трафик от клиентов:
livenessProbe
: если фейлится – Kubernetes перезапустит подinitialDelaySeconds
: should be longer than maximum initialization time for the container – сколько там надо контейнеру с Laravel? ну – пусть будет 5 секундfailureThreshold
: поставим три попытки, после трёх неудач – под пойдёт в ребутperiodSeconds
: дефолтные, кажется, 15 секунд – пусть так и остаётся
readinessProbe
: определяет, когда приложение готово принимать трафик. Если проверка фейлится – Kubernetes отключит этот под от балансировщика/СервисаinitialDelaySeconds
: пусть будет 5 секунд – тут запуск РНР, плюс подключение к БД, может занять времяperiodSeconds
: т.к. мы как раз ожидаем проблем с подключением к БД – то поставим пока 5failureThreshold
: тоже три, как и вlivenessProbe
successThreshold
: после скольких попыток считать, что под готов принимать трафик – ставим однуtimeoutSeconds
: дефолт 1, пусть так и остаётся
См. Configure Probes.
Обновим наши пробки в Deployment-е:
... livenessProbe: httpGet: path: {{ .Values.appConfig.healthcheckPath }} port: {{ .Values.appConfig.port }} initialDelaySeconds: 5 failureThreshold: 3 periodSeconds: 15 readinessProbe: httpGet: path: {{ .Values.appConfig.readycheckPath }} port: {{ .Values.appConfig.port }} initialDelaySeconds: 5 periodSeconds: 5 failureThreshold: 3 successThreshold: 1 timeoutSeconds: 1 ...
Потом вынесем в Helm values.yaml
.
И добавим переменную для Слейва БД:
... - name: DB_WRITE_HOST value: {{ .Values.appConfig.db.writeHost }} - name: DB_READ_HOST value: {{ .Values.appConfig.db.readHost }} ...
Kubernetes: PHP logs from Docker
Так – и логи жеж.
Девелоперы переключили логи в /dev/stderr
, откуда по идее их должен собирать Docker daemon, и отправялть в Kubernetes – но kubectl logs
показывает сообщения только от NGINX.
Смотрим пост Linux: PHP-FPM, Docker, STDOUT и STDERR — нет логов приложения, вспоминаем, как оно работает – проверяем дескрипторы.
В поде находим мастер-процесс РНР:
[simterm]
bash-4.4# ps aux |grep php-fpm | grep master root 9 0.0 0.2 171220 20784 ? S 12:00 0:00 php-fpm: master process (/etc/php/7.1/php-fpm.conf)
[/simterm]
Проверяем его дескрипторы:
[simterm]
bash-4.4# ls -l /proc/9/fd/2 l-wx------ 1 root root 64 Aug 21 12:04 /proc/9/fd/2 -> /var/log/php/7.1/php-fpm.log bash-4.4# ls -l /proc/9/fd/1 lrwx------ 1 root root 64 Aug 21 12:04 /proc/9/fd/1 -> /dev/null
[/simterm]
fd/2, stderr
процесса, направлен на файл /var/log/php/7.1/php-fpm.log
вместо /dev/stderr
– поэтому в kubectl
мы ничего не видим.
Грепаем /var/log/php/7.1/php-fpm.log
в каталоге /etc/php/7.1
– находим php-fpm.conf
, в котором по дефолту error_log = /var/log/php/7.1/php-fpm.log
, меняем на /dev/stderr
– готово.
Вроде всё…
Погнали тесты.
Разгоняемся с 1 до 15к за час.
Первое тестирование
3300 пользователей – всё OK:
Поды:
[simterm]
$ kk -n eks-dev-1-eat-backend-ns top pod NAME CPU(cores) MEMORY(bytes) eat-backend-867b59c4dc-742vf 856m 325Mi eat-backend-867b59c4dc-bj74b 623m 316Mi eat-backend-867b59c4dc-cq5gd 891m 319Mi eat-backend-867b59c4dc-mm2ll 600m 310Mi eat-ackend-867b59c4dc-x8b8d 679m 313Mi eat-backend-memcached-0 19m 68Mi
[/simterm]
HPA:
[simterm]
$ kk -n eks-dev-1-eat-backend-ns get hpa NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE eat-backend-hpa Deployment/eat-backend 30%/30% 4 40 5 20h
[/simterm]
На 7.000 пользователях пошли ошибки “php_network_getaddresses: getaddrinfo failed” – мои старые знакомые, уже сталкивался, жаль – не сделал пост на эту тему:
[simterm]
[2020-08-21 14:14:59] local.ERROR: SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Try again (SQL: insert into `order_logs` (`order_id`, `action`, `data`, `updated_at`, `created_at`) values (175951, nav, "Result page: ok", 2020-08-21 14:14:54, 2020-08-21 14:14:54)) {"exception":"[object] (Illuminate\\Database\\QueryException(code: 2002): SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Try again (SQL: insert into `order_logs` (`order_id`, `action`, `data`, `updated_at`, `created_at`) values (175951, nav, \"Result page: ok\", 2020-08-21 14:14:54, 2020-08-21 14:14:54))
[/simterm]
В двух слова тут: ошибка “php_network_getaddresses: getaddrinfo failed” в AWS может возникать по трём основным причинам:
- превышение количества пакетов на сетевом интерфейсе AWS EC2, cм. EC2 Packets per Second: Guaranteed Throughput vs Best Effort
- превышение пропускной способности канала, см. EC2 Network Performance Cheat Sheet
- превышение количества DNS-запросов к AWS VPC DNS – у него лимит 1024/сек, см. DNS quotas
Чуть ниже рассмотрим причину ошибок в данном случае.
На 9.000+ начали рестартиться поды:
[simterm]
$ kk -n eks-dev-1-eat-backend-ns get pod NAME READY STATUS RESTARTS AGE eat-backend-867b59c4dc-2m7fd 0/1 Running 2 4m17s eat-backend-867b59c4dc-742vf 0/1 CrashLoopBackOff 5 68m eat-backend-867b59c4dc-bj74b 1/1 Running 5 68m ... eat-backend-867b59c4dc-w24pz 0/1 CrashLoopBackOff 5 19m eat-backend-867b59c4dc-x8b8d 0/1 CrashLoopBackOff 5 68m eat-backend-memcached-0 1/1 Running 0 21h
[/simterm]
Потому как приложение переставало отвечать на Liveness и Readiness проверки:
[simterm]
0s Warning Unhealthy Pod Readiness probe failed: Get http://10.3.62.195:80/readiness: net/http: request canceled (Client.Timeout exceeded while awaiting headers) 0s Warning Unhealthy Pod Liveness probe failed: Get http://10.3.56.206:80/healthz: net/http: request canceled (Client.Timeout exceeded while awaiting headers)
[/simterm]
На 10.000 начал отбрасывать подключения уже сервер баз данных:
[simterm]
[2020-08-21 13:05:11] production.ERROR: SQLSTATE[HY000] [2002] Connection refused {"exception":"[object] (Doctrine\\DBAL\\Driver\\PDOException(code: 2002): SQLSTATE[HY000] [2002] Connection refused at /var/www/new-eat-backend/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOConnection.php:31, PDOException(code: 2002): SQLSTATE[HY000] [2002] Connection refused
[/simterm]
php_network_getaddresses: getaddrinfo failed и DNS
Итак, с чем мы столкнулись:
- ERROR: SQLSTATE[HY000] [2002] Connection refused
- php_network_getaddresses: getaddrinfo failed
С “ERROR: SQLSTATE[HY000] [2002] Connection refused” понятно – сейчас увеличим тип RDS инстанса с t3.medium на r5.large, а с DNS что? Так как из трёх возможных причин – кол-во пакетов на интерфейсе, загруженность канала или AWS VPC DNS – наиболее реальной выглядит именно вариант с DNS: каждый раз, когда приложение хочет подключиться к серверу баз данных – оно выполняет DNS-запрос на получение IP сервера, плюс все остальные запросы – мы легко можем забить лимит 1024/сек.
Кстати, см. Grafana: Loki — Prometheus-like счётчики и функции агрегации в LogQL и графики DNS запросов к dnsmasq.
Проверим, что у нас сейчас настроено для подов:
[simterm]
bash-4.4# cat /etc/resolv.conf nameserver 172.20.0.10 search eks-dev-1-eat-backend-ns.svc.cluster.local svc.cluster.local cluster.local us-east-2.compute.internal options ndots:5
[/simterm]
nameserver 172.20.0.10 – это наверняка наш kube-dns
:
[simterm]
bash-4.4# nslookup 172.20.0.10 10.0.20.172.in-addr.arpa name = kube-dns.kube-system.svc.cluster.local.
[/simterm]
Ага, он.
Который, между прочим, в логах рассказывал, что не может подключиться к АПИ-серверу:
E0805 21:32:40.283128 1 reflector.go:283] pkg/mod/k8s.io/[email protected]/tools/cache/reflector.go:98: Failed to watch *v1.Namespace: Get https://172.20.0.1:443/api/v1/namespaces?resourceVersion=23502628&timeout=9m40s&timeoutSeconds=580&watch=true: dial tcp 172.20.0.1:443: connect: connection refused
И что можно сделать, что бы перестать забивать DNS Амазона нашими запросами?
- поднимать
dnsmasq
? В Kubernetes как-то глупо выглядит – во-первых, у него есть свой DNS-сервис, во-вторых – 146%, что мы не первые столкнулись с такой проблемой, и сильно сомневаюсь, что люди решали это черезdnsmasq
(тем не менее – см. dnsmasq: ошибки в AWS — «Temporary failure in name resolution», логи, дебаг и размер кеша) - другой вариант – использовать DNS от Cloudflare (1.1.1.1) или Google (8.8.8.8) – тогда перестанем забивать VPC DNS, но время ответа от DNS будет дольше
Kubernetes dnsPolicy
Хорошо, а как подам вообще настраивается DNS?
Note: You can manage your pod’s DNS configuration with the dnsPolicy field in the pod specification. If this field isn’t populated, then the ClusterFirst DNS policy is used by default.
Значит, по-умолчанию для подов используется ClusterFirst
, который:
Any DNS query that does not match the configured cluster domain suffix, such as “
www.kubernetes.io
“, is forwarded to the upstream nameserver inherited from the node.
Ну и не сложно догадаться, что для AWS EC2 по дефолту будет ходить именно к AWS VPC DNS.
См. по теме – How do I troubleshoot DNS failures with Amazon EKS?
Что может быть переопределено в настройках ClusterAutoScaler:
[simterm]
$ kk -n kube-system get pod cluster-autoscaler-5dddc9c9b-fstft -o yaml ... spec: containers: - command: - ./cluster-autoscaler - --v=4 - --stderrthreshold=info - --cloud-provider=aws - --skip-nodes-with-local-storage=false - --expander=least-waste - --node-group-auto-discovery=asg:tag=k8s.io/cluster-autoscaler/enabled,k8s.io/cluster-autoscaler/bttrm-eks-dev-1 - --balance-similar-node-groups - --skip-nodes-with-system-pods=false ...
[/simterm]
Но в нашем случае тут ничего не менялось, всё по-умолчанию.
Запуск NodeLocal DNS в Kubernetes
А вот идея с dnsmasq
в целом была правильной, только для Kubernetes имеется NodeLocal DNS, который по сути является таким же кеширующим сервисом, как и dnsmasq
, только за DNS-записями он обращается к kube-dns
, а тот в свою очередь – к VPC DNS.
Что надо для запуска:
- kubedns: получим с помощью
kubectl get svc kube-dns -n kube-system -o jsonpath={.spec.clusterIP}
- domain: наш
<cluster-domain>
, cluster.local - localdns:
<node-local-address>
, адрес, на котором будет доступен лоrальный DNS-кеш, используем 169.254.20.10
Получаем IP Сервиса для kube-dns
:
[simterm]
$ kubectl get svc kube-dns -n kube-system -o jsonpath={.spec.clusterIP} 172.20.0.10
[/simterm]
См. Fixing EKS DNS.
Загружаем файл nodelocaldns.yaml
:
[simterm]
$ wget https://raw.githubusercontent.com/kubernetes/kubernetes/master/cluster/addons/dns/nodelocaldns/nodelocaldns.yaml
[/simterm]
Обновляем его:
[simterm]
$ sed -i "s/__PILLAR__LOCAL__DNS__/169.254.20.10/g; s/__PILLAR__DNS__DOMAIN__/cluster.local/g; s/__PILLAR__DNS__SERVER__/172.20.0.10/g" nodelocaldns.yaml
[/simterm]
Проверяем содержимое – что вообще будет выполняться.
Тут Kubernetes DaemonSet
, который задеплоит под с NodeLocal DNS на все ноды:
... --- apiVersion: apps/v1 kind: DaemonSet metadata: name: node-local-dns ...
И ConfigMap
для него:
... --- apiVersion: v1 kind: ConfigMap metadata: name: node-local-dns namespace: kube-system labels: addonmanager.kubernetes.io/mode: Reconcile data: Corefile: | cluster.local:53 { errors cache { success 9984 30 denial 9984 5 } reload loop bind 169.254.20.10 172.20.0.10 forward . __PILLAR__CLUSTER__DNS__ { force_tcp } prometheus :9253 health 169.254.20.10:8080 } ...
Применяем:
[simterm]
$ kubectl apply -f nodelocaldns.yaml serviceaccount/node-local-dns created service/kube-dns-upstream created configmap/node-local-dns created daemonset.apps/node-local-dns created
[/simterm]
Проверяем поды:
[simterm]
$ kk -n kube-system get pod | grep local-dns node-local-dns-7cndv 1/1 Running 0 33s node-local-dns-7hrlc 1/1 Running 0 33s node-local-dns-c5bhm 1/1 Running 0 33s
[/simterm]
Сервис:
[simterm]
$ kk -n kube-system get svc | grep dns kube-dns ClusterIP 172.20.0.10 <none> 53/UDP,53/TCP 88d kube-dns-upstream ClusterIP 172.20.245.211 <none> 53/UDP,53/TCP 107s
[/simterm]
kube-dns-upstream ClusterIP 172.20.245.21, но с подов должен быть доступен по 169.254.20.10, как мы указали в localdns
.
Доступен ли он? Проверяем из пода:
[simterm]
bash-4.4# dig @169.254.20.10 ya.ru +short 87.250.250.242
[/simterm]
Да, доступен.
Теперь надо отконфигурять поды, что бы они использовали 169.254.20.10 вместо kube-dns
.
В конфиге eksctl
можно добавить так:
... nodeGroups: - name: mygroup clusterDNS: 169.254.20.10 ...
Но для этого надо будет обновлять и наши WorkerNode Groups.
Kubernetes Pod dnsConfig
&& nameservers
Правим манифест с Deployment
– попробуем через dnsConfig
и nameservers
:
... resources: requests: cpu: 2500m memory: 500m terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsConfig: nameservers: - 169.254.20.10 dnsPolicy: None imagePullSecrets: - name: gitlab-secret nodeSelector: beta.kubernetes.io/instance-type: c5.xlarge role: eat-workers ...
Деплоим, проверяем:
[simterm]
$ kk -n eks-dev-1-eat-backend-ns exec -ti eat-backend-f7b49b4b7-4jtk5 cat /etc/resolv.conf nameserver 169.254.20.10
[/simterm]
Окей…
А работает?)
Пробуем dig
из пода:
[simterm]
$ kk -n eks-dev-1-eat-backend-ns exec -ti eat-backend-f7b49b4b7-4jtk5 dig ya.ru +short 87.250.250.242
[/simterm]
Да, всё нормально.
Хорошо – погнали следуюдий тест.
Первый тест выглядел так:
На 8+ начались ошибки.
Второе тестирование
8500 – нормально:
В прошлый раз на 7.000 уже было 150-200 ошибок, а тут пока только 5 штук проскочили.
Поды:
[simterm]
$ kk -n eks-dev-1-eat-backend-ns get pod NAME READY STATUS RESTARTS AGE eat-backend-5d8984656-2ftd6 1/1 Running 0 17m eat-backend-5d8984656-45xvk 1/1 Running 0 9m11s eat-backend-5d8984656-6v6zr 1/1 Running 0 5m10s ... eat-backend-5d8984656-th2h6 1/1 Running 0 37m eat-backend-memcached-0 1/1 Running 0 24h
[/simterm]
НРА:
[simterm]
$ kk -n eks-dev-1-eat-backend-ns get hpa NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE eat-backend-hpa Deployment/eat-backend 32%/30% 4 40 13 24h
[/simterm]
10.000 – всё хорошо:
НРА:
[simterm]
$ kk -n eks-dev-1-eat-backend-ns get hpa NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE eat-backend-hpa Deployment/eat-backend 30%/30% 4 40 15 24h
[/simterm]
Поды:
[simterm]
$ kk -n eks-dev-1-eat-backend-ns get pod NAME READY STATUS RESTARTS AGE eat-backend-5d8984656-2ftd6 1/1 Running 0 28m eat--backend-5d8984656-45xvk 1/1 Running 0 20m eat-backend-5d8984656-6v6zr 1/1 Running 0 16m ... eat-backend-5d8984656-th2h6 1/1 Running 0 48m eat-backend-5d8984656-z2tpp 1/1 Running 0 3m51s eat-backend-memcached-0 1/1 Running 0 24h
[/simterm]
Коннекты к серверу баз данных:
Ноды:
[simterm]
$ kk top node -l role=eat-workers NAME CPU(cores) CPU% MEMORY(bytes) MEMORY% ip-10-3-39-145.us-east-2.compute.internal 743m 18% 1418Mi 24% ip-10-3-44-14.us-east-2.compute.internal 822m 20% 1327Mi 23% ... ip-10-3-62-143.us-east-2.compute.internal 652m 16% 1259Mi 21% ip-10-3-63-96.us-east-2.compute.internal 664m 16% 1266Mi 22% ip-10-3-63-186.us-east-2.compute.internal <unknown> <unknown> <unknown> <unknown> ip-10-3-58-180.us-east-2.compute.internal <unknown> <unknown> <unknown> <unknown> ... ip-10-3-51-254.us-east-2.compute.internal <unknown> <unknown> <unknown> <unknown>
[/simterm]
Продолжают скейлится, всё работает:
В 17:45 скачёк времени ответа был, и немного ошибок – но всё прошло, хотя успел начал нервничать.
Ни одного рестарта пода:
[simterm]
$ kk -n eks-dev-1-eat-backend-ns get pod NAME READY STATUS RESTARTS AGE eat-backend-5d8984656-2ftd6 1/1 Running 0 44m eat-backend-5d8984656-45xvk 1/1 Running 0 36m eat-backend-5d8984656-47vp9 1/1 Running 0 6m49s eat-backend-5d8984656-6v6zr 1/1 Running 0 32m eat-backend-5d8984656-78tq9 1/1 Running 0 2m45s ... eat-backend-5d8984656-th2h6 1/1 Running 0 64m eat-backend-5d8984656-vbzhr 1/1 Running 0 6m49s eat-backend-5d8984656-xzv6n 1/1 Running 0 6m49s eat-backend-5d8984656-z2tpp 1/1 Running 0 20m eat-backend-5d8984656-zfrb7 1/1 Running 0 16m eat-backend-memcached-0 1/1 Running 0 24h
[/simterm]
Наскейлило 30 реплик:
[simterm]
$ kk -n eks-dev-1-eat-backend-ns get hpa NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE eat-backend-hpa Deployment/eat-backend 1%/30% 4 40 30 24h
[/simterm]
0% ошибок:
Apache JMeter и Grafana
В заключение – очень понравилось, как у ребят реализован вывод результатов тестирования: JMeter записывает результаты в InfluxDB, из которой Grafana потом рисует результаты тестирования: