Салют, Хабр!
Auto-scaling, или автоматическое масштабирование, — это механизм, позволяющий гибко адаптироваться к изменяющимся условиям нагрузки, автоматически расширяя или сокращая ресурсы. Эта технология очень актуальна в нашем мире.
К примеру вы создали свой невероятно крутой телеграм-бот, который стал неожиданно стрельнул. Сначала он справлялся со стабильным потоком запросов, но по мере роста и увеличения активности сабов бота, стало очевидно, что требуется более масштабируемая инфраструктура.
Auto-scaling позволит не только поддерживать стабильную работу бота при всплесках активности, но и существенно снизит затраты на поддержание избыточной инфраструктуры в периоды низкой активности.
База Auto-Scaling:
Основная цель auto-scaling - поддержание стабильной производительности при минимизации затрат.
Auto-scaling позволяет системе динамически адаптироваться к изменяющемуся объему работы, автоматически наращивая или уменьшая количество серверов или экземпляров приложений. Это помогает избежать перерасхода ресурсов в периоды низкой нагрузки и недостатка ресурсов в пиковые периоды.
Auto-scaling основывается на мониторинге ключевых показателей производительности, таких как CPU, память, сетевая активность или даже специфичные для приложения метрики, к примеру в случае с телеграм ботами - количество одновременных пользовательских сессий.
Определяются пороговые значения для этих метрик, при достижении которых система автоматически инициирует процесс масштабирования.
Горизонтальное масштабирование заключается в добавлении или удалении экземпляров серверов или контейнеров.
Вертикальное масштабирование включает увеличение или уменьшение ресурсов для существующих экземпляров (например, увеличение ОЗУ или CPU).
Все процессы автоскелинга автоматизированы и могут выполняться без человеческого вмешательства. После масштабирования система должна продолжать функционировать так же, как и до масштабирования, без потери данных или функциональности.
Определяются правила или политики, определяющие, когда и как следует проводить масштабирование. Эти правила могут быть основаны на расписании (например, разные параметры масштабирования для часов пик и внепиковых часов) или на адаптивных алгоритмах, реагирующих на изменения в реальном времени.
Когда и почему вашему боту может потребоваться Auto-Scaling.
Нагрузка на телеграм-бота определяется количеством запросов, которые он получает за единицу времени. Эти запросы могут варьироваться от простых команд до сложных интерактивных диалогов.
Типы нагрузки:
Пиковая нагрузка: Временные всплески активности, часто вызванные специфическими событиями или маркетинговыми акциями. Постоянная нагрузка: Регулярное количество запросов, которое поддерживается в течение длительного времени. Переменная нагрузка: Нерегулярные изменения в активности, часто связанные с временем суток или днями недели.
Метрики для анализа нагрузки:
Число активных пользователей (DAU/MAU): Ежедневно и ежемесячно активные пользователи. Количество сообщений в секунду/минуту (MPS/MPM): Среднее количество сообщений, обрабатываемых за секунду или минуту. Время отклика: Время, необходимое боту для ответа на запрос пользователя. Частота ошибок: Процент запросов, завершающихся неудачей.
Если метрики нагрузки превышают заранее определенные пороговые значения (например, время отклика или MPS), это является звоночком к масштабированию.
Как реализовать
Безсостоянийная архитектура (stateless architecture)
Основная фича состоит в том, что каждый запрос обрабатывается независимо, без сохранения какого-либо пользовательского состояния на сервере между запросами. Это позволяет любому экземпляру обрабатывать любой запрос.
Бот не хранит информацию о состоянии пользователя между последовательными запросами. Вся необходимая информация передается в каждом запросе. Все данные о состоянии пользователя и сессии хранятся во внешнем хранилище, к примеру БД. Каждый запрос должен быть идемпотентным, то есть его повторное выполнение должно приводить к одним и тем же результатам.
К примеру это может выглядеть так:
from telegram.ext import Updater, CommandHandler, MessageHandler, Filters
import os
import psycopg2
# Подключение к базе данных PostgreSQL
DATABASE_URL = os.getenv('DATABASE_URL')
conn = psycopg2.connect(DATABASE_URL, sslmode='require')
# Функция обработки команды /start
def start(update, context):
user_id = update.effective_user.id
with conn.cursor() as cur:
cur.execute("INSERT INTO users (user_id) VALUES (%s) ON CONFLICT DO NOTHING", (user_id,))
conn.commit()
update.message.reply_text("Привет! Я безсостоянийный телеграм-бот.")
# Функция для обработки обычных сообщений
def handle_message(update, context):
user_id = update.effective_user.id
# Получение данных пользователя из базы данных
with conn.cursor() as cur:
cur.execute("SELECT * FROM users WHERE user_id = %s", (user_id,))
user_data = cur.fetchone()
# Здесь можно обработать данные пользователя
update.message.reply_text("Ваше сообщение обработано!")
# основа примера
def main():
TOKEN = os.getenv('TELEGRAM_BOT_TOKEN')
updater = Updater(TOKEN, use_context=True)
dp = updater.dispatcher
dp.add_handler(CommandHandler("start", start))
dp.add_handler(MessageHandler(Filters.text & ~Filters.command, handle_message))
updater.start_polling()
updater.idle()
if __name__ == '__main__':
main()
Используется PostgreSQL для хранения данных о пользователях. Подключение осуществляется через psycopg2
. start
и handle_message
являются обработчиками команд и сообщений соответственно. Они взаимодействуют с базой данных для чтения или записи данных о пользователе.
Вся информацияо пользователе хранится в базе данных. Каждый запрос обрабатывается независимо, и состояние не сохраняется на сервере бота.
Системы управления очередями
Очереди позволяют приложениям асинхронно обрабатывать задачи. Производитель отправляет сообщения в очередь, а потребитель извлекает их и обрабатывает, когда готов. Помогают равномерно распределить нагрузку между различными рабочими узлами или процессами.
Они также увеличивают устойчивость системы к сбоям, поскольку сообщения могут быть сохранены и повторно обработаны в случае ошибок.
Позволяет легко добавлять дополнительные рабочие узлы для обработки сообщений.
Для примера сделаем интеграцию очереди RabbitMQ с ботом. В этом сценарии бот будет отправлять сообщения в очередь RabbitMQ, которая затем будет обрабатываться отдельными рабочими процессами:
Производитель: Телеграм-бот
import os
from telegram.ext import Updater, CommandHandler, MessageHandler, Filters
import pika
# Подключение к RabbitMQ
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.queue_declare(queue='telegram_queue')
# Функция обработки сообщений
def handle_message(update, context):
message = update.message.text
channel.basic_publish(exchange='',
routing_key='telegram_queue',
body=message)
update.message.reply_text('Сообщение отправлено в очередь!')
def main():
TOKEN = os.getenv('TELEGRAM_BOT_TOKEN')
updater = Updater(TOKEN, use_context=True)
dp = updater.dispatcher
dp.add_handler(MessageHandler(Filters.text & ~Filters.command, handle_message))
updater.start_polling()
updater.idle()
if __name__ == '__main__':
main()
Потребитель: Рабочий процесс обработки сообщений
import pika
def callback(ch, method, properties, body):
print(f"Получено сообщение: {body.decode()}")
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.queue_declare(queue='telegram_queue')
channel.basic_consume(queue='telegram_queue', on_message_callback=callback, auto_ack=True)
print('Ожидание сообщений. Для выхода нажмите CTRL+C')
channel.start_consuming()
RabbitMQ юзается как система управления очередями. Сообщения от телеграм-бота помещаются в очередь telegram_queue
. Бот отправляет полученные сообщения в очередь RabbitMQ. Каждое сообщение от пользователя становится отдельным сообщением в очереди.
Отдельный процесс (или несколько процессов) подписан на очередь и обрабатывает сообщения по мере их поступления.
Микросервисная архитектура
Думаю, микросервисы не нуждаются в представление.
Допустим, у нас есть бот, который предоставляет две основные функции: обработку текстовых команд и обработку изображений. В микросервисной архитектуре мы разделим эти функции на два отдельных сервиса:
TextCommandService: Обрабатывает текстовые команды от пользователей.
ImageProcessingService: Обрабатывает запросы на обработку изображений.
Оба сервиса могут быть развернуты независимо и масштабироваться в зависимости от нагрузки.
Напишем простой код, где оба микросервиса взаимодействуют через HTTP-запросы.
TextCommandService
# text_command_service.py
from flask import Flask, request
app = Flask(__name__)
@app.route('/process_text', methods=['POST'])
def process_text():
text = request.json.get('text')
# Обработка текстовой команды
response = f"Обработан текст: {text}"
return response
if __name__ == '__main__':
app.run(port=5000)
ImageProcessingService
# image_processing_service.py
from flask import Flask, request
app = Flask(__name__)
@app.route('/process_image', methods=['POST'])
def process_image():
image_data = request.json.get('image_data')
# Обработка изображения
response = f"Обработано изображение: {image_data[:10]}..."
return response
if __name__ == '__main__':
app.run(port=5001)
Телеграм-бот
# telegram_bot.py
import requests
from telegram.ext import Updater, CommandHandler, MessageHandler, Filters
def handle_text(update, context):
text = update.message.text
response = requests.post("http://localhost:5000/process_text", json={"text": text})
update.message.reply_text(response.text)
def handle_photo(update, context):
photo_file = update.message.photo[-1].get_file()
image_data = photo_file.download_as_bytearray()
response = requests.post("http://localhost:5001/process_image", json={"image_data": image_data})
update.message.reply_text(response.text)
def main():
TOKEN = 'YOUR_TELEGRAM_BOT_TOKEN'
updater = Updater(TOKEN, use_context=True)
dp = updater.dispatcher
dp.add_handler(MessageHandler(Filters.text & ~Filters.command, handle_text))
dp.add_handler(MessageHandler(Filters.photo, handle_photo))
updater.start_polling()
updater.idle()
if __name__ == '__main__':
main()
TextCommandService
и ImageProcessingService
запущены на разных портах и обрабатывают разные типы запросов. бот определяет тип входящего сообщения (текст или изображение) и перенаправляет запрос к соответствующему микросервису.
Каждый из микросервисов может быть масштабирован независимо. Например, если большинство запросов связано с обработкой изображений, ImageProcessingService
можно масштабировать, добавив больше экземпляров.
Контейнеризация и оркестрация
Контейнеризация — это процесс упаковки приложения вместе со всеми его зависимостями и конфигурациями в изолированный контейнер.
Пример Dockerfile
для телеграм-бота:
FROM python:3.12
# Устанавливаем рабочую директорию в контейнере
WORKDIR /bot
# Копируем файлы проекта в контейнер
COPY . /bot
# Устанавливаем зависимости
RUN pip install -r requirements.txt
CMD ["python", "./my_telegram_bot.py"]
Создаем образ Docker для телеграм-бота, начиная с базового образа Python, устанавливая необходимые зависимости и определяя команду для запуска бота.
Оркестрация контейнеров — это процесс управления жизненным циклом контейнеров.
Для развертывания телеграм-бота в Kubernetes можно создать файл конфигурации deployment.yaml
:
apiVersion: apps/v1
kind: Deployment
metadata:
name: telegram-bot-deployment
spec:
replicas: 2 # Начинаем с двух экземпляров
selector:
matchLabels:
app: telegram-bot
template:
metadata:
labels:
app: telegram-bot
spec:
containers:
- name: telegram-bot
image: my_telegram_bot:latest
ports:
- containerPort: 80
Для включения автоскейлинга в Kubernetes, можно использовать Horizontal Pod Autoscaler (HPA), который автоматически масштабирует количество подов в ответ на наблюдаемую нагрузку:
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
name: telegram-bot-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: telegram-bot-deployment
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 80
Этот HPA будет масштабировать количество подов для telegram-bot-deployment
от 2 до 10, основываясь на использовании CPU.
CI/CD
Непрерывная интеграция (CI) подразумевает автоматическое тестирование кода при каждом коммите в систему контроля версий.
Рассмотрим пример, где мы используем GitHub Actions для автоматического тестирования телеграм-бота при каждом коммите.
Создадим файл .github/workflows/python-app.yml
в репозитории на GitHub:
name: Python application
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout
- name: Set up Python 3.12
uses: actions/setup-python
with:
python-version: 3.12
- name: Install dependencies
run: |
pip install -r requirements.txt
- name: Run tests
run: |
pytest
on
: Определяет, когда должны выполняться действия (на push и pull request в ветку master).
jobs
: Определяет набор задач, которые должны быть выполнены (здесь только build
).
steps
: Последовательность шагов, включая проверку кода из репозитория, установку питона, установку зависимостей и запуск тестов с помощью pytest
.
Непрерывная доставка (CD) автоматизирует развертывание приложения в тестовую или рабочую среду после успешного прохождения тестов.
Допустим, мы хотим автоматически развернуть нашего телеграм-бота на Kubernetes после успешного прохождения тестов. Для этого нам потребуется Docker контейнер и Kubernetes конфигурация:
За пример возьмем докерфайл, который рассматривали выше. Настроим GitHub Actions для сборки и публикации Docker образа:
- name: Build and Push Docker image
if: github.event_name == 'push'
run: |
echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
docker build -t my_telegram_bot .
docker tag my_telegram_bot ${{ secrets.DOCKER_USERNAME }}/my_telegram_bot:latest
docker push ${{ secrets.DOCKER_USERNAME }}/my_telegram_bot:latest
Автоматическое развертывание в Kubernetes:
- name: Deploy to Kubernetes
if: github.event_name == 'push'
run: |
kubectl apply -f k8s-deployment.yaml
k8s-deployment.yaml
— это файл конфигурации Kubernetes, который описывает, как должен быть развернут и масштабирован телеграм-бот.
Грамотный автоскейлинг позволяет справляться с изменяющимися нагрузками и значительно повышает устойчивость и адаптивность бота.
Все инструменты для повышения устойчивости и производительности разрабатываемых систем можно изучить под руководством опытных инженеров на онлайн-курсах в OTUS.
Комментарии (2)
bevial
19.12.2023 22:23Какую нейроночку использовали для написания статьи? )))
Судя по иллюстрации от далишки, и некоторым паттернам, таким как чрезмерное разжевывание, это был chatgpt 4. Я прав? )))
ovsds
К сожалению горизонтального скалирования для телеграм бота нормального нет. Если вы запустите два инстанса, то увидете спам из следующего в трейсах:
telegram.error.Conflict: Conflict: terminated by other getUpdates request; make sure that only one bot instance is running
. Это вызывается особенностями того как устроен механизм получения обновлений ботом через long polling.В остальном - да, если вынести всю обработку, а на инстанс с телеграмм ботом оставить только хэндлинг, то можно очень не плохо выиграть, но к сожалению не в синхронном коде, поскольку в вашем примере обращение в сервисы обработки всё ещё блокирующие и по сути лишь негативно скажутся на времени обработки запросов (хоть и незначительно в случае если сервисы будут на одной ноде расположены).
Итого, если подытожить: что б выжать максимум из неинстансируемого приложения телеграмма, действительно нужно вынести всё что только можно в другие приложения, но само приложение-обработчик всё равно придётся скалировать только вертикально, да и плюс ко всему для увеличения РПС скорее всего придётся уйти в асинхронность, поскольку, по крайней мере выбранный в статье фреймворк не супер хорошо справляется в мультитрединг.
А так да, грамотный скейлинг - это важно. Форточку сам открою :)