Балансировка нагрузки между несколькими игровыми серверами
Проект растёт, онлайн на одном сервере упирается в потолок — TPS проседает, RCON отвечает с задержкой, новые игроки видят лаги в первые же минуты и уходят. Первая мысль обычно "проапгрейдить тариф", но у железа есть предел, и рано или поздно один процесс перестаёт вывозить весь онлайн физически. Разберём, как вместо одного раздутого сервера поднять несколько инстансов и грамотно раскидать по ним игроков — для Minecraft это делается через прокси-балансировку, а для шутеров вроде CS2 логика совсем другая.
Содержание
Когда одного сервера уже не хватает
Первый сигнал — не общая загрузка CPU, а конкретные метрики игры. Для Minecraft это TPS (тики в секунду): пока держится в районе 20, всё в порядке, просадка до 15 и ниже при пиковом онлайне — уже повод действовать, а не откладывать. Для CS2 и других Source-игр — тикрейт сервера и время отклика на RCON-команды. Если не уверены, как читать эти цифры — у нас есть отдельный разбор низкого TPS и лагов на Minecraft-сервере.
Важно понимать, откуда берётся потолок. Один процесс Minecraft-сервера (vanilla, Spigot, Paper) практически не умеет использовать больше одного-двух ядер CPU эффективно — генерация тиков в основном однопоточная, сколько бы ядер вы ни докупили. Поэтому вертикальное масштабирование (просто взять тариф пожирнее) работает только до определённого момента, а дальше выгоднее и надёжнее не растить один сервер, а поднять второй такой же рядом и развести игроков между ними.
Это же справедливо и для CS2: один процесс dedicated-сервера обслуживает один матч на фиксированное число слотов (обычно 10-64 в зависимости от режима), и "больше онлайна" здесь означает не более мощный сервер, а больше параллельных инстансов под разные матчи одновременно.
Что вообще значит "балансировка нагрузки" в играх
В классическом вебе балансировщик (nginx, HAProxy) стоит перед несколькими одинаковыми бэкендами и раскидывает запросы, не заботясь о состоянии — любой бэкенд обработает любой запрос одинаково. С игровыми серверами так не выйдет: игрок не запрос, он находится "внутри" конкретного процесса — в конкретном мире, с конкретным инвентарём и позицией. Поэтому балансировка нагрузки в играх — это не распределение отдельных запросов, а распределение самих игроков (сессий) между несколькими независимыми инстансами сервера так, чтобы онлайн был примерно поровну и ни один процесс не захлёбывался.
Для Minecraft это решается на уровне протокола: прокси-сервер (BungeeCord или Velocity) стоит перед несколькими бэкендами и на этапе подключения выбирает, к какому инстансу отправить игрока — причём сделать это можно как статично (фиксированный сервер по умолчанию), так и динамически, реально сверяясь с текущим онлайном каждого бэкенда. Про сам механизм прокси и его настройку с нуля мы подробно разбирали в статье про BungeeCord и Velocity: сеть Minecraft-серверов — если прокси у вас ещё не поднят, начните оттуда, здесь мы сосредоточимся именно на балансировке нагрузки поверх уже работающей сети.
Для игр без такого прокси-протокола (Source-шутеры, большинство survival-проектов) единого стандартного механизма нет вообще — балансировка там реализуется либо на уровне инфраструктуры (сколько инстансов запущено и как игроки о них узнают), либо игрой не поддерживается в принципе, и об этом ниже отдельно.
Нужен игровой сервер?
MAATRIX GAMES — 45+ игр, деплой за минуты, DDoS-защита, автобэкапы. Тарифы от 399 ₽/мес.
Создать серверБалансировка через прокси в Minecraft: Velocity и BungeeCord
По умолчанию ни Velocity, ни BungeeCord не балансируют нагрузку "из коробки" — они делают только маршрутизацию: список try (Velocity) или priorities (BungeeCord) задаёт порядок серверов, и прокси подключает игрока к первому доступному серверу из списка. Это защита от простоя (failover), а не равномерное распределение — если в списке первым стоит lobby-1, туда попадут все новые игроки, пока сервер не откажет или не заполнится по лимиту max_players.
Реальная балансировка по онлайну требует логики поверх этого механизма. Рабочий вариант — небольшой плагин на стороне прокси, который перехватывает событие подключения игрока и сам выбирает наименее загруженный сервер из пула. На Velocity это делается через официальный Plugin API и событие ServerPreConnectEvent:
@Subscribe
public void onServerPreConnect(ServerPreConnectEvent event) {
List<RegisteredServer> pool = List.of(
proxy.getServer("survival-1").orElseThrow(),
proxy.getServer("survival-2").orElseThrow(),
proxy.getServer("survival-3").orElseThrow()
);
RegisteredServer leastLoaded = pool.stream()
.min(Comparator.comparingInt(s -> s.getPlayersConnected().size()))
.orElseThrow();
event.setResult(ServerPreConnectEvent.ServerResult.allowed(leastLoaded));
}
Логика простая: на каждое подключение прокси сравнивает текущий онлайн на всех инстансах пула и отправляет игрока туда, где сейчас свободнее. Для BungeeCord тот же подход реализуется через его собственный Plugin API и событие ServerConnectEvent, идея не меняется. Писать такой плагин с нуля не обязательно ради одной сети из двух-трёх серверов — но если у вас уже есть штатный разработчик под проект или вы работаете с готовыми решениями с Hangar (для Velocity) или SpigotMC (для BungeeCord), стоит поискать готовый балансировщик именно под вашу версию прокси, прежде чем писать своё — экосистема Velocity моложе и таких плагинов там заметно меньше, чем под BungeeCord.
Пример: несколько инстансов выживания за одним входом
Практическая схема для растущего выживания — не один сервер survival, а несколько одинаковых по конфигу инстансов survival-1, survival-2 и так далее, каждый на своём порту (и, если позволяет тариф, на отдельной машине). В velocity.toml они прописываются как обычные бэкенды:
[servers]
lobby = "127.0.0.1:25566"
survival-1 = "127.0.0.1:25567"
survival-2 = "127.0.0.1:25568"
survival-3 = "127.0.0.1:25569"
try = ["lobby"]
Игрок сначала попадает в lobby — лёгкий сервер без построек и мобов, который почти не грузит железо и держит стабильный онлайн даже в пиках. Дальше плагин-балансировщик (или простое меню с NPC/табличками "Выживание 1 / 2 / 3", где на табличке выводится текущий онлайн через тот же RegisteredServer.getPlayersConnected()) решает, куда отправить игрока — автоматически или по его собственному выбору, что для многих проектов даже честнее, чем скрытая балансировка: игроки видят, что "Выживание 1" уже забито, и сами идут во второй мир.
Важный нюанс, о котором часто забывают: каждый survival-N — это отдельный мир с собственным инвентарём, экономикой и правами, если их специально не синхронизировать через общую MySQL-базу и LuckPerms, как описано в статье про сеть на BungeeCord/Velocity. Без синхронизации у вас по сути три разных сервера под одной вывеской, а не один большой мир — это стоит явно объяснить игрокам в правилах, иначе вопросы "куда делся мой инвентарь" гарантированы.
Шутеры и другие игры: другая модель балансировки
У Source-игр (CS2, Team Fortress 2, Left 4 Dead 2) нет прокси-протокола, аналогичного Minecraft-сети — игрок подключается напрямую к конкретному IP:порту конкретного инстанса, и "перекинуть" его на другой сервер без разрыва подключения нельзя в принципе. Балансировка здесь решается не маршрутизацией на уровне протокола, а организацией самого пула серверов:
- Несколько независимых инстансов под разные режимы/карты. Вместо одного CS2-сервера на 64 слота с постоянной перегрузкой — два-три сервера по 10-20 слотов под разные режимы (Competitive, Wingman, Deathmatch) или карты. Игроки распределяются сами, просто выбирая нужный сервер из списка избранного или community-браузера.
- Публичный статус онлайна. SourceMod-плагины (или простой RCON-опрос
statusпо крону) могут выгружать текущий онлайн каждого инстанса на сайт проекта или в Discord-бота — это подсказывает игрокам, куда идти, без скрытой магии на уровне сети. - Ротация карт и очередь. Если несколько инстансов под одну и ту же карту/режим регулярно упираются в лимит одновременно, это сигнал не балансировать хитрее, а честно добавить ещё один инстанс — благо для Source-серверов это дёшево по ресурсам (CS2 требует заметно меньше RAM на инстанс, чем полноценный Minecraft-мир).
Отдельный случай — ARK: Survival Evolved, у которой кластеризация встроена официально: несколько серверов с одинаковым -ClusterID и общим -ClusterDirOverride позволяют игрокам переносить персонажа и инвентарь между картами через обелиски или трансферы, оставаясь при этом полностью раздельными процессами по нагрузке. Это не "балансировка" в смысле равномерного онлайна, а управляемое распределение игроков по разным картам одного мира — концептуально ближе к нескольким survival-N из Minecraft-примера выше, чем к вебовому балансировщику.
Мониторинг, авто-масштабирование и типичные грабли
Прежде чем добавлять инстансы, нужно понимать, когда именно это делать — на глаз по жалобам игроков решение принимается слишком поздно. Практичный минимум — дашборд с TPS/тикрейтом, онлайном и загрузкой CPU по каждому инстансу; разворачивать его вручную не обязательно, у нас есть отдельная инструкция про Grafana и Prometheus для игрового хостинга. Понятное правило для команды: если инстанс стабильно держит онлайн выше 80% от комфортного максимума в течение нескольких дней подряд — пора готовить следующий, а не ждать, пока текущий начнёт лагать в прайм-тайм.
Про "авто-масштабирование" стоит быть честным: полностью автоматическое поднятие нового инстанса по нагрузке (как в вебе с Kubernetes HPA) для игровых серверов почти никогда не оправдано — новый мир Minecraft или новый матч CS2 нельзя поднять за секунды прозрачно для игроков. Реалистичный процесс — полуручной: мониторинг сигналит о приближении к порогу, админ поднимает новый инстанс на дополнительном тарифе, добавляет его в конфиг прокси и анонсирует игрокам. Если несколько инстансов уже крутятся на одной машине — см. распределение ресурсов между несколькими игровыми серверами на одной машине, чтобы новый инстанс не отъедал RAM у соседей.
Частые грабли на практике: забытый лимит max_players на бэкенде (прокси балансирует, но конкретный Paper-сервер молча отклоняет подключения сверх своего локального лимита в server.properties); DNS/SRV-запись, которая у части старых клиентов указывает не на прокси, а напрямую на один из бэкендов в обход всей балансировки; и попытка балансировать stateful-игру (survival, RPG-сервер) так же топорно, как веб-трафик — без синхронизации данных между инстансами это создаёт больше путаницы, чем пользы.
Нужен игровой сервер?
MAATRIX GAMES — 45+ игр, деплой за минуты, DDoS-защита, автобэкапы. Тарифы от 399 ₽/мес.
Создать серверЧастые вопросы
Балансирует ли Velocity нагрузку сам, без дополнительного плагина?
Нет, из коробки он только маршрутизирует по списку try в порядке приоритета (failover), а не выбирает наименее загруженный сервер — для реального распределения по онлайну нужен плагин с логикой поверх ServerPreConnectEvent, как в примере выше.
Можно ли балансировать нагрузку между инстансами на разных физических серверах?
Да, и для растущих проектов это даже правильнее — прокси не обязан стоять на той же машине, что бэкенды, важно только держать низкую задержку между ними (в идеале один регион/дата-центр), иначе игроки почувствуют лишний пинг при переключении.
Что делать, если нет возможности писать свой плагин для Velocity/BungeeCord?
Начните с ручного варианта — несколько подписанных лобби-табличек или пунктов меню с онлайном каждого инстанса, куда игрок заходит сам. Это не автоматика, но закрывает саму задачу распределения нагрузки без единой строчки кода.
Работает ли балансировка через прокси для модовых серверов на Forge/Fabric?
Да, сам механизм прокси не зависит от того, что происходит внутри бэкенда — модовый сервер точно так же регистрируется как обычный бэкенд в velocity.toml/config.yml. Другое дело, что синхронизировать модовые данные между несколькими модовыми мирами обычно сложнее, чем ванильные плагины.
Есть ли смысл балансировать нагрузку, если у меня всего 20-30 игроков в пике?
Почти никогда. Балансировка через несколько инстансов имеет смысл, когда один нормально настроенный сервер физически не вывозит онлайн — для небольших и средних проектов до пары сотен одновременных игроков обычно хватает одного правильно оптимизированного инстанса, и первым шагом стоит проверить оптимизацию, а не сразу городить сеть.