Деплоим проект на Kubernetes в Mail.ru Cloud Solutions. Часть 2: настройка и запуск приложения для транскрибации видео

от автора

Это продолжение практикума по развертыванию Kubernetes-кластера на базе облака Mail.ru Cloud Solutions и созданию MVP для реального приложения, выполняющего транскрибацию видеофайлов из YouTube.

Я Василий Озеров, основатель агентства Fevlake и действующий DevOps-инженер (опыт в DevOps — 8 лет), покажу все этапы разработки Cloud-Native приложений на K8s: от запуска кластера до построения CI/CD и разработки собственного Helm-чарта.

Напомню, что в первой части статьи мы выбрали архитектуру приложения, написали API-сервер, запустили Kubernetes c балансировщиком и облачными базами, развернули кластер RabbitMQ через Helm в Kubernetes. Сейчас во второй части мы настроим и запустим приложение для преобразования аудио в текст, сохраним результат и настроим автомасштабирование нод в кластере.

Также запись практикума можно посмотреть: часть 1, часть 2, часть 3.

Кодирование обработчиков Worker на Python

Теперь нам необходимо написать код для конвертеров (Worker), которые будут получать сообщения из очереди RabbitMQ для последующей обработки. В их задачи будет входить загрузка видео, извлечение из него аудио, преобразование аудио в текст и сохранение полученной расшифровки в бакет S3. В качестве языка программирования будем использовать Python. Репозиторий с кодом доступен по ссылке.

Рассмотрим файл worker.py. Сначала импортируем стандартные системные модули os, sys, time, logging, а также модули для работы с JSON (json), HTTP (requests), RabbitMQ (pika) и Environment-переменными (Env). Чтобы не писать собственный парсер для загрузки видео с Youtube, будем использовать библиотеку youtube_dl. Для отправки файлов в S3 подключим модуль boto3:

# System modules import json import os import sys import time import logging import subprocess  # Third party modules import pika import requests from environs import Env import youtube_dl import boto3 from urllib import parse

Далее читаем переменные из конфигурационного файла .env, который будет размещаться в директории с нашим приложением. При этом подкладывать его туда в дальнейшем будет Kubernetes при помощи configMap — жестко прописывать файл в Docker-образе мы не будем:

## read config env = Env() env.read_env()  # read .env file, if it exists  rabbitmq_host = env("RABBIT_HOST", 'localhost') rabbitmq_port = env("RABBIT_PORT", 5672) rabbitmq_user = env("RABBIT_USER") rabbitmq_pass = env("RABBIT_PASS") rabbitmq_queue = env("RABBIT_QUEUE", "AutoUrlTemplateQueue")  api_url = env("API_URL") api_key = env("API_KEY")  s3_endpoint = env("S3_ENDPOINT") s3_web = env("S3_WEB") s3_access_key = env("S3_ACCESS_KEY") s3_secret_key = env("S3_SECRET_KEY") s3_bucket = env("S3_BUCKET")

Пройдемся по переменным:

  • RABBIT_HOST, RABBIT_PORT, RABBIT_USER, RABBIT_PASS и RABBIT_QUEUE нужны для взаимодействия с RabbitMQ.

  • API_URL и API_KEY — для отправки статуса обработки видео на API-сервер.

  • S3_ENDPOINT — Endpoint для подключения к S3: переменную необходимо заполнить, если используется отличное от AWS хранилище.

  • S3_WEB описывает URL для загрузки итоговых текстовых файлов с расшифровкой видео.

  • S3_ACCESS_KEY, S3_SECRET_KEY и S3_BUCKET — это ключи доступа и ссылка на бакет S3, в котором будут храниться файлы.

Далее подключаемся к RabbitMQ и создаем клиента S3:

# Connecting to rabbitmq credentials = pika.PlainCredentials(rabbitmq_user, rabbitmq_pass) connection = pika.BlockingConnection(pika.ConnectionParameters(rabbitmq_host, rabbitmq_port, '/', credentials)) channel = connection.channel()  s3client = boto3.client('s3', endpoint_url=s3_endpoint, aws_access_key_id = s3_access_key, aws_secret_access_key = s3_secret_key)

Настраиваем логирование для вывода сообщений в заданном формате:

# Setting logger logging.basicConfig(   format='%(asctime)s | %(levelname)-7s | %(name)-12s | %(message)s',   datefmt='%d-%b-%y %H:%M:%S',   level=logging.INFO )  logger = logging.getLogger(__name__)

И после этого запускаем чтение сообщений из очереди, указывая process_event в качестве функции-обработчика и отключая Auto Acknowledgement (auto_ack=false). То есть мы не подтверждаем сообщение автоматически, а будем ждать логического завершения операции, чтобы в случае ошибок попробовать обработать сообщение повторно. При вызове channel.start_consuming приложение подключается к RabbitMQ и начинает ждать новых сообщений в очереди. В случае нажатия Ctrl+C выполнение прервется с кодом sys.exit(0):

# Starting consuming rabbitmq queue logger.info('Waiting for messages. To exit press CTRL+C')  channel.basic_consume(queue=rabbitmq_queue, on_message_callback=process_event, auto_ack=False)  try:     channel.start_consuming() except KeyboardInterrupt:     logger.info('Interrupted')     try:         sys.exit(0)     except SystemExit:         os._exit(0)

Теперь рассмотрим логику основной функции process_event. Вначале мы логируем поступление нового сообщения из RabbitMQ и запускаем таймер для отсчета времени обработки. Далее получаем JSON из тела сообщения и парсим его, извлекая две переменные: уникальное название запроса (name) и ссылку на Youtube-видео, которое нам необходимо загрузить (video_url). Логируем результат парсинга:

# Processing event from rabbit and sending to internal queue def process_event(ch, method, properties, body):     logger.info("Got new message from rabbitmq %r" % body)     t_start = time.time()      # Parsing data     event = json.loads(body)      name = event['name']     video_url = event['video_url']      logger.info("Got name: " + str(name) + ", video_url: " + str(video_url))

Перед загрузкой удаляем ранее созданные временные файлы с расширением .wav:

# Removing audio.wav before downloading os.chdir("/app") filelist = [ f for f in os.listdir("/app") if f.endswith(".wav") ] for f in filelist:   os.remove(os.path.join("/app", f))

В следующем блоке загружаем видео с YouTube, используя YouTube Downloader (youtube_dl).

При загрузке будет использоваться набор опций ydl_opts. Так как нас интересует только аудио, отключаем сохранение видео (keepvideo: False). В блоке postprocessors выбираем FFmpeg, кодек wav и quality 192 для конвертации файла. В блоке postprocessor_args указываем rate, равный 16 кГц, и количество каналов аудио, равное 1.

На YouTube практически все видео stereo, но нам обязательно нужно mono, так как софт, который будет переводить речь в текст, работает только с mono-аудиофайлами. В поле outtmpl вводим шаблон для имени сохраняемого файла: <ID видео, которое мы передали>.<расширение (wav)>.

Загрузка запускается с помощью функции ydl.download с указанием video_url, который мы в самом начале получили из сообщения RabbitMQ. При любом сбое во время загрузки файла в лог запишется сообщение «Can’t download audio file», а в API отправится информация «Error downloading video». И при получении статуса обработки видео клиент увидит это сообщение:

# Downloading video     ydl_opts = {         'format': 'bestaudio/best',         'postprocessors': [{             'key': 'FFmpegExtractAudio',             'preferredcodec': 'wav',             'preferredquality': '192'         }],         'postprocessor_args': [             '-ar', '16000', '-ac', '1'         ],         'prefer_ffmpeg': True,         'keepvideo': False,         'outtmpl': '%(id)s.%(ext)s'     }      try:       with youtube_dl.YoutubeDL(ydl_opts) as ydl:         ydl.download([video_url])     except:       logger.error("Can't download audio file, sending callback")       headers = {"X-API-KEY": api_key}       payload = {"processed": True, "text_url": "Error downloading video"}       r = requests.put(api_url + '/requests/' + name, data=json.dumps(payload), headers=headers)       logger.info("Callback sent, response code: " + str(r.status_code))       return

Следующий шаг – конвертация аудио в текст. Для этой цели будем использовать Leopard от компании Picovoice. Leopard читает wav-файл на английском языке и переводит его в текст. Он работает полностью локально без обращения к каким-то внешним API. Инструмент платный, но для домашнего использования есть 30-дневный триал. Для перевода текста в real-time режиме у этого же разработчика есть программа Cheetah.

Перед использованием Leopard необходимо скомпилировать. На странице в GitHub есть инструкция: предлагается использовать GNU C Compiler (GCC) и создать бинарник из исходника на C.

Мы запускаем Leopard, указывая в параметрах путь к библиотеке C, к акустическим моделям, к языковым моделям, а также лицензионный файл (его можно получить на официальном сайте Picovoice) и wav-файл. После этого в stdout получаем транскрипт аудио в текст. В случае сбоев в обработке, как и на предыдущем шаге, выполняем логирование и отправку соответствующего callback через API:

# Converting audio to text     logger.info("Converting audio to text")     try:       p = subprocess.Popen("/app/leopard/leopard_demo        leopard/lib/linux/x86_64/libpv_leopard.so        leopard/lib/common/acoustic_model.pv leopard/lib/common/language_model.pv license.lic *.wav", stdout=subprocess.PIPE, shell=True)       (output, err) = p.communicate()       p_status = p.wait()       logger.info("Command output : " + str(output))       logger.info("Command exit status/return code : " + str(p_status))     except:       logger.error("Can't convert audio to text, sending callback")       headers = {"X-API-KEY": api_key}       payload = {"processed": True, "text_url": "Error converting audio"}       r = requests.put(api_url + '/requests/' + name, data=json.dumps(payload), headers=headers)       logger.info("Callback sent, response code: " + str(r.status_code))       return

И далее загружаем наш транскрипт, сохраненный в output, на S3. Название бакета берем из environment-переменной S3_BUCKET. Путь к файлу будет иметь следующий вид: <URL бакета из переменной S3_WEB>/converted/<name исходного запроса>.txt. В ACL необходимо обязательно установить public-read, чтобы все могли прочесть файл:

   # Uploading file to s3     try:       s3client.put_object(Body=output, Bucket=s3_bucket, Key='converted/' + name + '.txt', ACL='public-read')     except:       logger.error("Can't upload text to s3, sending callback")       headers = {"X-API-KEY": api_key}       payload = {"processed": True, "text_url": "Error uploading to s3"}       r = requests.put(api_url + '/requests/' + name, data=json.dumps(payload), headers=headers)       logger.info("Callback sent, response code: " + str(r.status_code))       return

После этого мы отправляем информацию в API об успешной обработке файла, сообщая URL, по которому можно загрузить файл из S3:

   # Sending callback to API     headers = {"X-API-KEY": api_key}     payload = {"processed": True, "text_url": s3_web + "/converted/" + name}     r = requests.put(api_url + '/requests/' + name, data=json.dumps(payload), headers=headers)     logger.info("Callback sent, response code: " + str(r.status_code))

В конце выводим время обработки и отправляем подтверждение в очередь:

   t_elapsed = time.time() - t_start     logger.info("Finished with " + video_url  + " in " + str(t_elapsed) + " seconds")      ch.basic_ack(delivery_tag = method.delivery_tag)

В завершение рассмотрим Dockerfile. Мы берем Python 3.9, создаем директорию «/app» и переходим в нее. Устанавливаем ffmpeg, который будет использоваться для конвертации файлов, загруженных с Youtube. Клонируем репозиторий с Leopard, переходим в папку с ним, компилируем и возвращаемся обратно. После этого копируем файл requirements.txt, описывающий зависимости в Python: soundfile, youtube_dl, pika, boto, numpy. Устанавливаем их. И далее копируем все остальные файлы:

FROM python:3.9  WORKDIR "/app"  RUN apt update && apt-get install -y ffmpeg  RUN git clone https://github.com/Picovoice/leopard && cd leopard && gcc -I include/ -O3 demo/c/leopard_demo.c -ldl -o leopard_demo && cd ..  COPY requirements.txt requirements.txt RUN pip3 install -r requirements.txt  COPY . .

Настройка переменных окружения и создание бакета S3

Далее необходимо прописать environment-переменные в .env файле:

API_URL = "http://video-api-svc.stage.svc:8080" API_KEY = "804b95f13b714ee9912b19861faf3d25"  RABBIT_HOST = "rabbitmq.stage.svc" RABBIT_USER = "user" RABBIT_PASS = "6NvlZY77Fu" RABBIT_QUEUE = "VideoParserWorkerQueue"  S3_ENDPOINT = "http://hb.bizmrg.com/" S3_SECRET_KEY = "6qg2TaFo3tq9L93mNkd959Kw7YxPEp7iyybK4FsXw9T8" S3_ACCESS_KEY = "fAFSLdYjNKVEdEh2Vs27vc" S3_BUCKET = "converted" S3_WEB = "http://converted.hb.bizmrg.com"

Впоследствии все критичные переменные мы зашифруем и будем хранить в секретах. Пока остановимся на том, откуда для них брать значения.

API_KEY у нас был прописан в main.go, оставляем его. Для получения API_URL и RABBIT_HOST выведем сервисы в Kubernetes-кластере.

Команда kubectl –n stage get svc возвращает внутренние сервисы в Namespace stage:

Команда kubectl –n stage get ing позволяет посмотреть их внешние API:

Так как мы будем запускать конвертер внутри кластера, то нам достаточно внутренних адресов. Итоговое имя хоста формируется по маске <NAME из вывода первой команды>.<Namespace (в нашем случае stage)>.svc.

Далее в переменных RABBIT_USER, RABBIT_PASS, RABBIT_QUEUE указываются пользователь, пароль и название очереди в RabbitMQ.

Затем идут настройки для подключения к S3. На них остановимся подробнее. Вначале создадим бакет S3 в облаке MCS. Для этого выберем команду «Создать бакет» в пункте меню «Объектное хранилище». В качестве названия бакета укажем converted (скопировав его в переменную S3_BUCKET), а в качестве класса хранения — Hotbox. Для нашего приложения требуется хранение горячих данных. Использовать Icebox рекомендуется при редких обращениях к данным, например, несколько раз в месяц:

Стоит отметить, что MCS из коробки предлагает подключение CDN к вашим бакетам, а также привязку собственного домена. В нашем приложении мы это использовать не будем, но функции очень полезные.

Далее в пункте меню «Объектное хранилище» — «Аккаунты» создаем новый аккаунт worker для подключения к бакету:

Затем копируем его токены Access Key Id и Secret Key в переменные S3_ACCESS_KEY и S3_SECRET_KEY, соответственно:

В пункте меню «Объектное хранилище» можно посмотреть S3 Endpoint URL. Именно это значение мы прописываем в переменной S3_ENDPOINT. Для доступа к конкретному бакету в начале этого URL дописывается название бакета: http://converted.hb.bizmrg.com. Это значение мы прописываем в переменной S3_WEB:

Развертывание и проверка обработчиков Worker в кластере Kubernetes

Так как конвертеру понадобится дополнительное количество CPU, мы не будем запускать его ни на Master-ноде, ни на рабочей ноде, на которой размещаются наши API и RabbitMQ. Поэтому в облаке MCS добавим новую группу узлов в наш кластер.

Для этого возвращаемся в раздел «Кластеры», напротив нашего кластера нажимаем на три точки и выбираем «Добавить группу узлов»:

Назовем ее converters, пусть у нее будет 2 CPU и 4 ГБ памяти, SSD на 50 ГБ и один узел по умолчанию.

Важный момент: установим флажок «Включить автомасштабирование» и укажем минимальное количество узлов равным 1, а максимальное количество — равным 5. Это необходимо для работы автомасштабирования, которое мы позднее рассмотрим:

Проверяем, что у нас появилась нода с помощью команды kubectl get nodes:

При помощи команды get node можно вывести все параметры ноды в формате YAML:

kubectl get node kub-vc-dev-converters-0 -o yaml

Обратите внимание на секцию allocatable: здесь отображается, какие ресурсы и в каком объеме могут быть размещены на ноде. Например, в поле cpu видим, что на ноде можно разместить 1930 milicores, или миллиядер (в одном ядре 1000 миллиядер):

В секции labels отображаются все метки ноды. Нас интересует метка msc.mail.ru/mcs-nodepool: converters. Мы пропишем ее в deployment нашего конвертера для явного указания того, на каком node-пуле может запускаться приложение:

Для начала создадим deployment для нашего конвертера, назовем его converter-dp. В нем будет создаваться контейнер с названием worker с образом vozerov/converter:v24 (этот образ я также залил на hub.docker.com). Внутри будет запускаться python3 /app/worker.py. Далее в ресурсах указываем, что контейнер запрашивает 1,5 ядра CPU (1500 миллиядер) и 1 ГБ памяти, в лимитах укажем те же значения. И заполняем nodeSelector, сообщая Deployment, что поды можно запускать только на нодах, у которых есть label mcs.mail.ru/mcs-nodepool: converters:

apiVersion: apps/v1 kind: Deployment metadata:   name: converter-dp spec:   selector:     matchLabels:       app: converter   template:     metadata:       labels:         app: converter     spec:       containers:         - name: worker           image: vozerov/converter:v24           command: ["python3"]           args: ["/app/worker.py"]           volumeMounts:           - name: config             mountPath: /app/.env             subPath: .env           resources:             requests:               cpu: 1500m               memory: 1Gi             limits:               cpu: 1500m               memory: 1Gi       nodeSelector:         mcs.mail.ru/mcs-nodepool: converters       volumes:       - name: config         configMap:           name: converter-config

Теперь по поводу конфигураций. Обычно в кластере множество нод, и вы не будете знать, на какой конкретно ноде запустится ваш под. Помещать в Docker-контейнер конфигурации всех сред было бы некорректно — они должны подключаться внутрь. Поэтому в Kubernetes есть важный ресурс — configMap. Вы создаете configMap и контейнер и говорите Kubernetes, что при запуске пода необходимо подгрузить определенную конфигурацию из configMap, после чего ваш контейнер сможет ее использовать.

Создадим новый configMap на основе файла .env. Назовем его converter-config:

kubectl -n stage create configmap converter-config --from-file=.env

Откроем его в формате YAML:

kubectl -n stage get configmap/converter-config -o yaml

Информация в нем хранится в виде пар <ключ>|<значение>. В нашем случае ключ один — .env. Но их может быть несколько:

Возвращаемся к Deployment. У нас есть volume с именем config, который смотрит на созданный нами configMap с именем converter-config. И из этого configMap мы берем значение ключа .env, создаем файл и монтируем его в /app/.env:

Теперь можно создать конвертер, применив созданный deployment:

kubectl -n stage apply -f yaml/deployment.yaml

Проверим, что появился новый под с помощью kubectl -n stage get pods:

Выводим логи пода kubectl -n stage logs -f converter-dp-68cdfdf9c8-ctbqc и видим, что конвертер находится в ожидании сообщений из RabbitMQ:

Давайте теперь отправим новый запрос в наше API. В качестве имени name укажем roger, а в video_url добавим адрес любого видео с YouTube на английском языке:

curl -X POST -d '{"name": "roger", "“video_url":  “https://www.youtube.com/watch?v=A72M2mZ2wHA"}' -s -H 'X-API-KEY: 804b95f13b714ee9912b19861faf3d25' http://api.stage.kis.im/requests | jq .

Запрос принят:

Если теперь открыть логи пода cubectl -n stage logs -f converter-dp-68cdfdf9c8-ctbqc, то можно увидеть все этапы обработки нашего запроса в конвертере:

В конце выводится общее время выполнения — 23 секунды.

Давайте обратимся к нашему API и получим конкретный request по имени roger:

curl -X GET -s -H 'X-API-KEY: 804b95f13b714ee9912b19861faf3d25'  http://api.stage.kis.im/requests/roger | jq .

Здесь в поле text_url выводится URL для загрузки сформированного для нас файла в S3. Программный код необходимо доработать, чтобы URL возвращался вместе с расширением .txt: сейчас сохраняется без расширения:

Можем вывести содержимое файла через CURL:

curl http://converted.hb.bizmrg.com/converted/roger.txt

Получим текстовую расшифровку переданного нами видео:

Если теперь зайти в облако MCS в созданный нами бакет converted, то в директории /converted будет размещаться итоговый файл:

Таким образом, проверка нашего MVP-решения успешно выполнена.

Автомасштабирование в Kubernetes

Далее было бы неплохо, чтобы наш сервис мог автоматически масштабироваться. Так как перевод аудио в текст занимает некоторое время, при поступлении большого количества ссылок на конвертацию один Worker может обрабатывать их очень долго. В таких случаях необходимо автоматически добавлять новые Worker, чтобы они быстро все обрабатывали, а при отсутствии задач отключались.

В этом нам помогут Cluster Autoscaler и Horizontal Pod Autoscaler, доступные в Kubernetes. Первый отвечает за создание новых нод, второй — за увеличение числа подов. Кроме них есть еще Vertical Pod Autoscaler, изменяющий ресурсы для пода, но мы его рассматривать не будем.

Вернемся к deployment нашего конвертера. При создании нового пода необходимо определить, сколько ресурсов ему потребуется. Для этого предназначена секция resources, состоящая из двух разделов: requests и limits:

         resources:             requests:               cpu: 1500m               memory: 1Gi             limits:               cpu: 1500m               memory: 1Gi

Блок requests анализируется планировщиком Kubernetes. Когда создается новый под, планировщик его берет и назначает на какую-то ноду, опираясь на большое количество критериев и правил. Могут учитываются Labels (как мы делали с Node Selector), Affinity, Anti-Affinity, Taints, Tolerations и так далее.

Нас сейчас интересуют именно ресурсы. Как мы видели ранее, у каждой ноды есть доступное количество ресурсов (allocatable). В нашем случае, например, 1930 миллиядер. Соответственно, один под, которому требуется 1,5 ядра, может быть размещен на данной ноде, а второму ресурсов уже не хватит. Поэтому планировщик разместит его на имеющуюся свободную ноду.

Второй блок в описании ресурсов limits — это уже жесткие лимиты, аналог Docker Limits. Мы ограничиваем наше приложение: использовать не более 1,5 ядер и 1 ГБ памяти.

От заполнения секции resources зависит очень важный момент. Если применить команду describe к нашему поду, можно получить его QoS (Quality of Service) Class. В нем возможны три значения: Best Effort, Burstable и Guaranteed. Guaranteed назначается тем подам, у которых все контейнеры имеют одинаковые limits и requests. Если requests либо limits заполнены частично либо не совпадают друг с другом (limits выше), мы получаем класс Burstable. Если же resources не заполнены или вовсе убраны, то это класс Best Effort. Соответственно, если Kubernetes обнаружит проблему с нодой, он в первую очередь будет убивать класс Best Effort, затем Burstable и только потом Guaranteed. Поэтому всегда заполняйте секцию resources:

Если применить kubectl describe node kub-vc-dev-converters-0 к ноде, то можно увидеть все требования к ресурсам для сервисов, запущенных на ноде:

Теперь переходим непосредственно к автоскейлингу. Давайте увеличим вручную число подов под наш конвертер до двух, и выведем список подов:

kubectl -n stage scale deploy converter-dp --replicas=2 kubectl -n stage get pods

Новый под пока отображается в статусе Pending:

Применим к нему kubectl -n stage describe pod converter-dp-68cdfdf9c8-6tfvr и посмотрим секцию Events:

Из трех нод доступно ноль. Одна нода не попадает по Node Selector, который мы указали, а у двух нод недостаточно CPU. И после этого в дело включается Cluster Autoscaler. Он видит, что новый под запуститься не может: на ноде доступно 1930 миллиядер, а для двух подов требуется 3000. Поэтому группа узлов, для которой мы предварительно указали опцию «Включить автомасштабирование», начинает самостоятельно расширяться до двух нод. Если зайти в консоль управления облаком MCS, то можно увидеть статус кластера «Производится масштабирование кластера»:

В этом преимущество автоскейлинга в облаках: от нас ничего не требуется. Будет автоматически создана новая нода с тем же Node Selector, и новый под запустится на ней. Подождем некоторое время и проверим поды:

kubectl -n stage get pods

Оба контейнера запустились:

Теперь проверим ноды:

kubectl get nodes

Появилась новая нода:

Давайте теперь уменьшим число реплик обратно до одной:

kubectl -n stage scale deploy converter-dp --replicas=1

Проверим, что запустилось уничтожение нового пода с помощью kubectl -n stage get pods:

Через некоторое время Cluster Autoscaler увидит, что новая нода никак не используется, и уничтожит и ее — и у нас опять останется ровно одна нода в группе узлов.

Таким образом, мы рассмотрели, как при ручном увеличении числа подов Cluster Autoscaler добавляет новую ноду в группу узлов. Осталось научиться выполнять автомасштабирование подов при увеличении нагрузки на них. Для этого в Kubernetes существует ресурс HPA (Horizontal Pod Autoscaler).

Создадим hpa.yaml под нашу задачу. Заполняем имя — converter_hpa. В scaleTragetRef указываем deployment, к которому будет применяться масштабирование — converter-dp. В minReplicas и maxReplicas вводим минимальное и максимальное число подов — 1 и 5. В секции resource выбираем в качестве отслеживаемой метрики CPU и указываем его допустимое значение, при превышении которого запускать увеличение подов — 50%. Мы намеренно указываем низкое значение, чтобы продемонстрировать работу HPA:

apiVersion: autoscaling/v2beta1 kind: HorizontalPodAutoscaler metadata:   name: converter-hpa spec:   scaleTargetRef:     apiVersion: apps/v1     kind: Deployment     name: converter-dp   minReplicas: 1   maxReplicas: 5   metrics:   - type: Resource     resource:       name: cpu       targetAverageUtilization: 50

Применяем hpa.yaml к Namespace stage:

kubectl -n stage apply -f hpa.yaml

Если выполнить команду kubectl -n stage top pods, можно увидеть, сколько ресурсов потребляют поды:

При вызове get hpa видим превышение потребления CPU на 15%:

Применим kubectl -n stage describe hpa converter-hpa к нашему converter-hpa и посмотрим, что происходит. В секции Events можно увидеть увеличение числа подов до двух: «New size: 2». Horizontal Pod Autoscaler увеличил число подов:

В общем списке под также появился, в статусе Pending, смотрим с помощью kubectl -n stage get pods:

А если к новому поду применить команду describe, то в секции Events увидим, как к скейлингу подключился Cluster Autoscaler. Horizontal Pod Autoscaler увеличил количество подов, но подам не хватает ресурсов — и Cloud Autoscaler добавляет ноды:

Однако схема с отслеживанием CPU не самая подходящая для нашего приложения. Предположим, у нас в очереди одно сообщение. Worker берет его в обработку и загружает весь CPU. В ответ на это HPA создает новый под, а Cluster Scaler — новую ноду. Но новых сообщений в очереди еще нет, и поду нечего обрабатывать — в итоге дополнительно оплачиваемая нода будет простаивать.

Очевидно, что в качестве метрики нас интересует не CPU, а количество сообщений в очереди. Нам нужно настроить HPA таким образом, чтобы количество сообщений в очереди было не больше одного. Если их больше — можно увеличивать количество подов.

В Kubernetes api-resources есть встроенные метрики. Они находятся в группе metrics.k8s.io. За них отвечает сервис kube-system/metrics-server. Metrics-server следит за подами, нодами и создает соответствующие ресурсы PodMetrics и NodeMetrics, которые используются в Horizontal Pod Autoscaler для принятия решения об изменении количества подов.

Применив команду kubectl -n stage get podmetrics, можно посмотреть на PodMetrics наших сервисов:

Вызвав ту же команду kubectl -n stage get pods podmetrics rabbitmq-0 -o yaml для конкретной метрики rabbitmq-0, увидим, что RabbitMQ, например, у нас использует 121 миллиядро и 119 МБ памяти:

Наша следующая задача — добавление кастомных метрик для RabbitMQ с возможностью их использования в HPA.

В третьей части мы организуем мониторинг с помощью Prometheus, построим CI/CD и даже разработаем собственный Helm-чарт.

Новым пользователям платформы Mail.ru Cloud Solutions доступны 3000 бонусов после полной верификации аккаунта. Вы сможете повторить сценарий из статьи или попробовать другие облачные сервисы.

И обязательно вступайте в сообщество Rebrain в Telegram — там постоянно разбирают различные проблемы и задачи из сферы Devops, обсуждают вещи, которые пригодятся и на собеседованиях, и в работе.

Что еще почитать по теме:

  1. Как развернуть кластер Kubernetes на платформе MCS.

  2. Запускаем etcd-кластер для Kubernetes.

  3. Как устроен Kubernetes aaS на платформе Mail.ru Cloud Solutions.

ссылка на оригинал статьи https://habr.com/ru/company/mailru/blog/549804/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *