Кластер с распределением нагрузки
В этой схеме несколько равноправных узлов iQBot обрабатывают диалоги одновременно. Балансировщик распределяет запросы iQChannels между узлами, узлы связаны через Redis и работают с одной базой данных PostgreSQL. Нагрузку увеличивают добавлением узлов, отказ одного узла не прерывает обслуживание, а изменения ботов, сделанные в Studio любого узла, сразу действуют на всех. Общие требования и сравнение с резервным узлом — в разделе Установка в режиме кластера.
Схема
| Элемент | Назначение |
|---|---|
| Балансировщик | Принимает все запросы по общему адресу, который указан в 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 с, достаточно).
Порядок при обновлении настроек:
- Перезапустите один узел:
sudo systemctl restart iqbot. - Дождитесь, когда
curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/statusвернёт200. - Повторите для следующего узла.
Проверка
Проверьте схему до выхода в продуктив. Запросы ниже идут к узлам напрямую, минуя балансировщик, поэтому на время проверки откройте порт 3000 узлов с вашего компьютера.
-
Отправьте первое сообщение через первый узел:
curl -X POST http://node1.example.com:3000/api/v1/bots/<бот>/converse/test-user \
-H 'Content-Type: application/json' -d '{"type":"text","text":"Привет"}' -
Отправьте следующее сообщение того же клиента через второй узел, подставив
node2и тот же идентификаторtest-user. Ответ должен продолжать диалог, а не начинать его заново. -
Измените в Studio первого узла текст ответа бота и повторите первый запрос ко второму узлу с новым идентификатором клиента: ответ должен содержать новый текст.
-
Выйдите из консоли на первом узле и попробуйте использовать тот же токен на втором: он должен быть отклонён.
-
Остановите один узел и отправьте сообщение через балансировщик: ответ должен прийти, а клиент, обслуживавшийся остановленным узлом, должен продолжить диалог на другом.
Те же проверки в виде скрипта лежат в репозитории платформы: scripts/load-test/functional-checks.js.
Отказы и обновление
| Что вышло из строя | Что происходит | Что делать |
|---|---|---|
| Узел iQBot | Балансировщик исключает его, клиенты переходят на остальные узлы и продолжают диалог. Сообщения, обрабатывавшиеся в момент отказа, могут остаться без ответа. | Восстановить узел и вернуть его в балансировку. |
| Redis | Узлы продолжают обслуживать диалоги, но не узнают об изменениях друг друга; Studio и веб-чат получают события только со своего узла. | Восстановить Redis. Узлы переподключаются сами; правки, сделанные за время простоя Redis, на других узлах появятся после перезапуска этих узлов. |
| База данных | Останавливается вся система. | Переключить базу на реплику, проверить /status на всех узлах, при необходимости перезапустить службы. |
Обновление версии платформы меняет структуру базы, поэтому на время миграции работу всех узлов останавливают. Остановите все узлы, сделайте резервную копию базы (Резервное копирование и обновление), замените дистрибутив на всех узлах, один раз запустите один узел с IQB_AUTO_MIGRATE=true, затем запустите остальные по одному.
Ограничения
- Состояние диалога храните в переменных сессии и пользователя. Значения в глобальных переменных кода действий лежат в памяти одного узла и другим узлам недоступны.
- Резервирование iQChannels и iQGPT выполняется в этих продуктах отдельно.