Вступление
Всем привет! Я работаю Dev-Ops инженером в небольшой команде и мы уже 4-ый месяц используем Yandex Cloud для наших сервисов. Так сложилось, что с Kubernetes и облачными вычислениями я столкнулся впервые, поэтому многое приходится изучать на ходу, иногда на "горьком" опыте. На данный момент наши микросервисы развернуты в зональном кластере Kubernetes с одним рабочим узлом, по одной реплике на каждый Deployment. Это означает, что никакой отказоустойчивости и масштабируемости нет и при малейшей нагрузке приложения упадут.
Поэтому, пока нагрузка идет только от команды разработчиков, я решил заранее побеспокоиться об отказоустойчивости наших сервисов, развернутых в Yandex Managed Service for Kubernetes. Сегодня я разверну на своем облаке Kubernetes кластер и покажу как будет вести себя автомасштабируемое приложение под нагрузкой. Для управления инфраструктурой воспользуемся Terraform, для имитирования нагрузки сервисом Yandex Load Testing (в стадии Preview) от Yandex Cloud.
Перед началом
У вас должно быть настроено свое облако в Yandex Cloud и добавлен платежный аккаунт.
Также убедитесь, что у вас установлены:
yc
kubectl
terraform с настроенным ~/.terraformrc
удобный текстовый редактор для YAML и HCL, например Visual Studio Code
Создание сервисного аккаунта и аутентификация в yc
Чтобы использовать terraform для управления инфраструктурой сначала нужно создать сервисный аккаунт, от лица которого будут создаваться ресурсы.
Перейдем в каталог в облаке -> Сервисные аккаунты -> Создать сервисный аккаунт. Введем произвольное имя, описание по желанию, а также назначим роль admin, чтобы у terraform была возможность создавать любые ресурсы, а также назначать роли.
После создания сервисного аккаунта необходимо создать авторизированный ключ для аутентификации в yc (Yandex Cloud CLI). Выберите созданный сервисный аккаунт в списке -> Создать авторизированный ключ -> Алгоритм шифрования оставьте RSA_2048:
Нажмите создать и скачайте файл в формате JSON.
Зайдем в терминал и настроим доступ к облаку в yc. Для начала создадим профиль. Профили служат для хранения конфигурации для доступа к различным облакам если у вас их несколько:
$ yc config profiles create yc-k8s-scale
Profile 'yc-k8s-scale' created and activated
Назначим service-account-key (путь до authorized-key.json, который мы установили ранее), cloud-id и folder-id:
$ yc config set cloud-id <your_cloud_id>
$ yc config set folder-id <your_folder_id>
$ yc config set service-account-key <your_path_to_authorized_key>
Убедимся, что настроили доступ к облаку в yc корректно. Попробуем получить список всех сервисных аккаунтов:
$ yc iam service-account list
+----------------------+--------------+
| ID | NAME |
+----------------------+--------------+
| ajevk652eetmf0dn96eo | terraform-sa |
+----------------------+--------------+
Terraform
Как говорилось ранее, разворачивать инфраструктуру будем не через GUI консоль облака, а через Terraform (Infrastructure as Code инструмент для управления инфраструктурой). Исходный код для создания нужных ресурсов можно посмотреть в GitHub, тут же заденем только один файл: set_env.sh. В нем будет активироваться наш профиль, назначаться переменные окружения, нужные для работы Terraform с провайдером Yandex Cloud (YC_TOKEN, YC_CLOUD_ID, YC_FOLDER_ID):
echo "You are using yc-k8s-scale profile!"
yc config profiles activate yc-k8s-scale
export YC_TOKEN=$(yc iam create-token)
export YC_CLOUD_ID=$(yc config get cloud-id)
export YC_FOLDER_ID=$(yc config get folder-id)
Для аутентификации будет создаваться IAM токен, время жизни которого 12 часов.
Выполним terraform plan, чтобы узнать какие ресурсы будут созданы:
$ terraform plan
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
...
Plan: 10 to add, 0 to change, 0 to destroy.
Примем изменения при помощи команды terraform apply и подтвердим действие, написав 'yes':
$ terraform apply
Plan: 10 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
Дождитесь пока все ресурсы не будут созданы, это может занять от 15 до 30 минут.
Обзор созданных ресурсов
После завершения команды будут созданы следующие ресурсы: одна сеть с 4 подсетями, две из которых для ресурсов облака в зонах доступности ru-central1-a и ru-central1-b, остальные две для подов и сервисов Kubernetes кластера.
Также будет создан кластер в Managed Service for PostgreSQL с базой данных db1 и пользователем user1 с паролем password12345
Будет создан один кластер в Managed Service for Kubernetes с двумя группами узлов:
yc-scale-node-group-1 (подсеть default-ru-central1-a, начальное кол-во узлов 1, максимальное кол-во узлов 3)
yc-scale-node-group-2 (подсеть default-ru-central1-b, начальное кол-во узлов 0, максимальное кол-во узлов 3)
Узлы используют самую дешевую конфигурацию, чтобы избежать больших расходов.
Автоматическое масштабирование
В Yandex Managed service For Kubernetes поддерживается три вида автомасштабирования:
Cluster Autoscale
HPA
VPA
В Kubernetes поддерживается два вида масштабирования: горизонтальное и вертикальное. При горизонтальном масштабировании создается ресурс HPA (HorizatalPodAutoscaler), который отслеживает одну из метрик (обычно CPU или кол-во запросов) и увеличивает/уменьшает количество подов, при этом можно задать минимальное и максимальное количество подов.
В случае с вертикальным масштабированием создается ресурс VPA (VerticalPodAutoscaler), автоматический назначающий (или дающий рекомендации) ресурсы для пода.
Облачные провайдеры, в том числе Yandex Cloud, предлагают автоматическое масштабирование рабочих узлов (Cluster Autoscale). В случае, если планировщик (kube-scheduler) не может назначить поду узел из-за нехватки ресурсов, Managed Service for Kubernetes создаcт дополнительный узел в одной из группе узлов и развернет там под. В случае, если поды можно разместить на меньшем количестве узлов, Cluster Autoscale будет выселять поды из узла и впоследствии удалит его. Зона доступности, начальное, минимальное и максимальное количество узлов указывается при создании группы узлов. В нашем случае, terraform создаст две группы узлов в двух зонах доступности. В группе узлов в зоне доступности ru-central1-a изначально будет создан один рабочий узел с возможностью масштабирования до 3. В группе узлов в зоне доступности ru-central1-b начальное количество узлов равно 0, максимальное количество узлов такое же, как и у первой группы узлов - 3.
Создание ресурсов через kubectl
Настроим доступ к нашему кластеру из kubectl. Для этого нужно получить конфигурационный файл при помощи yc:
$ yc managed-kubernetes cluster get-credentials --name yc-scale --external
Context 'yc-yc-scale' was added as default to kubeconfig '/home/azamat/.kube/config'.
Check connection to cluster using 'kubectl cluster-info --kubeconfig /home/azamat/.kube/config'.
Note, that authentication depends on 'yc' and its config profile 'yc-k8s-scale'.
To access clusters using the Kubernetes API, please use Kubernetes Service Account.
Проверим количество рабочих узлов:
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
cl1a9h5asplrhnjmkn5g-unaq Ready <none> 6m57s v1.26.2
Изначально используется один рабочий узел.
Узнаем host созданного кластера Managed Service for PostgreSQL:
$ yc postgres hosts list --cluster-name postcreator-psql
+-------------------------------------------+
| NAME |
+-------------------------------------------+
| rc1a-0cwboiqngq4xea8z.mdb.yandexcloud.net |
+-------------------------------------------+
По этому хосту приложение будет подключаться к базе данных. Полные данные для подключения:
host: rc1a-0cwboiqngq4xea8z.mdb.yandexcloud.net
database: db1
user: user1
password: password12345
Создадим Secret, где будут храниться данные для подключения к БД. В дальнейшем данные из этого секрета будут вставляться в контейнер в качестве переменных окружения:
$ kubectl create secret generic postcreator-db-creds \
--from-literal=username=user1 \
--from-literal=password=password12345 \
--from-literal=jdbc_url='jdbc:postgresql://rc1a-0cwboiqngq4xea8z.mdb.yandexcloud.net:6432/db1'
Перед созданием Deployment, необходимо создать внутрении балансировщик нагрузки, через который внутри облачной сети можно будет обращаться к нашим подам. Узнаем id подсети в зоне доступности ru-central1-a:
$ yc vpc subnet get --name default-ru-central1-a
id: e9bg1u4mljgn5rdvs2bt
...
Создадим внутренний балансировщик нагрузки с внутренним IP адресом 10.128.0.100. Вместо $SUBNET_ID укажите id подсети, который мы получили выше:
$ kubectl apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
name: postcreator-service
annotations:
yandex.cloud/load-balancer-type: internal
yandex.cloud/subnet-id: $SUBNET_ID
spec:
selector:
app: postcreator
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: LoadBalancer
loadBalancerIP: 10.128.0.100
EOF
service/postcreator-service created
LoadBalancer позволит обращаться к подам внутри облачной сети по внутреннему адресу. Убедимся, что балансировщик нагрузки был создан с указанным IP адресом:
$ kubectl get svc/postcreator-service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
postcreator-service LoadBalancer 10.96.212.0 10.128.0.100 80:30671/TCP 5m9s
Мы будем использовать этот адрес для отправки множества запросов.
В качестве приложения, которое будет под нагрузкой, мы будем использовать обычное CRUD серверное приложение, написанное на Java с использованием Spring 3. Исходный код и Docker образ опубликованы.
Развернем Deployment без указания количества реплик, а также не забудем назначить запросы и лимиты.
$ kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: postcreator-deployment
spec:
selector:
matchLabels:
app: postcreator
template:
metadata:
labels:
app: postcreator
spec:
containers:
- name: postcreator-app
image: azamatkomaev/postcreator:0.0.6
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: postcreator-db-creds
key: jdbc_url
- name: DATABASE_USERNAME
valueFrom:
secretKeyRef:
name: postcreator-db-creds
key: username
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: postcreator-db-creds
key: password
EOF
deployment.apps/postcreator-deployment created
Подождем некоторое время, чтобы Deployment развернулся и посмотрим список всех подов:
$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
postcreator-deployment-5975fcb7f9-445p8 1/1 Running 0 30s 10.112.128.7 cl16f30cle1m0sg9583f-awij <none> <none>
Передав параметр -o wide можно увидеть подробную информацию о поде. Как видим, единственная реплика приложения была назначена на наш единственный узел.
Создание агента Load Testing
Для нагрузочного тестирования будем использовать сервис Yandex Load Testing. Создадим агент для Load Testing со следующими параметрами:
Имя: agent112
Зона доступности: ru-central1-a (агент должен быть в той же подсети, что и балансировщик нагрузки)
Агент: Small
Подсеть: default/default-ru-central1-a
Сервисный аккаунт: terraform-sa
Логин: admin
-
SSH-ключ: сгенерируйте ключ при помощи команды ssh-keygen и вставьте в текстовое поле публичный ключ
После нажания "Создать" следует подождать пока облако выделит ресурсы для выбранного агента. Так как Load Testing находится в стадии Preview, он не тарифицируется. Плата за использование агентов взимается по тарифам Compute Cloud. Перейдем в Compute Cloud и если ВМ перешла в статус "Running", назначим ей публичный адрес. Это необходимо, чтобы агент имел доступ к сервису Load Testing.
Статус агента во вкладке Load Testing должен перейти из "Initializing connection" в "Ready for test" Создадим тестовые данные в формате HTTP_JSON и сохраним его в файл payload.json:
{"host": "10.128.0.100", "method": "POST", "uri": "/api/v1/posts", "tag": "url1", "headers": {"Connection": "close", "Content-Type": "application/json"}, "body":"{\"title\": \"Hello!\", \"description\": \"I am not waiting for heaven\", \"author_name\": \"Azamat Komaev\"}"}
Перейдем на страницу с агентом и нажмем "Создать тест".
Укажем следующие значения:
Агент: agent112
Тестовые данные: выберем ранее созданный файл payload.json
Продолжим конфигурацию теста:
Способ настройки: Форма
Генератор нагрузки: PANDORA
Load Testing поддерживает два генератора нагрузки: Pandora и Phantom, подробнее о них можно почитать тут. Я буду использовать генератор нагрузки Pandora со ступенчатой нагрузкой.
Адрес цели: 10.128.0.100
Порт цели: 80
Тестирующие потоки: 2500
-
Тип нагрузки: + Профиль нагрузки:
Профиль 1: step
От: 500
До: 2500
Шаг: 500
Длительность: 1m
Тип запросов: HTTP_JSON
Генератор будет наращивать нагрузку от 500 до 2500 запросов в секунду в течение 1 минуты с шагом в 500 запросов. Общая длительность нагрузочного тестирования составит 5 минут.
Тип запросов: HTTP_JSON
Тестовые данные: Прикрепленный файл
Конфигурационный данные, в частности, я взял из практического руководства Yandex Cloud, подробнее по ссылке.
Начинаем нагружать
В нашем случае, для мониторинга потребления CPU и Memory контейнерами использовать Grafana + Prometheus было бы излишним, поэтому воспользуемся командой kubectl top:
$ kubectl top pod postcreator-deployment-5975fcb7f9-7mvb5
NAME CPU(cores) MEMORY(bytes)
postcreator-deployment-5975fcb7f9-7mvb5 14m 171Mi
И так! Запустим тест и посмотрим как будет себя вести под и кластер под нагрузкой.
Подождем минуту и посмотрим кол-во потребляемых ресурсов:
$ kubectl top pod postcreator-deployment-5975fcb7f9-7mvb5
NAME CPU(cores) MEMORY(bytes)
postcreator-deployment-5975fcb7f9-7mvb5 499m 318Mi
Остановим тест. Утилизация CPU не поднимется выше 500m, так как ранее мы установили для него лимит на такой отметке.
Посмотрим кол-во узлов и подов:
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
cl1a9h5asplrhnjmkn5g-unaq Ready <none> 18m v1.26.2
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
postcreator-deployment-5975fcb7f9-7mvb5 1/1 Running 0 13m
- "Ну и где твое автомасштабирование?!" - захотите вы спросить меня. Его нет! Потому что помимо настройки Cluster Autoscale в Yandex Managed service for Kubernetes нужно настроить HPA.
$ kubectl autoscale deployment/postcreator-deployment --cpu-percent=70 --min=1 --max=20
horizontalpodautoscaler.autoscaling/postcreator-deployment autoscaled
Параметр --cpu-percent указывает при каком процентом соотношении нагрузки к запросам повышать кол-во подов. Например, у нас стоит запрос по CPU в 256m, в случае, если нагрузка на CPU превысит 180m, то HPA развернет дополнительный под.
Проверим, что HPA развернулся:
$ kubectl get hpa
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
postcreator-deployment Deployment/postcreator-deployment 2%/70% 1 10 1 27s
Перезапустим тест и будем отслеживать состояние HPA в настоящем времени
$ kubectl get hpa --watch
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
postcreator-deployment Deployment/postcreator-deployment 90%/70% 1 20 1 2m15s
postcreator-deployment Deployment/postcreator-deployment 200%/70% 1 20 2 2m46s
postcreator-deployment Deployment/postcreator-deployment 198%/70% 1 20 3 3m16s
postcreator-deployment Deployment/postcreator-deployment 198%/70% 1 20 6 3m32s
postcreator-deployment Deployment/postcreator-deployment 199%/70% 1 20 9 4m2s
postcreator-deployment Deployment/postcreator-deployment 197%/70% 1 20 15 4m48s
postcreator-deployment Deployment/postcreator-deployment 167%/70% 1 20 15 6m48s
postcreator-deployment Deployment/postcreator-deployment 167%/70% 1 20 17 7m3s
postcreator-deployment Deployment/postcreator-deployment 142%/70% 1 20 17 7m18s
postcreator-deployment Deployment/postcreator-deployment 142%/70% 1 20 20 7m33s
postcreator-deployment Deployment/postcreator-deployment 56%/70% 1 20 20 7m48s
postcreator-deployment Deployment/postcreator-deployment 36%/70% 1 20 20 8m34s
postcreator-deployment Deployment/postcreator-deployment 50%/70% 1 20 20 9m20s
postcreator-deployment Deployment/postcreator-deployment 38%/70% 1 20 20 9m50s
Остановим тест. Как видно по выводу в терминал, HPA постепенно поднимал количество подов до 20 Под конец нагрузка CPU на один под упала, что говорит о том, что автомасштабирование и балансировка нагрузки настроены верно.
Посмотрим список всех узлов через консоль Яндекс Облака:
Так как новосозданным подам не хватало место на существующих узлах, кластер Managed Service for Kubernetes развернул дополнительные узлы. Обратите внимание, что он сам распределил узлы по зонам доступности (группам узлов). После падения нагрузки, HPA аналогично созданию будет постепенно удалять поды. В свою очередь, кластер Kubernetes будет сокращать количество узлов.
После тестирования
Не забудьте удалить созданные ресурсы, если они вам больше не нужны:
$ terraform destroy
Plan: 0 to add, 0 to change, 13 to destroy.
Do you really want to destroy all resources?
Terraform will destroy all your managed infrastructure, as shown above.
There is no undo. Only 'yes' will be accepted to confirm.
Enter a value: yes
Также следует отдельно удалить агент в Load Testing, так как он не находится под управлением terraform.
Результаты работы Yandex Load Testing
Изначально я не планировал использовать Load Testing для нагрузочного тестирования. Основная цель статьи была показать как будет масштабироваться приложение, развёрнутое в Yandex Managed Service for Kubernetes под нагрузкой. Поэтому анализ нагрузочных тестов я анализировать не буду.
Вывод
Yandex Managed Service for Kubernetes предлагает отличное решение для разворачивания автомасштабируемых приложении в кластере. Но использовать только Cluster Autoscale без HPA, встроенного в Kubernetes, смысла нет, они хорошо работают в паре!