MAATRIX GAMES / Блог / Несколько игровых серверов на одной машине — как разделить ресурсы

Несколько игровых серверов на одной машине — как разделить ресурсы

MAATRIX GAMES

Рано или поздно у любого админа, который держит один сервер, появляется мысль: а зачем платить за вторую машину, если на этой ещё половина ресурсов простаивает? Ставишь Rust-сервер для друзей рядом с основным Minecraft — и через неделю оба начинают лагать одновременно, хотя по отдельности каждый летал. Дальше разберём, как без магии и без переплаты развести несколько игровых процессов на одном хосте так, чтобы они друг другу не мешали.

Почему один процесс топит другой

Игровые серверы почти никогда не упираются в диск — они упираются в CPU и RAM, причём неравномерно. Minecraft на Paper может часами держать 5% ядра, а потом на генерации нового чанка или при массовом взрыве TNT выжрать целое ядро на секунду-две. Rust в момент респавна ресурсов по карте или при полной синхронизации нового игрока грузит CPU скачком. Если два таких процесса на одной машине не разграничены, их пики просто складываются — и оба получают тормоза одновременно, хотя по отдельности каждый спокойно пережил бы свой всплеск.

Второй источник проблем — память. Java-процесс Minecraft со временем разрастается до выделенного -Xmx, и если вы не задали жёсткий потолок, JVM будет жадно забирать всё, что видит свободным в системе. Без изоляции второй сервер в момент своего пика может обнаружить, что физической RAM просто не осталось, и уйти в swap — это гораздо хуже, чем просадка FPS, это уже фризы на секунды.

Разделение ресурсов решает обе проблемы: каждый процесс получает свою гарантированную долю CPU и жёсткий потолок памяти, и что бы ни творилось у соседа, вашему серверу это не грозит — в пределах его собственного лимита, конечно.

Планирование: сколько серверов реально влезет

Прежде чем городить cgroups, посчитайте на бумаге. Возьмите суммарный -Xmx (или аналог для не-Java игр) всех серверов, которые хотите запустить, добавьте 15-20% на систему и служебные процессы (панель управления, SSH, cron-бэкапы) — это и есть минимальная RAM машины. По CPU логика проще: если сервера будут активны одновременно (а не по очереди), считайте, что каждому нужно минимум одно физическое ядро в пике, иначе оба начнут делить время планировщика и оба почувствуют лаг.

Пример расчёта для VPS с 4 ядрами и 16 ГБ RAM:

СерверRAM (лимит)CPU (лимит)Пиковая нагрузка
Minecraft (Paper, 20 игроков)6 ГБ1.5 ядрагенерация чанков, редстоун
CS2 (competitive, 10 игроков)2 ГБ1 ядротикрейт 128, спавн раунда
Rust (small map, 30 слотов)6 ГБ1.5 ядравайп, ресурс-респавн
Система + панель1.5 ГБ0.5-1 ядробэкапы, мониторинг

Это уже 15.5 ГБ из 16 — впритык, но рабочий вариант, если сервера не топовые по онлайну. Если хотя бы один из трёх регулярно упирается в лимит по логам — на этой машине ему тесно, и лучше вынести его отдельно, а не резать лимиты соседям.

Если вы ещё выбираете между несколькими небольшими серверами на одной машине и одним мощным выделенным сервером под конкретный проект — сравнение подходов есть в статье про выбор между выделенным сервером и VPS под крупный модпак.

Нужен игровой сервер?

MAATRIX GAMES — 45+ игр, деплой за минуты, DDoS-защита, автобэкапы. Тарифы от 399 ₽/мес.

Создать сервер

Разные порты для каждого сервера

Базовое разделение — сетевое. Каждый игровой процесс слушает свой порт, и тут важно не наступить на грабли с портами по умолчанию, которые одинаковы у всех игр этого типа.

Типичная раскладка для нескольких серверов на одном IP:

Minecraft #1:      25565/tcp, 25565/udp (query)
Minecraft #2:      25566/tcp, 25566/udp
CS2:                27015/tcp, 27015/udp
Rust:               28015/udp (game), 28016/udp (rcon), 28017/udp (app)
ARK: Survival:      7777/udp, 7778/udp (raw), 27020/udp (query)

Правило простое: если запускаете два сервера одной игры, у второго нужно сдвинуть все используемые порты, а не только основной — иначе получите конфликт Address already in use на query- или rcon-порту, который не сразу очевиден в логах. Настройки портов, query-протокола и файрвола под каждую игру подробно разобраны в статье про настройку портов и файрвола для игрового сервера — там же про то, какие порты можно закрыть снаружи, оставив только rcon для админки.

Если серверов становится много и вручную открывать десяток портов в файрволе неудобно, посмотрите в сторону обратного прокси перед несколькими Minecraft-инстансами: BungeeCord/Velocity показывают наружу один порт 25565, а внутри разводят трафик по бэкендам, каждый на своём внутреннем порту.

cgroups: жёсткие лимиты CPU и RAM

Разные порты решают только сетевой конфликт. Чтобы один процесс физически не мог отожрать ресурсы у другого, нужны control groups (cgroups) — механизм ядра Linux, которым сейчас управляет systemd. На современных дистрибутивах (Ubuntu 22.04+, Debian 12+) действует cgroups v2, и проще всего работать с ним через systemd-юниты, а не вручную через /sys/fs/cgroup.

Если ваш игровой сервер уже запускается как systemd-сервис (а это правильный способ для автозапуска, с автоматическим рестартом после падения и после перезагрузки хоста), лимиты добавляются прямо в юнит-файл, без установки дополнительных пакетов.

Пример юнита для Minecraft-сервера с жёстким лимитом памяти и долей CPU:

# /etc/systemd/system/mc-server1.service
[Unit]
Description=Minecraft Server 1 (Paper)
After=network.target

[Service]
Type=simple
User=mcuser
WorkingDirectory=/opt/servers/mc1
ExecStart=/usr/bin/java -Xms4G -Xmx6G -jar paper.jar --nogui
Restart=on-failure

# --- Лимиты cgroups v2 ---
MemoryMax=6500M
MemoryHigh=6000M
CPUQuota=150%
CPUWeight=100

[Install]
WantedBy=multi-user.target

Разберём ключевые параметры:

  • MemoryMax — жёсткий потолок. Если процесс попытается выйти за него, ядро начнёт убивать самые прожорливые задачи внутри cgroup (OOM killer сработает локально, не тронув соседние сервисы). Ставьте чуть выше -Xmx JVM, чтобы оставить место под off-heap память — обычно +300-500 МБ достаточно.
  • MemoryHigh — мягкий порог: при превышении ядро начинает throttling, притормаживая процесс, не убивая его сразу. Хорошая страховка перед жёстким MemoryMax.
  • CPUQuota — доля CPU в процентах от одного ядра. 150% значит "полтора ядра максимум", даже если остальные ядра свободны — это ограничивает пиковое потребление, чтобы сервер не мог временно съесть все ядра машины.
  • CPUWeight — относительный приоритет при конкуренции за CPU (по умолчанию 100, диапазон 1-10000). Если хотите, чтобы приоритетный сервер получал больше времени CPU при одновременной нагрузке, поднимите его вес, а у второстепенного — понизьте.

После правки юнита:

sudo systemctl daemon-reload
sudo systemctl restart mc-server1

Проверить, что лимиты реально применились, можно через:

systemctl show mc-server1 -p MemoryMax -p CPUQuotaPerSecUSec
cat /sys/fs/cgroup/system.slice/mc-server1.service/memory.max
cat /sys/fs/cgroup/system.slice/mc-server1.service/cpu.max

Для не-systemd запусков (например, через screen/tmux напрямую) можно создать cgroup вручную через systemd-run:

sudo systemd-run --unit=rust-server --scope \
  -p MemoryMax=6500M -p CPUQuota=150% \
  /opt/servers/rust/RustDedicated -batchmode +server.port 28015

Это удобно для быстрого теста лимитов без правки постоянных юнит-файлов.

Изоляция диска и I/O

CPU и RAM — не всё. Если один сервер делает автобэкап мира (архивирует несколько гигабайт), а в этот момент другой пишет свои сейвы, оба упрутся в диск, особенно на HDD или на VPS с shared-storage без выделенного IOPS. Симптом — внезапные фризы записи, которые не видны ни в top, ни в CPU-графиках, только в iostat.

Два практичных решения:

  1. Разнести расписание бэкапов по времени — самое дешёвое и эффективное. Если у вас несколько серверов, не ставьте им бэкап на одну и ту же минуту (стандартная ошибка — все на 0 4 * * * в cron). Разведите задачи на 15-20 минут друг от друга.
  2. Ограничить I/O через cgroups, если диск всё равно узкое место:
IOWeight=50
IOReadBandwidthMax=/dev/sda 50M
IOWriteBandwidthMax=/dev/sda 50M

Это работает только если файловая система на блочном устройстве, а не на сетевом сторадже с уже урезанной пропускной способностью — на некоторых VPS-тарифах диск и так виртуализирован, и на уровень ядра гостя эти лимиты просто не попадут. В таком случае единственный рабочий рычаг — разнесение по времени.

Мониторинг: где реально упирается система

Настроить лимиты один раз недостаточно — нагрузка со временем растёт (больше игроков, больше построек, новые моды), и лимиты, которые были comfortable в момент установки, через полгода могут стать тесными. Смотреть нужно на три метрики по каждому cgroup отдельно, а не на систему в целом:

# Живой мониторинг всех systemd-юнитов с их cgroup-потреблением
systemd-cgtop

# Точечная проверка одного сервиса
systemctl status mc-server1
cat /sys/fs/cgroup/system.slice/mc-server1.service/memory.current
cat /sys/fs/cgroup/system.slice/mc-server1.service/cpu.stat

В выводе cpu.stat обратите внимание на nr_throttled и throttled_usec — если эти числа растут, значит CPUQuota реально режет процесс и, возможно, лимит пора поднять (или разгрузить машину, убрав/перенеся один из серверов). Именно throttling — самая частая причина фразы "сервер вроде не падает, но иногда подтормаживает без видимой причины".

Для игровой части (TPS, задержка тика) нужен отдельный мониторинг внутри самой игры — это не покрывается системными метриками cgroups, потому что cgroup видит только "сколько CPU съедено", а не "успевает ли игра обработать тик за 50 мс". Как это отслеживать конкретно для игровых серверов — в статье про мониторинг TPS и лагов на игровом сервере.

Когда разделение ресурсов уже не спасает

Честно: cgroups — это защита от "один сервер не убьёт другого", а не гарантия, что оба будут летать. Если суммарная пиковая нагрузка всех серверов регулярно упирается в физические лимиты машины (все ядра заняты, throttling виден в cpu.stat каждый день, а не только на вайпах), никакая тонкая настройка лимитов это не исправит — она просто честно покажет, кому не хватает ресурсов, вместо того чтобы позволить одному серверу тихо давить остальных.

Признаки, что пора выносить сервер на отдельную машину:

  • nr_throttled растёт даже вне пиковых событий (не только на вайпе/рестарте);
  • память стабильно упирается в MemoryHigh, а не только в моменты пиков;
  • игроки жалуются на лаги именно на одном конкретном сервере из набора, при этом остальные работают ровно;
  • машина работает без запаса даже ночью, когда онлайн минимальный.

Перенос сервера на отдельный хост без потери мира/прогресса — процедура несложная, но с нюансами (сохранить UUID мира, RCON-пароли, привязку доменов). Она разобрана в статье про миграцию сервера к другому хостеру без потери мира.

Нужен игровой сервер?

MAATRIX GAMES — 45+ игр, деплой за минуты, DDoS-защита, автобэкапы. Тарифы от 399 ₽/мес.

Создать сервер

Частые вопросы

Можно ли разделить ресурсы без systemd, просто через Docker?

Да, и это даже проще: у каждого контейнера свои флаги --memory и --cpus, а порты пробрасываются явно через -p. Логика лимитов та же самая (Docker тоже использует cgroups под капотом), просто интерфейс настройки другой. Если вы уже развёртываете сервера в контейнерах — используйте --memory=6g --cpus=1.5 вместо системного юнита.

Что будет, если превысить MemoryMax?

Ядро убьёт процесс внутри этой cgroup через OOM killer, не трогая соседние сервисы на машине. Это звучит жёстко, но именно так и должна работать изоляция — лучше упасть предсказуемо и перезапуститься по Restart=on-failure, чем утащить в OOM всю систему целиком.

Нужно ли резервировать 100% CPU под каждый сервер, если они не активны одновременно?

Нет. Если пики нагрузки у серверов не совпадают по времени (например, один популярен днём, другой — вечером), можно смело давать им CPUQuota с перекрытием суммарно больше 100% физических ядер — в моменте они всё равно не будут конкурировать. Но если оба активны в прайм-тайм одновременно — считайте лимиты консервативно, без overcommit.

Как понять, что машине уже не хватает ресурсов, а не просто лимиты выставлены неверно?

Поднимите лимиты временно (или снимите вовсе) и посмотрите, продолжает ли падать TPS/FPS у игроков. Если да — упирается не cgroup, а сама машина; если нет — значит лимит действительно был занижен и его стоит скорректировать постоянно.

Стоит ли ставить лимиты, если сервер на машине всего один?

В целом не обязательно, но полезно как страховка от утечек памяти в плагинах/модах — жёсткий MemoryMax не даст одному кривому плагину положить всю систему через swap.