Несколько игровых серверов на одной машине — как разделить ресурсы
Рано или поздно у любого админа, который держит один сервер, появляется мысль: а зачем платить за вторую машину, если на этой ещё половина ресурсов простаивает? Ставишь 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 сработает локально, не тронув соседние сервисы). Ставьте чуть выше-XmxJVM, чтобы оставить место под 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.
Два практичных решения:
- Разнести расписание бэкапов по времени — самое дешёвое и эффективное. Если у вас несколько серверов, не ставьте им бэкап на одну и ту же минуту (стандартная ошибка — все на
0 4 * * *в cron). Разведите задачи на 15-20 минут друг от друга. - Ограничить 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.