Перейти к основному содержимому
Версия: 3.0

Кластер с распределением нагрузки

В этой схеме несколько равноправных узлов iQBot обрабатывают диалоги одновременно. Балансировщик распределяет запросы iQChannels между узлами, узлы связаны через Redis и работают с одной базой данных PostgreSQL. Нагрузку увеличивают добавлением узлов, отказ одного узла не прерывает обслуживание, а изменения ботов, сделанные в Studio любого узла, сразу действуют на всех. Общие требования и сравнение с резервным узлом — в разделе Установка в режиме кластера.

Схема​

Балансировщик, равноправные узлы iQBot, Redis и общая база данных PostgreSQL с репликой

ЭлементНазначение
БалансировщикПринимает все запросы по общему адресу, который указан в IQB_EXTERNAL_URL, и проверяет узлы по /status. Запросы диалога одного клиента направляет на один и тот же узел, остальные запросы — на любой.
Узлы iQBotОдинаковые установки: ядро, Studio, консоль, свои NLU и Messaging. Каждый принимает сообщения клиентов и запросы сценаристов. Их два или больше.
RedisСвязь между узлами: задания (монтирование ботов, сброс кэшей, отзыв токенов), инвалидация кэша файлов ботов, комнаты socket.io для Studio и веб-чата, блокировки фоновых задач. Один экземпляр или Redis Cluster.
База данныхОдна PostgreSQL с репликой за единым адресом. В ней боты, диалоги, пользователи и модели NLU.

Что делает Redis​

Без Redis каждый узел держит ботов и настройки в собственной памяти и не узнаёт об изменениях, сделанных на другом узле. С IQB_CLUSTER_ENABLED=true и IQB_REDIS_URL узлы обмениваются:

  • заданиями: монтирование и размонтирование бота, сброс кэша процессов и контента, смена версии токенов пользователя, перезагрузка сервера выполняются на каждом узле;
  • инвалидацией кэша файлов: файл бота, записанный на одном узле, сбрасывается из кэша всех узлов, и процессы, действия, хуки и настройки перечитываются;
  • событиями realtime: комнаты socket.io живут в Redis, поэтому Studio и веб-чат, подключённые к одному узлу, получают события с других;
  • блокировками: обработку тайм-аутов сессии и загрузку Messaging для бота выполняет один узел.

Состояние диалогов, пользователи и модели NLU передаются через базу данных и в Redis не хранятся. Messaging каждого узла получает те же переменные и использует Redis для своей синхронизации.

Что проверено​

Поведение проверено на стенде из двух узлов с Redis и общей базой данных (методика и результаты — в репозитории платформы, docs/load-testing):

  • правка текста ответа в Studio первого узла видна в ответах бота второго узла через 0,3–0,4 с;
  • бот, созданный на первом узле, появляется в списке ботов второго и отвечает на нём сразу;
  • выход сотрудника из системы на первом узле отзывает его токен на втором немедленно;
  • пересохранение процесса в Studio первого узла приходит событием в Studio, открытую на втором;
  • клиент, сообщения которого попадали по очереди на разные узлы, продолжал диалог с нужного шага;
  • тайм-аут сессии обрабатывается одним узлом;
  • два узла обработали 120 сообщений в секунду против 79 у одного; Redis на пропускную способность не повлиял.

Сколько узлов нужно​

Расчёт числа узлов и пропускная способность одного узла описаны в разделе Требования к серверам. Добавьте один узел запаса: при отказе узла остальные принимают его клиентов.

Узел ограничивает число одновременно обрабатываемых сообщений (IQB_CONVERSE_MAX_IN_FLIGHT, по умолчанию 100). Сверх лимита запрос сразу получает ответ 503 с заголовком Retry-After, а не ждёт в очереди до тайм-аута. Если такие ответы появляются при нормальной нагрузке, узлов не хватает.

Что нужно подготовить​

  • Не менее двух серверов для узлов iQBot. Конфигурацию каждого подбирают по разделу Требования к серверам.
  • Отказоустойчивая PostgreSQL с единым адресом записи, см. общие требования.
  • Redis 6 или новее, доступный со всех узлов. Для отказоустойчивости — Redis Sentinel с виртуальным адресом или Redis Cluster (адреса узлов задают JSON-массивом в IQB_REDIS_URL). При недоступности Redis узлы продолжают обслуживать диалоги, но перестают узнавать об изменениях друг друга, пока Redis не вернётся.
  • Балансировщик с HTTPS: NGINX или аппаратный. Пример ниже для NGINX.
  • Общее DNS-имя, например bot.example.com. Его указывают в IQB_EXTERNAL_URL на всех узлах и в iQChannels при подключении бот-платформы.
  • Дистрибутив и файлы языковых моделей одной версии для всех узлов.

Шаг 1. Установите первый узел​

Выполните шаги 2–7 раздела Установка iQBot на Linux. В файле .env задайте значения, общие для всех узлов:

IQB_DATABASE_URL=postgres://iqbot:<пароль>@db.example.com:5432/iqbot
IQB_IQBFS_STORAGE=database
IQB_PRODUCTION=true
IQB_EXTERNAL_URL=https://bot.example.com
IQB_REVERSE_PROXY=true
IQB_CLUSTER_ENABLED=true
IQB_REDIS_URL=redis://redis.example.com:6379
IQB_REDIS_SCOPE=iqbot-prod
IQB_ADMIN_EMAIL=admin@example.com
IQB_ADMIN_PASSWORD=<стойкий пароль>
ПеременнаяНазначение
IQB_CLUSTER_ENABLED=trueВключает обмен между узлами через Redis. Без IQB_REDIS_URL узел пишет предупреждение и работает один.
IQB_REDIS_URLАдрес Redis: строка redis://[:пароль@]хост:порт[/база] или JSON-массив узлов Redis Cluster [{"host":"a","port":6379},{"host":"b","port":6379}].
IQB_REDIS_SCOPEПрефикс ключей и каналов. Задайте, если один Redis обслуживает несколько установок iQBot.
IQB_REDIS_OPTIONSНеобязательно: JSON с параметрами клиента ioredis, например {"password":"...","tls":{}}.
IQB_REVERSE_PROXY=trueПлатформа работает за обратным прокси и берёт адрес клиента из его заголовков.

Первый запуск на пустой базе создаёт таблицы, поэтому первый узел запускайте один и дождитесь, пока curl http://localhost:3000/status вернёт код 200 и "ready":true. В журнале узла должна быть строка Cluster mode: node <id> joined the core job bus и socket.io rooms are shared through Redis.

Шаг 2. Установите остальные узлы​

На каждом следующем узле повторите шаги 2–5 и 7 раздела Установка iQBot на Linux и скопируйте файл .env с первого узла без изменений. Узлы запускайте по одному. Порт 3000 узлов откройте на файрволе только для балансировщика.

Шаг 3. Настройте балансировщик​

Запросы диалога содержат идентификатор клиента в адресе: /api/v1/bots/<бот>/converse/<клиент>. Балансировщик должен отправлять сообщения одного клиента на один и тот же узел, пока тот доступен: так два быстрых сообщения подряд не обрабатываются на разных узлах одновременно. Остальные запросы, включая Studio и консоль, может принимать любой узел. Проверка состояния узла — GET /status: узел отвечает 503, пока запускается и пока завершает работу, и 200, когда готов.

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

upstream iqbot_dialog {
hash $uri consistent;
server 10.0.0.11:3000 max_fails=2 fail_timeout=10s;
server 10.0.0.12:3000 max_fails=2 fail_timeout=10s;
}

upstream iqbot_ui {
ip_hash;
server 10.0.0.11:3000 max_fails=2 fail_timeout=10s;
server 10.0.0.12:3000 max_fails=2 fail_timeout=10s;
}

map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}

server {
listen 443 ssl;
server_name bot.example.com;
ssl_certificate /etc/nginx/ssl/bot.example.com.crt;
ssl_certificate_key /etc/nginx/ssl/bot.example.com.key;

# Диалоги клиентов от iQChannels
location ~ ^/api/v1/bots/[^/]+/converse/ {
proxy_pass http://iqbot_dialog;
proxy_next_upstream error timeout http_503;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

# Studio, консоль, веб-чат и остальное API
location / {
proxy_pass http://iqbot_ui;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

hash $uri consistent выбирает узел по адресу запроса, а значит, по клиенту; при выходе узла из строя на другие узлы переходят только его клиенты. ip_hash держит браузер сценариста и посетителя веб-чата на одном узле: события Studio всё равно приходят со всех узлов через Redis, а для соединения socket.io это обязательное условие (см. ниже). proxy_next_upstream ... http_503 передаёт запрос другому узлу, если первый перегружен или завершает работу. В NGINX Plus и аппаратных балансировщиках дополнительно включите активную проверку GET /status с ожидаемым кодом 200.

WebSocket и «липкие» сессии​

Studio, консоль и веб-чат (в том числе эмулятор в Studio) получают события по соединению socket.io по адресу /socket.io/. Балансировщик и все прокси перед узлами должны выполнять два условия.

  • Пропускать WebSocket. В NGINX для этого служат заголовки Upgrade и Connection из примера выше (proxy_http_version 1.1, proxy_set_header Upgrade $http_upgrade, proxy_set_header Connection $connection_upgrade). В других прокси, WAF и облачных балансировщиках включите поддержку WebSocket. Если WebSocket недоступен, клиенты автоматически переходят на обычные запросы (long polling), и всё продолжает работать, но с лишней нагрузкой: вместо одного соединения идёт поток коротких запросов. Признак проблемы в консоли браузера: ошибки WebSocket connection to 'wss://…/socket.io/…' failed и множество запросов GET …/socket.io/?…transport=polling.
  • Держать клиента на одном узле («липкие» сессии). Это нужно для long polling: сессия socket.io живёт на узле, который её создал, и если следующий запрос того же браузера попадёт на другой узел, тот ответит Session ID unknown, и соединение оборвётся. Для WebSocket это условие тоже полезно: после обрыва клиент подключается к тому же узлу. В NGINX используйте ip_hash или hash $cookie_… для location / (так сделано в примере), в аппаратных балансировщиках включите привязку по cookie или по IP-адресу клиента. Redis этого не заменяет: он передаёт между узлами события, а не сессии соединений.

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

Затем в iQChannels подключите бот-платформу по общему адресу https://bot.example.com, как описано в разделе Подключение к iQChannels.

Как вносить изменения в ботов​

Сценаристы работают в Studio по общему адресу https://bot.example.com/studio/, на каком узле — не важно. Правки процессов, содержимого, намерений и настроек действуют на всех узлах через несколько десятых секунды, перезапуск не нужен. Обучение NLU запускают там же: модель попадает в базу и используется всеми узлами.

Что требует перезапуска узлов по очереди: обновление версии платформы и изменение переменных окружения. Порядок перезапуска описан ниже.

Перезапуск узла без потери сообщений​

При остановке службы (systemctl stop или restart, сигнал SIGTERM) узел сначала переводит /status в 503 и отвечает 503 на новые сообщения, чтобы балансировщик передал их другим узлам (proxy_next_upstream ... http_503 в примере выше), затем дожидается ответов на сообщения, которые уже обрабатывает (не дольше IQB_DRAIN_TIMEOUT_MS, по умолчанию 15 с), и только потом завершается вместе со своими NLU, Messaging и Studio. Убедитесь, что TimeoutStopSec службы systemd больше этого времени (по умолчанию 90 с, достаточно).

Порядок при обновлении настроек:

  1. Перезапустите один узел: sudo systemctl restart iqbot.
  2. Дождитесь, когда curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/status вернёт 200.
  3. Повторите для следующего узла.

Проверка​

Проверьте схему до выхода в продуктив. Запросы ниже идут к узлам напрямую, минуя балансировщик, поэтому на время проверки откройте порт 3000 узлов с вашего компьютера.

  1. Отправьте первое сообщение через первый узел:

    curl -X POST http://node1.example.com:3000/api/v1/bots/<бот>/converse/test-user \
    -H 'Content-Type: application/json' -d '{"type":"text","text":"Привет"}'
  2. Отправьте следующее сообщение того же клиента через второй узел, подставив node2 и тот же идентификатор test-user. Ответ должен продолжать диалог, а не начинать его заново.

  3. Измените в Studio первого узла текст ответа бота и повторите первый запрос ко второму узлу с новым идентификатором клиента: ответ должен содержать новый текст.

  4. Выйдите из консоли на первом узле и попробуйте использовать тот же токен на втором: он должен быть отклонён.

  5. Остановите один узел и отправьте сообщение через балансировщик: ответ должен прийти, а клиент, обслуживавшийся остановленным узлом, должен продолжить диалог на другом.

Те же проверки в виде скрипта лежат в репозитории платформы: scripts/load-test/functional-checks.js.

Отказы и обновление​

Что вышло из строяЧто происходитЧто делать
Узел iQBotБалансировщик исключает его, клиенты переходят на остальные узлы и продолжают диалог. Сообщения, обрабатывавшиеся в момент отказа, могут остаться без ответа.Восстановить узел и вернуть его в балансировку.
RedisУзлы продолжают обслуживать диалоги, но не узнают об изменениях друг друга; Studio и веб-чат получают события только со своего узла.Восстановить Redis. Узлы переподключаются сами; правки, сделанные за время простоя Redis, на других узлах появятся после перезапуска этих узлов.
База данныхОстанавливается вся система.Переключить базу на реплику, проверить /status на всех узлах, при необходимости перезапустить службы.

Обновление версии платформы меняет структуру базы, поэтому на время миграции работу всех узлов останавливают. Остановите все узлы, сделайте резервную копию базы (Резервное копирование и обновление), замените дистрибутив на всех узлах, один раз запустите один узел с IQB_AUTO_MIGRATE=true, затем запустите остальные по одному.

Ограничения​

  • Состояние диалога храните в переменных сессии и пользователя. Значения в глобальных переменных кода действий лежат в памяти одного узла и другим узлам недоступны.
  • Резервирование iQChannels и iQGPT выполняется в этих продуктах отдельно.