MAATRIX GAMES / Блог / MTA:SA: лагает при большом онлайне

MTA:SA: лагает при большом онлайне

MAATRIX GAMES

Вечером, когда онлайн набирается до полусотни и больше, начинается: игроков телепортирует, машины дёргаются, команды выполняются с задержкой в секунду-две, а на 10-15 игроках всё было идеально. Первый инстинкт — заказать сервер помощнее, но в девяти случаях из десяти в MTA:SA дело не в нехватке CPU, а в конкретном Lua-коде, который был написан и протестирован на маленьком онлайне и не масштабируется на большой. Разберём, где искать виновника, какие паттерны сажают производительность именно с ростом числа игроков и что делать прямо сейчас, пока код не починен.

Почему сервер лагает именно из-за Lua, а не только из-за онлайна

Сервер MTA:SA исполняет весь server-side Lua-код всех ресурсов в одном общем окружении, последовательно, тик за тиком. Никакого распараллеливания логики между ядрами процессора здесь нет — это принципиально другая модель, чем у движков с многопоточным ядром. Если один хук на 5 игроках отрабатывает за доли миллисекунды, а на 60 игроках тот же хук выполняет O(n) или, того хуже, O(n²) работу, потому что внутри цикл по всем игрокам вложен в другой цикл по всем игрокам — именно рост онлайна и превращает безобидный код в реальную проблему. Дело не в том, что серверу "не хватило мощности", а в том, что код, который писался и тестировался вами или автором RP-сборки на 10-15 подключениях, никогда не нагружался так, как нагружается на пиковый вечер.

Отсюда практический вывод: прежде чем менять тариф, стоит убедиться, что проблема не лечится правкой конкретного обработчика. Если у вас ещё не настроена базовая часть сервера — конфиг, порты, автозапуск — вернитесь сначала к статье про установку MTA:SA и настройку Lua-скриптов и про запуск сервера через systemd, а здесь сосредоточимся на том, что происходит внутри resources/, когда online действительно большой.

Таймеры, которые никто не убивает

Самая частая причина деградации именно с ростом онлайна — это setTimer, который создаётся заново при каждом событии, но никогда не останавливается через killTimer. Работает это незаметно на 10 игроках (десяток лишних активных таймеров погоды не делает) и превращается в проблему на 60, потому что таймеров становится в разы больше, а каждый исполняет свой код на каждом цикле.

Плохо — таймер создаётся при каждом логине, а старый не убивается при повторном логине или дисконнекте:

addEventHandler("onPlayerLogin", root, function()
    setTimer(function()
        updatePlayerHUD(source)
    end, 1000, 0) -- бесконечный таймер, и при каждом релогине плодится новый
end)

Хорошо — храним handle таймера на игрока и чистим его и при повторном создании, и при выходе:

local hudTimers = {}

addEventHandler("onPlayerLogin", root, function()
    if isTimer(hudTimers[source]) then
        killTimer(hudTimers[source])
    end
    hudTimers[source] = setTimer(function()
        if isElement(source) then
            updatePlayerHUD(source)
        end
    end, 1000, 0)
end)

addEventHandler("onPlayerQuit", root, function()
    if isTimer(hudTimers[source]) then
        killTimer(hudTimers[source])
        hudTimers[source] = nil
    end
end)

Проверить свои подозрения просто: заведите в подозрительном ресурсе счётчик созданных и убитых таймеров и раз в минуту пишите разницу через outputDebugString. Если счётчик активных таймеров растёт линейно вместе с онлайном за смену, а не держится на разумном плато — вот он, виновник.

Поднять сервер MTA:SA за пару минут

Готовый образ MAATRIX GAMES: NVMe, AMD EPYC, DDoS-защита, панель управления. Локации UK, US, RU. Оплата картой РФ, СБП или криптой.

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

Как найти виновный ресурс: ручной профайлинг и bisect

Готового профайлера с отчётом по функциям, как в некоторых других играх на Lua, в стандартной поставке MTA:SA нет — но встроенная функция getTickCount() (миллисекунды с момента запуска сервера) отлично закрывает задачу ручного замера конкретного участка кода:

local t0 = getTickCount()
heavyCalculation()
local dt = getTickCount() - t0
if dt > 5 then
    outputDebugString(("heavyCalculation() took %dms"):format(dt), 2)
end

Обкладывайте таким замером подозрительные обработчики (события, которые срабатывают часто и завязаны на количество игроков или объектов на карте) и смотрите вывод в консоли сервера или в логе — outputDebugString с уровнем 2-3 виден и там, и там. Уровень отладки клиентских ошибок переключается отдельно командой debugscript 3 в игровой консоли, но для серверных лагов это не поможет — там нужен именно серверный лог.

Если замеры по коду не дают чёткой картины (например, гейммод чужой и большой), работает старый добрый bisect: останавливаете половину не критичных для геймплея ресурсов командой stop имя-ресурса, смотрите загрузку CPU процесса сервера в top/htop на хосте и поведение сервера под тем же онлайном. Если стало легче — виновник в отключённой половине, дальше делите пополам. Долго, но результат гарантированный, и не нужно гадать по логам, которые чужой ресурс, возможно, вообще не пишет.

Паттерны, которые сажают производительность с ростом онлайна

  • getElementsByType("player") внутри цикла по другим сущностям. Если у вас NPC-подобная логика или система событий, которая для каждой машины/зоны/точки обходит всех игроков — при 10 игроках это дёшево, при 60 это уже заметная O(n×m) работа на каждый вызов. Кешируйте список игроков один раз за тик/таймер, а не запрашивайте заново на каждой итерации внешнего цикла.
  • Широковещательные triggerClientEvent(root, ...) и outputChatBox без адресата. Отправка события или сообщения в чат всем сразу, когда на самом деле оно нужно 1-2 игрокам (например, уведомление о завершении личного квеста), одинаково дешева на маленьком онлайне и одинаково дорога — в сетевом и Lua-смысле — на большом. Меняйте root на конкретного player или таблицу нужных игроков.
  • Тяжёлая логика в часто вызываемых событиях боя. onPlayerWasted, onVehicleExplode, обработчики урона — на RP или DayZ-style серверах с массовыми замесами эти события срабатывают пачками именно тогда, когда онлайн максимальный. Если внутри них идут SQL-запросы, сложные проверки инвентаря или сериализация — это ровно тот момент, когда сервер лагает у всех разом.
  • Полинг вместо событий. Таймер, который каждые 100-200 мс проверяет состояние всех игроков (позиция, здоровье, инвентарь), когда можно было бы подписаться на конкретное событие (onPlayerDamage, onElementDataChange) — классический источник лишней постоянной нагрузки, которая растёт линейно с онлайном, даже если сама по себе логика не выглядит тяжёлой.

Быстрые временные меры, пока код не поправлен:

МераКакЭффект
Увеличить интервал некритичных таймеровsetTimer с 100-200 мс поднять до 500-1000 мссразу меньше суммарных вызовов в секунду
Снизить maxplayers на пиковые часы<maxplayers> в mtaserver.confменьше n в O(n)-обработчиках
Временно отключить подозрительный ресурсstop имя-ресурса в консолипроверка гипотезы без даунтайма всего сервера
Адресные события вместо широковещательныхtriggerClientEvent(player, ...) вместо rootменьше сетевого и Lua-оверхеда

Блокирующие запросы к базе — отдельный источник лагов при росте онлайна

Встроенные функции работы с БД в MTA:SA (dbConnect, dbQuery, dbExec) выполняются асинхронно, если вы используете их правильно — с колбэком, а не в стиле "запросил и тут же жду результат" внутри частого обработчика. Правильный паттерн:

local db = dbConnect("mysql", "dbname=mta_rp;host=127.0.0.1", "user", "pass")

dbQuery(function(qh)
    local result = dbPoll(qh, 0)
    if result and result[1] then
        givePlayerMoney(source, result[1].balance)
    end
end, db, "SELECT balance FROM accounts WHERE name=?", getPlayerName(source))

Проблема обычно не в самой функции, а в том, где и как часто она вызывается: если запрос к базе стоит внутри события, которое срабатывает на каждого игрока при каждом действии (подбор предмета, каждый выстрел, каждое перемещение), то при 10 игроках это 10 нечастых запросов, а при 60 — уже поток, который может упираться в лимиты самой MySQL-инсталляции, а не только в Lua. Общие принципы настройки индексов и пула соединений — в статье про MySQL для игровых плагинов, там же разбираются грабли, которые не специфичны для MTA, но бьют по любому серверу с базой под нагрузкой.

Сколько CPU и RAM закладывать под растущий онлайн

Раз server-side Lua в MTA:SA не параллелится между ядрами, для сервера важнее частота (clock speed) одного ядра, чем их количество — дополнительные ядра нужны в первую очередь операционной системе, сетевому стеку и, если крутите MySQL на той же машине, отдельному процессу базы. Это не значит, что многоядерность бесполезна — просто наращивание количества ядер само по себе не разгонит один тяжёлый Lua-хук, который душит одно ядро.

Ниже — ориентировочная раскладка, не точный бенчмарк: реальные цифры сильно зависят от того, насколько тяжёлый у вас гейммод и сколько сторонних ресурсов запущено одновременно.

Пиковый онлайнТипичная сборкаОриентир по железу
до 20лёгкий DM/freeroam, минимум плагиноводно быстрое ядро, RAM от 1-2 ГБ хватает почти всегда
20-60средний RP-гейммод + MySQLнужна высокая частота ядра плюс запас в 2+ ядра под ОС/базу, RAM 2-4 ГБ
60+тяжёлый RP/DayZ-style, много ресурсов Communityявный запас по частоте и памяти, RAM 4-8 ГБ — но профилирование важнее апгрейда

За более развёрнутым разбором того, сколько ядер реально нужно под стабильную работу игрового сервера и как соотносится частота с количеством ядер, — отдельная статья про выбор количества ядер CPU; там принципы общие для игр на Lua/Source-подобных движках, а не только для MTA.

Поднять сервер MTA:SA за пару минут

Готовый образ MAATRIX GAMES: NVMe, AMD EPYC, DDoS-защита, панель управления. Локации UK, US, RU. Оплата картой РФ, СБП или криптой.

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

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

Поможет ли просто взять тариф помощнее, если лагает при большом онлайне?

Часто нет, если причина в конкретном Lua-хуке — один тяжёлый обработчик всё равно упрётся в возможности одного ядра быстрее, чем вы почувствуете разницу от более мощного железа. Сначала профилируйте через getTickCount() и bisect, потом уже думайте про апгрейд.

Как понять, что дело в Lua, а не в сети?

Сравните жалобы игроков: если у всех высокий пинг и рывки именно в движении — вероятнее сеть или маршрут до хоста. Если пинг у игроков в норме, а рывки и задержка команд появляются волнами, коррелирующими с событиями (массовый бой, вайп, спавн волны NPC) — почти всегда виноват конкретный обработчик.

Можно ли распараллелить тяжёлые ресурсы MTA по ядрам процессора?

Нет, стандартный сервер MTA:SA исполняет весь server-side Lua в одном потоке. Разносить логику по ядрам самостоятельно нельзя — единственный рабочий путь — оптимизировать код и убирать лишнюю работу из часто вызываемых обработчиков.

Что делать, если тормозит чужой RP-гейммод, а не свой код?

Локализуйте виновника через bisect (stop половины ресурсов по очереди), проверьте, обновлялся ли гейммод под актуальную версию сервера, и посмотрите issues на GitHub проекта — часто проблема уже известна и есть патч или форк без неё.

Нужно ли убирать таймеры совсем и переходить только на события?

Не всегда — таймеры нормальны для периодических задач (автосохранение, проверка АФК). Проблема не в самом setTimer, а в отсутствии killTimer при повторном создании и в слишком коротком интервале там, где секундная задержка ничего не испортит.