| title | Несколько экземпляров панели |
|---|---|
| description | Запуск нескольких экземпляров панели за балансировщиком: общая база, кэш, файлы и обмен событиями, сертификаты и типовые ошибки настройки. |
| layout | default |
| lang | ru |
| category | Администрирование |
| order | 338 |
Панель можно запустить в нескольких экземплярах за балансировщиком — ради отказоустойчивости или чтобы распределить нагрузку. Для этого экземпляры должны разделять состояние: базу данных, кэш, файлы и обмен событиями.
Для большинства установок это не нужно. Одна панель справляется с сотнями игровых серверов — тяжёлую работу выполняют демоны на выделенных серверах, а не панель.
| Компонент | Настройка | Что будет иначе |
|---|---|---|
| База данных | PostgreSQL или MySQL | SQLite не рассчитан на несколько подключённых экземпляров |
| Кэш | CACHE_DRIVER=redis |
При memory сессии, ключи установки и билеты входа SSO видит только один экземпляр |
| Обмен событиями | PUBSUB_DRIVER=redis или postgres |
При memory экземпляры не узнают о событиях друг друга |
| Файлы | FILES_DRIVER=s3 |
При local у каждого экземпляра свои файлы, сертификаты и файлы плагинов |
Нагляднее всего это видно на билетах входа SSO. POST /api/auth/sso/tickets кладёт одноразовый
билет в кэш, а POST /api/auth/sso/exchange забирает его оттуда — как правило, уже на другом
экземпляре, на который балансировщик направил браузер. При CACHE_DRIVER=memory обмен удастся,
только если попадёт на экземпляр, выпустивший билет, поэтому SSO будет работать через раз. Билет
живёт AUTH_SSO_TICKET_TTL (по умолчанию 60 секунд, не больше 120) — слишком мало, чтобы
выручали повторы. См. Безопасность.
Пример конфигурации:
DATABASE_DRIVER=postgres
DATABASE_URL=postgres://gameap:пароль@db.example.com:5432/gameap?sslmode=require
CACHE_DRIVER=redis
CACHE_REDIS_ADDR=redis.example.com:6379
PUBSUB_DRIVER=redis
PUBSUB_REDIS_ADDR=redis.example.com:6379
PUBSUB_INSTANCE_ID=panel-1
FILES_DRIVER=s3
FILES_S3_ENDPOINT=https://s3.example.com
FILES_S3_BUCKET=gameap
FILES_S3_ACCESS_KEY_ID=...
FILES_S3_SECRET_ACCESS_KEY=...AUTH_SECRET и ENCRYPTION_KEY должны быть одинаковыми на всех экземплярах: иначе токен,
выданный одним, не примет другой, а зашифрованные данные окажутся нечитаемыми. С версии 4.5 это
касается и секретов плагинов: таблица plugin_secrets шифруется ключом ENCRYPTION_KEY
(AES-256-GCM), поэтому экземпляр с другим ключом не прочитает секрет, записанный соседом, а при
PLUGINS_SECRETS_REQUIRE_ENCRYPTION=true (по умолчанию) экземпляр без ключа вообще откажется
записывать секреты.
PUBSUB_INSTANCE_ID=panel-1Значение должно быть уникальным для каждого экземпляра. По нему экземпляры отличают собственные сообщения от чужих и адресуют ответы друг другу.
Если переменную не задать, используется значение default — одинаковое у всех, и обмен между
экземплярами работать не будет. Задавайте её явно.
Демон подключается к одному из экземпляров — к тому, на который его направил балансировщик — и держит с ним постоянное соединение. Все остальные экземпляры к этому демону обращаться напрямую не могут.
Поэтому команда, пришедшая на экземпляр, который не владеет соединением с нужным демоном, передаётся через общий обмен событиями тому экземпляру, который им владеет; ответ возвращается тем же путём. Так же работают файловые операции, консоль и запросы метрик.
Из этого следует, что общий обмен событиями обязателен: без него панель будет управлять только теми демонами, которые подключились именно к этому экземпляру.
Задачи, появившиеся в базе, пока демон был отключён или пока его соединением владел другой экземпляр, доставляются сразу при подключении, а не при следующем переподключении.
Разделите два потока:
- HTTP (8025) — можно балансировать обычным образом, привязка сессии к экземпляру не нужна: токены проверяются по общему ключу, а сессионное состояние лежит в общем кэше.
- gRPC (31718) — соединения долгоживущие. Балансировщик должен уметь их держать и не рвать по таймауту простоя: собственного механизма keepalive в канале нет, живость держится на heartbeat раз в 30 секунд.
Задайте GRPC_EXTERNAL_HOST с адресом, по которому демоны видят панель через балансировщик,
и GRPC_EXTERNAL_PORT, если порт наружу опубликован под другим номером.
Сертификаты gRPC должны быть общими. Они хранятся в файловом хранилище панели, поэтому при
FILES_DRIVER=s3 автоматически оказываются общими для всех экземпляров.
При FILES_DRIVER=local каждый экземпляр сгенерирует собственный центр сертификации, и демон,
зарегистрированный через один экземпляр, не сможет подключиться к другому.
Если сертификат панели выпускается через ACME, есть две особенности.
Хранилище ACME тоже лежит в файловом хранилище — при S3 оно общее.
Блокировка, не дающая нескольким экземплярам одновременно запрашивать сертификат, работает через
Redis. Она включается только при CACHE_DRIVER=redis; при кэше в памяти блокировка локальная,
и экземпляры будут мешать друг другу.
Подтверждение по http-01 требует, чтобы запрос центра сертификации попал на тот же экземпляр,
который начал выпуск. Проще использовать dns-01 либо завершать TLS на балансировщике.
См. HTTPS и сертификаты.
Счётчики попыток входа живут в кэше. При CACHE_DRIVER=redis они общие, при memory — свои
у каждого экземпляра, то есть фактический предел умножается на число экземпляров.
Журнал аудита пишется в вывод каждого экземпляра отдельно. Сводить их нужно внешним сборщиком журналов, см. Безопасность.
Задачи плагинов дедуплицируются между экземплярами распределёнными блокировками, поэтому периодическая задача плагина выполнится один раз, а не на каждом экземпляре.
Состояние плагинов синхронизируется. Желаемое состояние — таблица plugins, и каждый
экземпляр приводит себя в соответствие с ней: установка, обновление, удаление, изменение прав или
перезагрузка плагина на одном экземпляре объявляется остальным через общий обмен событиями
(сообщение gameap:plugin:sync) и подхватывается сразу; периодический проход раз в
PLUGINS_SYNC_REFRESH_INTERVAL (по умолчанию 60 секунд) — страховка на случай потерянного
сообщения. Перезагрузка увеличивает счётчик generation в записи, поэтому кнопка
«Перезагрузить», нажатая на одном экземпляре, перезапускает плагин везде.
Плагин, файла которого на экземпляре нет, скачивается из каталога заново и сверяется с
сохранённой контрольной суммой. Плагин, установленный из локального файла, так не восстановить —
другим экземплярам негде взять файл, — поэтому таким плагинам нужно общее файловое хранилище
(FILES_DRIVER=s3). Плагин, который не удалось загрузить на экземпляре, повторно загружается там
с задержкой, растущей от PLUGINS_SYNC_MIN_BACKOFF (15 секунд) до PLUGINS_SYNC_MAX_BACKOFF
(15 минут); плагин, отключённый средой выполнения, перезапускается по правилам
PLUGINS_RECOVERY_*. PLUGINS_SYNC_DISABLED=true отключает синхронизацию — тогда изменения
доходят до экземпляра только при его перезапуске. См. Справочник config.env.
Локальными для экземпляра остаются две вещи:
- SSH-соединения, открытые плагинами через хост-библиотеку
gameap-ssh, живут в памяти открывшего их экземпляра и другим не видны (PLUGINS_SSH_ENABLEDпо умолчаниюfalse). - Кэш среды выполнения плагинов — скомпилированные модули WebAssembly — по умолчанию
держится в памяти; если задан
PLUGINS_RUNTIME_CACHE_DIR, это локальный путь на данном экземпляре, а не общее состояние. У каждого экземпляра должен быть свой каталог.
После запуска убедитесь, что:
- демон, зарегистрированный через один экземпляр, управляется и через другой;
- вход, выполненный на одном экземпляре, действует на остальных;
- файловый менеджер открывается независимо от того, на какой экземпляр попал запрос;
- плагин, установленный через один экземпляр, загружен и на остальных.
Если что-то из этого не работает, почти всегда причина в том, что один из четырёх общих компонентов остался локальным.