MTA:SA: лагает при большом онлайне
Вечером, когда онлайн набирается до полусотни и больше, начинается: игроков телепортирует, машины дёргаются, команды выполняются с задержкой в секунду-две, а на 10-15 игроках всё было идеально. Первый инстинкт — заказать сервер помощнее, но в девяти случаях из десяти в MTA:SA дело не в нехватке CPU, а в конкретном Lua-коде, который был написан и протестирован на маленьком онлайне и не масштабируется на большой. Разберём, где искать виновника, какие паттерны сажают производительность именно с ростом числа игроков и что делать прямо сейчас, пока код не починен.
Содержание
- Почему сервер лагает именно из-за Lua, а не только из-за онлайна
- Таймеры, которые никто не убивает
- Как найти виновный ресурс: ручной профайлинг и bisect
- Паттерны, которые сажают производительность с ростом онлайна
- Блокирующие запросы к базе — отдельный источник лагов при росте онлайна
- Сколько CPU и RAM закладывать под растущий онлайн
Почему сервер лагает именно из-за 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 при повторном создании и в слишком коротком интервале там, где секундная задержка ничего не испортит.