![](https://habrastorage.org/getpro/habr/upload_files/e66/71c/d3b/e6671cd3bebd801965d51e24a391f262.jpg)
Это продолжение практикума по развертыванию 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:
![](https://habrastorage.org/getpro/habr/upload_files/280/52d/ce5/28052dce513cb7c08be693e52c7bc078.png)
Команда kubectl –n stage get ing
позволяет посмотреть их внешние API:
![](https://habrastorage.org/getpro/habr/upload_files/b34/f97/e9f/b34f97e9fddca841a91edc6ce4ab4550.jpg)
Так как мы будем запускать конвертер внутри кластера, то нам достаточно внутренних адресов. Итоговое имя хоста формируется по маске <NAME из вывода первой команды>.<Namespace (в нашем случае stage)>.svc.
Далее в переменных RABBIT_USER, RABBIT_PASS, RABBIT_QUEUE указываются пользователь, пароль и название очереди в RabbitMQ.
Затем идут настройки для подключения к S3. На них остановимся подробнее. Вначале создадим бакет S3 в облаке MCS. Для этого выберем команду «Создать бакет» в пункте меню «Объектное хранилище». В качестве названия бакета укажем converted (скопировав его в переменную S3_BUCKET), а в качестве класса хранения — Hotbox. Для нашего приложения требуется хранение горячих данных. Использовать Icebox рекомендуется при редких обращениях к данным, например, несколько раз в месяц:
![](https://habrastorage.org/getpro/habr/upload_files/786/f42/105/786f4210547fefcb00a083af3b64b937.jpg)
Стоит отметить, что MCS из коробки предлагает подключение CDN к вашим бакетам, а также привязку собственного домена. В нашем приложении мы это использовать не будем, но функции очень полезные.
Далее в пункте меню «Объектное хранилище» — «Аккаунты» создаем новый аккаунт worker для подключения к бакету:
![](https://habrastorage.org/getpro/habr/upload_files/277/d37/071/277d37071bcb5de506b3e2bcb0a5ce4f.jpg)
Затем копируем его токены Access Key Id и Secret Key в переменные S3_ACCESS_KEY и S3_SECRET_KEY, соответственно:
![](https://habrastorage.org/getpro/habr/upload_files/dd9/ec9/83f/dd9ec983fce8b006cebc9c3a65381a09.jpg)
В пункте меню «Объектное хранилище» можно посмотреть S3 Endpoint URL. Именно это значение мы прописываем в переменной S3_ENDPOINT. Для доступа к конкретному бакету в начале этого URL дописывается название бакета: http://converted.hb.bizmrg.com. Это значение мы прописываем в переменной S3_WEB:
![](https://habrastorage.org/getpro/habr/upload_files/941/a59/14f/941a5914f7758589a72caacb481cdd41.jpg)
Развертывание и проверка обработчиков Worker в кластере Kubernetes
Так как конвертеру понадобится дополнительное количество CPU, мы не будем запускать его ни на Master-ноде, ни на рабочей ноде, на которой размещаются наши API и RabbitMQ. Поэтому в облаке MCS добавим новую группу узлов в наш кластер.
Для этого возвращаемся в раздел «Кластеры», напротив нашего кластера нажимаем на три точки и выбираем «Добавить группу узлов»:
![](https://habrastorage.org/getpro/habr/upload_files/9d0/98f/010/9d098f0103ffb208f0d63e90329b9b40.jpg)
Назовем ее converters, пусть у нее будет 2 CPU и 4 ГБ памяти, SSD на 50 ГБ и один узел по умолчанию.
Важный момент: установим флажок «Включить автомасштабирование» и укажем минимальное количество узлов равным 1, а максимальное количество — равным 5. Это необходимо для работы автомасштабирования, которое мы позднее рассмотрим:
![](https://habrastorage.org/getpro/habr/upload_files/4bd/346/333/4bd3463336a0831c2e18541cc1fff5f8.jpg)
Проверяем, что у нас появилась нода с помощью команды kubectl get nodes
:
![](https://habrastorage.org/getpro/habr/upload_files/31d/c54/395/31dc54395129d4c42637a722a568eb45.jpg)
При помощи команды get node можно вывести все параметры ноды в формате YAML:
kubectl get node kub-vc-dev-converters-0 -o yaml
Обратите внимание на секцию allocatable: здесь отображается, какие ресурсы и в каком объеме могут быть размещены на ноде. Например, в поле cpu видим, что на ноде можно разместить 1930 milicores, или миллиядер (в одном ядре 1000 миллиядер):
![](https://habrastorage.org/getpro/habr/upload_files/542/4af/780/5424af7803b2430ac15f14f942fae9a9.jpg)
В секции labels отображаются все метки ноды. Нас интересует метка msc.mail.ru/mcs-nodepool: converters
. Мы пропишем ее в deployment нашего конвертера для явного указания того, на каком node-пуле может запускаться приложение:
![](https://habrastorage.org/getpro/habr/upload_files/bb9/13e/212/bb913e212a2ba5b344c7fdca2f24e8b0.jpg)
Для начала создадим 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. Но их может быть несколько:
![](https://habrastorage.org/getpro/habr/upload_files/c90/92d/201/c9092d201bfee9b5d0d79672d130b458.jpg)
Возвращаемся к Deployment. У нас есть volume с именем config, который смотрит на созданный нами configMap с именем converter-config. И из этого configMap мы берем значение ключа .env, создаем файл и монтируем его в /app/.env:
![](https://habrastorage.org/getpro/habr/upload_files/e03/db9/aff/e03db9affc89f64f56ffce72b0ec741f.jpg)
Теперь можно создать конвертер, применив созданный deployment:
kubectl -n stage apply -f yaml/deployment.yaml
Проверим, что появился новый под с помощью kubectl -n stage get pods
:
![](https://habrastorage.org/getpro/habr/upload_files/0c0/902/eea/0c0902eea6c2e3231c2b8508bb13fc7a.jpg)
Выводим логи пода kubectl -n stage logs -f converter-dp-68cdfdf9c8-ctbqc
и видим, что конвертер находится в ожидании сообщений из RabbitMQ:
![](https://habrastorage.org/getpro/habr/upload_files/80d/605/dc9/80d605dc9e428b66681b129bd29f13ef.jpg)
Давайте теперь отправим новый запрос в наше 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 .
Запрос принят:
![](https://habrastorage.org/getpro/habr/upload_files/944/425/94e/94442594e8b32e1cdfe07db2c002a78c.jpg)
Если теперь открыть логи пода cubectl -n stage logs -f converter-dp-68cdfdf9c8-ctbqc
, то можно увидеть все этапы обработки нашего запроса в конвертере:
![](https://habrastorage.org/getpro/habr/upload_files/735/dae/b13/735daeb1386511ba090a16d540383b5a.jpg)
В конце выводится общее время выполнения — 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: сейчас сохраняется без расширения:
![](https://habrastorage.org/getpro/habr/upload_files/923/a52/e24/923a52e24ee429355d5c69e85957d5aa.jpg)
Можем вывести содержимое файла через CURL:
curl http://converted.hb.bizmrg.com/converted/roger.txt
Получим текстовую расшифровку переданного нами видео:
![](https://habrastorage.org/getpro/habr/upload_files/b8e/72a/97a/b8e72a97a98a930ceb1b4ececaa7d053.jpg)
Если теперь зайти в облако MCS в созданный нами бакет converted, то в директории /converted будет размещаться итоговый файл:
![](https://habrastorage.org/getpro/habr/upload_files/6d3/fd5/c08/6d3fd5c085620ef7ce9cf41f5bda615b.jpg)
Таким образом, проверка нашего 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:
![](https://habrastorage.org/getpro/habr/upload_files/91c/942/499/91c94249989987653b014c680ce4ca6d.jpg)
Если применить kubectl describe node kub-vc-dev-converters-0
к ноде, то можно увидеть все требования к ресурсам для сервисов, запущенных на ноде:
![](https://habrastorage.org/getpro/habr/upload_files/423/91e/241/42391e241404f1b02d541aaaec43c9a4.jpg)
Теперь переходим непосредственно к автоскейлингу. Давайте увеличим вручную число подов под наш конвертер до двух, и выведем список подов:
kubectl -n stage scale deploy converter-dp --replicas=2
kubectl -n stage get pods
Новый под пока отображается в статусе Pending:
![](https://habrastorage.org/getpro/habr/upload_files/51e/442/a6c/51e442a6cb29ba9b5ea60bb20f8378d1.jpg)
Применим к нему kubectl -n stage describe pod converter-dp-68cdfdf9c8-6tfvr
и посмотрим секцию Events:
![](https://habrastorage.org/getpro/habr/upload_files/da3/c9d/5b5/da3c9d5b53e72457c92acb3e8e3cfcbc.jpg)
Из трех нод доступно ноль. Одна нода не попадает по Node Selector, который мы указали, а у двух нод недостаточно CPU. И после этого в дело включается Cluster Autoscaler. Он видит, что новый под запуститься не может: на ноде доступно 1930 миллиядер, а для двух подов требуется 3000. Поэтому группа узлов, для которой мы предварительно указали опцию «Включить автомасштабирование», начинает самостоятельно расширяться до двух нод. Если зайти в консоль управления облаком MCS, то можно увидеть статус кластера «Производится масштабирование кластера»:
![](https://habrastorage.org/getpro/habr/upload_files/987/7e7/d7a/9877e7d7ab0efbe87e680f7b6b589f29.jpg)
В этом преимущество автоскейлинга в облаках: от нас ничего не требуется. Будет автоматически создана новая нода с тем же Node Selector, и новый под запустится на ней. Подождем некоторое время и проверим поды:
kubectl -n stage get pods
Оба контейнера запустились:
![](https://habrastorage.org/getpro/habr/upload_files/b34/d58/e0a/b34d58e0a9c16e8151a36deebd5921e6.jpg)
Теперь проверим ноды:
kubectl get nodes
Появилась новая нода:
![](https://habrastorage.org/getpro/habr/upload_files/930/b0f/9e0/930b0f9e0d82d58c9e9b54a9cf306a4a.jpg)
Давайте теперь уменьшим число реплик обратно до одной:
kubectl -n stage scale deploy converter-dp --replicas=1
Проверим, что запустилось уничтожение нового пода с помощью kubectl -n stage get pods
:
![](https://habrastorage.org/getpro/habr/upload_files/d9c/ef6/335/d9cef6335fc096bdfa10b2688edbf237.jpg)
Через некоторое время 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
, можно увидеть, сколько ресурсов потребляют поды:
![](https://habrastorage.org/getpro/habr/upload_files/8dc/753/3a6/8dc7533a6d9bb0ae804f593ac0dbd3da.jpg)
При вызове get hpa видим превышение потребления CPU на 15%:
![](https://habrastorage.org/getpro/habr/upload_files/36a/1fd/f6c/36a1fdf6cdbda0cc5f2323ab4e6c7afd.jpg)
Применим kubectl -n stage describe hpa converter-hpa
к нашему converter-hpa и посмотрим, что происходит. В секции Events можно увидеть увеличение числа подов до двух: «New size: 2». Horizontal Pod Autoscaler увеличил число подов:
![](https://habrastorage.org/getpro/habr/upload_files/d7a/ed1/5a4/d7aed15a40466633f0eeb2ca75f421cb.jpg)
В общем списке под также появился, в статусе Pending, смотрим с помощью kubectl -n stage get pods
:
![](https://habrastorage.org/getpro/habr/upload_files/853/f9a/44d/853f9a44db9eeec3e9d039b8554ddd42.jpg)
А если к новому поду применить команду describe, то в секции Events увидим, как к скейлингу подключился Cluster Autoscaler. Horizontal Pod Autoscaler увеличил количество подов, но подам не хватает ресурсов — и Cloud Autoscaler добавляет ноды:
![](https://habrastorage.org/getpro/habr/upload_files/116/2bf/5b8/1162bf5b8ed6f040f8265bfbd3066b3a.jpg)
Однако схема с отслеживанием 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 наших сервисов:
![](https://habrastorage.org/getpro/habr/upload_files/397/1d6/fd2/3971d6fd29e2b8f8e57d1977fe9ed421.jpg)
Вызвав ту же команду kubectl -n stage get pods podmetrics rabbitmq-0 -o yaml
для конкретной метрики rabbitmq-0, увидим, что RabbitMQ, например, у нас использует 121 миллиядро и 119 МБ памяти:
![](https://habrastorage.org/getpro/habr/upload_files/76d/69c/1f9/76d69c1f9ece5b50e24ad96392063a1f.jpg)
Наша следующая задача — добавление кастомных метрик для RabbitMQ с возможностью их использования в HPA.
В третьей части мы организуем мониторинг с помощью Prometheus, построим CI/CD и даже разработаем собственный Helm-чарт.
Новым пользователям платформы Mail.ru Cloud Solutions доступны 3000 бонусов после полной верификации аккаунта. Вы сможете повторить сценарий из статьи или попробовать другие облачные сервисы.
И обязательно вступайте в сообщество Rebrain в Telegram — там постоянно разбирают различные проблемы и задачи из сферы Devops, обсуждают вещи, которые пригодятся и на собеседованиях, и в работе.
Что еще почитать по теме: