Оптимизация сервера MTA:SA — снижение нагрузки и лагов
Оптимизация сервера MTA:SA начинается с поиска конкретного источника нагрузки. Высокое потребление процессора, задержки команд, рост памяти и зависания игроков часто вызываются не слабым VPS, а одним тяжёлым Lua-ресурсом, частыми таймерами, утечкой элементов, лишней синхронизацией или медленными запросами MySQL.
В руководстве разобраны диагностика MTA:SA Server 1.6, работа с performancebrowser, поиск тяжёлых ресурсов, оптимизация Lua-кода, клиентских интерфейсов, сетевого обмена и базы данных.
Установка и настройка ресурсов:
Работа с базой данных:
Признаки проблем с производительностью MTA Server
Серверная нагрузка
- команды выполняются с задержкой;
- игроки и транспорт периодически замирают;
- авторизация занимает несколько секунд;
- таймеры срабатывают позже заданного времени;
- ресурсы долго запускаются или перезапускаются;
- процесс MTA постоянно использует много CPU;
- оперативная память постепенно увеличивается;
- после перезапуска работа временно улучшается.
Клиентская нагрузка
- FPS падает только у игроков;
- проблема появляется в одной локации;
- интерфейс снижает FPS при открытии;
- просадка начинается после загрузки моделей;
- CEF продолжает потреблять ресурсы после закрытия;
- на слабых компьютерах проблема заметнее.
Сеть и MySQL
- повышается ping или появляются потери пакетов;
- лаги усиливаются при массовых событиях;
- вход в аккаунт и загрузка имущества задерживаются;
- в
db.logповторяются одинаковые запросы; - ресурс создаёт новое соединение перед каждой операцией;
- запросы выполняются без индексов и ограничений.
Создание исходных показателей
Перед оптимизацией зафиксируйте состояние сервера при одинаковой нагрузке.
- версия MTA Server;
- число запущенных ресурсов;
- количество игроков;
- использование CPU и памяти;
- объём сетевого трафика;
- количество игровых элементов;
- самые тяжёлые ресурсы;
- время основных SQL-запросов;
- размер базы и журналов.
Быстрые команды
sver
debuguptime
list
Сравнивайте показатели при одинаковом онлайне, наборе ресурсов и игровом сценарии. Тест с двумя игроками нельзя напрямую сравнивать с часом пик.
| Показатель | До изменения | После изменения |
|---|---|---|
| CPU | Записать | Записать |
| Память | Записать | Записать |
| Время авторизации | Записать | Записать |
| Время SQL-запроса | Записать | Записать |
| Пакеты в секунду | Записать | Записать |
| Количество элементов | Записать | Записать |
Как отличить серверные лаги от клиентских
- Низкий FPS при нормальном ping — чаще клиентский интерфейс, модели или графика.
- Высокий ping при нормальном FPS — сеть или чрезмерная отправка данных.
- Нормальные FPS и ping при задержке команд — Lua-код, база данных или серверный процесс.
- Проблема у всех одновременно — серверная или сетевая причина.
- Проблема только у одного игрока — сначала проверяется его клиент и соединение.
Проверка подозрительного ресурса
stop custom_hud
stop vehicle_system
Если после остановки интерфейса восстанавливается FPS, исследуйте клиентские скрипты. Если исчезает задержка игрового режима, проверяйте серверные таймеры, циклы и запросы ресурса.
Поиск нагрузки через performancebrowser
performancebrowser показывает потребление CPU, Lua-памяти, количество элементов и сетевые пакеты по ресурсам.
Запуск
start ajax
start performancebrowser
Адрес
http://IP_СЕРВЕРА:HTTP_ПОРТ/performancebrowser/
Пример локального адреса:
http://127.0.0.1:22005/performancebrowser/
Ограничение доступа через ACL
<group name="PerformanceDevelopers">
<acl name="PerformanceDeveloperRights" />
<object name="user.ServerDeveloper" />
</group>
<acl name="PerformanceDeveloperRights">
<right
name="resource.performancebrowser.http"
access="true"
/>
<right
name="resource.ajax.http"
access="true"
/>
</acl>
После ручного изменения:
reloadacl
Внутриигровой IPB
start ipb
После административной авторизации откройте монитор командой:
ipb
Какие показатели проверять
Lua timing
Показывает суммарное время выполнения Lua-кода и количество вызовов. Ищите ресурсы, которые значительно превышают остальные, а также функции с большим количеством вызовов.
Lua memory
Постоянный рост памяти у одного ресурса часто связан с таблицами вышедших игроков, повторным добавлением обработчиков, неубранными таймерами или кэшем без ограничения.
Element count
Проверяйте количество автомобилей, объектов, педов, маркеров, колшейпов, пикапов и временных элементов.
Event Packet usage
Ищите события, которые отправляются десятки раз в секунду, передают большие таблицы или рассылаются всем игрокам без необходимости.
Однократный всплеск при запуске ресурса может быть нормальным. Опаснее постоянная нагрузка или постепенный рост, который не возвращается к прежнему уровню.
Мониторинг Windows и Linux
Windows
В Диспетчере задач и Мониторе ресурсов проверьте процесс MTA Server: CPU, память, диск, сеть и время работы.
Linux
ps aux | grep mta-server
top
free -h
df -h
ip -s link
Размер журналов:
du -sh /home/mtasa/mta-server/mods/deathmatch/logs/
При установленном пакете системной статистики:
iostat -xz 1
Как найти тяжёлый ресурс
- Сравните ресурсы в Lua timing.
- Проверьте рост Lua memory.
- Посмотрите количество созданных элементов.
- Найдите события с большим числом пакетов.
- Сопоставьте нагрузку с последним обновлением.
- Проверьте зависимые ресурсы и экспорты.
- Повторите тест после временной остановки подозрительной системы.
stop suspected_resource
Если источник не очевиден, разделите дополнительные ресурсы на две группы и отключайте их поочерёдно на тестовой копии. Так быстрее найти проблемную половину сборки.
Измерение времени выполнения Lua-кода
Для локального измерения участка кода используйте getTickCount.
local startedAt = getTickCount()
loadAllServerVehicles()
local elapsed = getTickCount() - startedAt
outputDebugString(
"[PROFILE] loadAllServerVehicles: "
.. tostring(elapsed)
.. " ms",
elapsed > 100 and 2 or 3
)
Что стоит измерять
- загрузку аккаунта и персонажа;
- создание транспорта;
- сохранение имущества;
- циклы по игрокам и объектам;
- массовые SQL-операции;
- создание большой карты;
- формирование административных отчётов.
Частые события и циклы
Даже небольшая функция становится тяжёлой, если вызывается слишком часто.
Особого внимания требуют
onClientRender;onClientPreRender;onClientHUDRender;- короткие бесконечные таймеры;
- обработчики на
root; - циклы по всем игрокам и элементам;
- частые клиентские события.
Нежелительный пример
addEventHandler("onClientRender", root,
function()
for _, player in ipairs(
getElementsByType("player")
) do
triggerServerEvent(
"hud:updatePlayer",
localPlayer,
player
)
end
end
)
При высоком FPS и большом онлайне код создаст огромное количество сетевых событий.
Оптимизация циклов
Не перебирайте весь сервер, когда можно хранить список нужных элементов.
local ownedVehicles = {}
local function registerOwnedVehicle(vehicle)
if not isElement(vehicle) then
return false
end
ownedVehicles[vehicle] = true
return true
end
local function updateOwnedVehicles()
for vehicle in pairs(ownedVehicles) do
if isElement(vehicle) then
updateVehicleState(vehicle)
else
ownedVehicles[vehicle] = nil
end
end
end
Завершайте поиск после результата
local function findPlayerByAccount(accountName)
for _, player in ipairs(
getElementsByType("player")
) do
local account = getPlayerAccount(player)
if not isGuestAccount(account)
and getAccountName(account) == accountName then
return player
end
end
return false
end
Также не вызывайте одну и ту же функцию несколько раз, если её результат можно сохранить в локальной переменной.
Оптимизация таймеров и обработчиков
Плохой подход
addEventHandler("onPlayerJoin", root,
function()
setTimer(
updatePlayerStatus,
100,
0,
source
)
end
)
Для каждого игрока создаётся отдельный частый таймер.
Один общий таймер
local playersToUpdate = {}
addEventHandler("onPlayerJoin", root,
function()
playersToUpdate[source] = true
end
)
addEventHandler("onPlayerQuit", root,
function()
playersToUpdate[source] = nil
end
)
setTimer(
function()
for player in pairs(playersToUpdate) do
if isElement(player) then
updatePlayerStatus(player)
else
playersToUpdate[player] = nil
end
end
end,
1000,
0
)
Не сохраняйте всех игроков каждые 100 миллисекунд
Нежелательно:
setTimer(saveAllPlayers, 100, 0)
Разумнее сохранять только изменённые данные через более длинный интервал:
setTimer(saveChangedPlayers, 60000, 0)
Утечка обработчиков
Нельзя добавлять один обработчик внутри другого повторяющегося события.
local function handlePlayerQuit()
savePlayerData(source)
end
addEventHandler(
"onPlayerQuit",
root,
handlePlayerQuit
)
Временный интерфейс
Обработчик отрисовки добавляется при открытии и удаляется при закрытии:
local interfaceVisible = false
local function openInterface()
if interfaceVisible then
return
end
interfaceVisible = true
addEventHandler(
"onClientRender",
root,
drawInterface
)
end
local function closeInterface()
if not interfaceVisible then
return
end
interfaceVisible = false
removeEventHandler(
"onClientRender",
root,
drawInterface
)
end
Утечки элементов и Lua-памяти
Временные автомобили, объекты, маркеры, колшейпы, текстуры и браузеры должны уничтожаться после использования.
Удаление временного элемента
local function createTemporaryZone(x, y, radius)
local zone = createColCircle(x, y, radius)
if not zone then
return false
end
setTimer(
function(element)
if isElement(element) then
destroyElement(element)
end
end,
60000,
1,
zone
)
return zone
end
Очистка данных игроков
local playerCache = {}
addEventHandler("onPlayerJoin", root,
function()
playerCache[source] = {
joinedAt = getRealTime().timestamp
}
end
)
addEventHandler("onPlayerQuit", root,
function()
playerCache[source] = nil
end
)
Причины постоянного роста памяти
- данные вышедших игроков остаются в таблицах;
- кэш не имеет ограничения размера;
- повторно добавляются обработчики;
- не уничтожаются браузеры и текстуры;
- сохраняются ссылки на удалённые элементы;
- создаются бесконечные журналы в памяти.
Если после команды restart название_ресурса память заметно уменьшается, источник утечки, вероятно, находится внутри этого ресурса.
Оптимизация setElementData
Element data удобно для синхронизации, но оно не должно заменять обычные Lua-таблицы и базу данных.
Нежелительно
addEventHandler("onClientRender", root,
function()
setElementData(
localPlayer,
"currentFPS",
getCurrentFPS()
)
end
)
Значение будет синхронизироваться каждый кадр.
Серверный кэш
local playerMoneyCache = {}
local function setCachedMoney(player, amount)
if not isElement(player) then
return false
end
playerMoneyCache[player] = amount
return true
end
local function getCachedMoney(player)
return playerMoneyCache[player] or 0
end
Основные правила
- не обновляйте значение, если оно не изменилось;
- не храните в element data секретные данные;
- не синхронизируйте большие таблицы;
- для приватных данных отправляйте событие конкретному игроку;
- серверные значения храните в обычных Lua-таблицах;
- очищайте таблицы после выхода игрока.
Проверка изменения
local function updatePlayerLevel(player, newLevel)
newLevel = tonumber(newLevel)
if not newLevel then
return false
end
if getElementData(player, "player:level")
== newLevel then
return true
end
return setElementData(
player,
"player:level",
newLevel
)
end
Оптимизация triggerClientEvent и triggerServerEvent
Не отправляйте персональные данные всем
Неправильно:
triggerClientEvent(
root,
"inventory:update",
resourceRoot,
playerInventory
)
Правильно:
triggerClientEvent(
player,
"inventory:update",
resourceRoot,
playerInventory
)
Передавайте изменения, а не всю таблицу
triggerClientEvent(
player,
"inventory:updateSlot",
resourceRoot,
slotId,
itemData
)
Сервер должен проверять
- объект
client; - частоту вызова;
- типы аргументов;
- размер переданных данных;
- расстояние до игрового объекта;
- права пользователя;
- реальную необходимость операции.
MTA уже синхронизирует игроков и транспорт. Не создавайте дополнительную передачу координат каждый кадр без специальной причины.
Оптимизация клиентской части
Сервер может работать стабильно, но игроки будут терять FPS из-за клиентских ресурсов.
Проверьте
- количество обработчиков отрисовки;
- DX-текстуры, шрифты и render target;
- CEF-браузеры;
- размер моделей и текстур;
- циклы по стримящимся элементам;
- создание объектов внутри рендера;
- освобождение видеопамяти.
Тестируйте интерфейсы не только на мощном компьютере разработчика, но и на более слабой системе.
Оптимизация onClientRender, DX и CEF
onClientRender вызывается каждый кадр. Внутри него нельзя создавать текстуры, шрифты, браузеры, выполнять тяжёлые циклы или отправлять серверные события.
Подготовка данных заранее
local screenWidth, screenHeight = guiGetScreenSize()
local hudX = screenWidth - 320
local hudY = 40
local cachedMoneyText = "$0"
addEvent("hud:updateMoney", true)
addEventHandler("hud:updateMoney", resourceRoot,
function(amount)
cachedMoneyText = "$"
.. tostring(tonumber(amount) or 0)
end
)
local function drawHUD()
dxDrawText(
cachedMoneyText,
hudX,
hudY,
hudX + 300,
hudY + 40
)
end
Создание текстуры один раз
local backgroundTexture = false
addEventHandler(
"onClientResourceStart",
resourceRoot,
function()
backgroundTexture = dxCreateTexture(
"assets/background.png"
)
end
)
addEventHandler(
"onClientResourceStop",
resourceRoot,
function()
if isElement(backgroundTexture) then
destroyElement(backgroundTexture)
backgroundTexture = nil
end
end
)
Рекомендации для DX
- уменьшайте разрешение изображений;
- повторно используйте шрифты;
- не создавайте render target каждый кадр;
- проверяйте видеопамять через
dxGetStatus; - удаляйте материалы после закрытия системы;
- ограничивайте тени и повторные проходы текста.
Оптимизация CEF
- не создавайте браузер при каждом открытии;
- скрывайте и повторно используйте один экземпляр;
- не вызывайте JavaScript каждый кадр;
- отключайте лишние CSS-анимации;
- не накапливайте DOM-элементы;
- уничтожайте браузер, когда он больше не нужен.
if isElement(interfaceBrowser) then
destroyElement(interfaceBrowser)
interfaceBrowser = nil
end
Оптимизация моделей, карт и игровых элементов
- сокращайте число декоративных объектов;
- удаляйте невидимые и дублирующиеся колшейпы;
- не создавайте пустой транспорт без необходимости;
- загружайте крупные модели по мере необходимости;
- уменьшайте размер TXD, DFF и изображений;
- упрощайте слишком сложные COL-файлы;
- удаляйте временные маркеры, педов и объекты.
Крупные текстуры и модели увеличивают время первого подключения, потребление памяти, видеопамяти и риск вылетов на слабых компьютерах.
Оптимизация MySQL для MTA:SA
Медленная база данных способна задерживать игровой процесс, даже если Lua-код использует мало процессорного времени.
Основные причины нагрузки
- новое соединение перед каждым запросом;
- синхронное ожидание результата;
- отсутствие индексов;
- использование
SELECT *; - загрузка тысяч строк без
LIMIT; - запросы внутри частых таймеров;
- сохранение неизменившихся данных;
- отдельный запрос для каждого предмета;
- неограниченные журнальные таблицы.
Увеличение памяти MySQL не исправит неправильный запрос, отсутствующий индекс или бесконечное повторение операций.
Соединения и асинхронные запросы
Создавайте соединение один раз
local connection = false
addEventHandler("onResourceStart", resourceRoot,
function()
connection = dbConnect(
"mysql",
table.concat({
"dbname=mta_server",
"host=127.0.0.1",
"port=3306",
"charset=utf8mb4"
}, ";"),
"mta_user",
"СЛОЖНЫЙ_ПАРОЛЬ",
"share=0;autoreconnect=1;batch=1"
)
if not connection then
outputDebugString(
"[DATABASE] Ошибка подключения.",
1
)
end
end
)
Соединение можно передавать другим ресурсам через экспортированную функцию.
Не блокируйте сервер через dbPoll с -1
Нежелительно:
local queryHandle = dbQuery(
connection,
"SELECT * FROM `players`"
)
local rows = dbPoll(queryHandle, -1)
Асинхронный вариант
local function loadPlayerCallback(queryHandle, player)
local rows = dbPoll(queryHandle, 0)
if rows == false then
outputDebugString(
"[DATABASE] Ошибка загрузки.",
1
)
return
end
if not isElement(player) then
return
end
if rows[1] then
applyPlayerData(player, rows[1])
end
end
local function loadPlayer(player, accountId)
dbQuery(
loadPlayerCallback,
{player},
connection,
[[
SELECT
`id`,
`nickname`,
`money`,
`level`
FROM `players`
WHERE `account_id` = ?
LIMIT 1
]],
accountId
)
end
Запросы без возвращаемого результата
dbExec(
connection,
[[
UPDATE `players`
SET `money` = ?
WHERE `id` = ?
]],
money,
playerId
)
Индексы и оптимизация SQL-запросов
Индекс для поиска по аккаунту
CREATE INDEX `idx_players_account_id`
ON `players` (`account_id`);
Составной индекс
CREATE INDEX `idx_vehicles_owner_deleted`
ON `vehicles` (`owner_id`, `deleted`);
Проверка плана выполнения
EXPLAIN
SELECT
`id`,
`model`,
`position_x`,
`position_y`,
`position_z`
FROM `vehicles`
WHERE `owner_id` = 125
AND `deleted` = 0;
Индексируйте столбцы, используемые в
WHERE;JOIN;ORDER BY;- поиске по serial;
- связях аккаунта и персонажа;
- загрузке имущества владельца.
Не используйте SELECT *
Нежелательно:
SELECT *
FROM `players`
WHERE `id` = ?
LIMIT 1
Лучше:
SELECT
`nickname`,
`money`,
`level`,
`skin`
FROM `players`
WHERE `id` = ?
LIMIT 1
Ограничивайте строки
SELECT
`admin_name`,
`action`,
`created_at`
FROM `admin_logs`
ORDER BY `id` DESC
LIMIT 100
Не выполняйте запрос для каждого элемента
Вместо множества запросов загружайте транспорт владельца одной выборкой:
SELECT
`id`,
`model`,
`position_x`,
`position_y`,
`position_z`
FROM `vehicles`
WHERE `owner_id` = ?;
Связанные изменения баланса и имущества выполняйте в транзакции, чтобы обе операции либо завершились успешно, либо были отменены.
Кэширование данных и debugdb
Не запрашивайте из MySQL значение, которое уже загружено и редко изменяется.
Подходящие данные для кэша
- настройки сервера;
- список предметов;
- цены магазинов;
- данные активного персонажа;
- состояние загруженного транспорта;
- справочники организаций.
Пример кэша игрока
local playerData = {}
local function setPlayerData(player, data)
if not isElement(player)
or type(data) ~= "table" then
return false
end
playerData[player] = data
return true
end
addEventHandler("onPlayerQuit", root,
function()
playerData[source] = nil
end
)
У кэша должны быть понятные правила загрузки, обновления, сохранения и удаления. Неограниченный кэш сам становится утечкой памяти.
Диагностика MySQL
debugdb 1
Ошибки и полный журнал запросов:
debugdb 2
Отключение:
debugdb 0
Журнал обычно находится здесь:
mods/deathmatch/logs/db.log
Ищите
- одинаковые запросы каждую секунду;
- запросы внутри циклов;
- выборки без
WHERE; - полные сохранения всех игроков;
- медленные запросы;
- операции после выхода игрока;
- ошибки соединения и таблиц.
Параметры производительности в mtaserver.conf
Снижение сетевого трафика
<bandwidth_reduction>medium</bandwidth_reduction>
none— меньше ограничений и больше трафика;medium— сбалансированное значение;maximum— максимальное сокращение трафика.
Лимит клиентского FPS
<fpslimit>74</fpslimit>
Этот параметр не исправляет тяжёлые серверные циклы, SQL-запросы и утечки памяти.
Интервалы синхронизации
<player_sync_interval>100</player_sync_interval>
<lightweight_sync_interval>1500</lightweight_sync_interval>
<camera_sync_interval>500</camera_sync_interval>
<ped_sync_interval>500</ped_sync_interval>
<unoccupied_vehicle_sync_interval>400</unoccupied_vehicle_sync_interval>
Не изменяйте эти значения как первую меру оптимизации. Малые интервалы увеличивают трафик, а слишком большие ухудшают синхронизацию.
HTTP-потоки
<httpthreadcount>8</httpthreadcount>
Параметр относится к встроенному HTTP-серверу и загрузке файлов. Он не ускоряет Lua и MySQL.
Пошаговый план оптимизации
- Создайте резервную копию ресурсов, конфигурации и MySQL.
- Зафиксируйте CPU, память, онлайн, пакеты и время запросов.
- Откройте performancebrowser.
- Найдите один подозрительный ресурс.
- Воспроизведите проблему на тестовой копии.
- Определите конкретную функцию, таймер или запрос.
- Исправьте одну причину.
- Повторите тот же нагрузочный тест.
- Сравните показатели.
- Проверьте потерю данных, ошибки и синхронизацию.
Типичные исправления
- увеличить интервал таймера;
- удалять обработчик после закрытия интерфейса;
- заменить element data серверной таблицей;
- добавить индекс MySQL;
- сделать запрос асинхронным;
- отправлять событие конкретному игроку;
- уничтожать временные элементы;
- ограничить размер кэша.
Частые вопросы по оптимизации MTA:SA
Как найти ресурс, который нагружает сервер?
Запустите performancebrowser и проверьте Lua timing, Lua memory, количество элементов и Event Packet usage.
Почему CPU высокий при небольшом онлайне?
Причиной может быть короткий таймер, тяжёлый цикл, повторяющийся обработчик или постоянный SQL-запрос.
Почему память постепенно растёт?
Проверьте таблицы игроков, обработчики, таймеры, элементы, текстуры, браузеры и кэши, которые не очищаются.
Нужно ли ежедневно перезапускать сервер?
Нет. Регулярный перезапуск временно скрывает утечку, но не устраняет её причину.
Можно ли хранить все данные в element data?
Нет. Серверные данные лучше хранить в Lua-таблицах, синхронизируя только нужные клиентам значения.
Почему нельзя использовать dbPoll с -1?
Ожидание результата способно задержать серверную логику. Для обычных выборок используйте асинхронный callback dbQuery.
Сколько соединений dbConnect нужно?
Обычно достаточно одного постоянного соединения или небольшого контролируемого набора.
Поможет ли более мощный VPS?
Он может увеличить запас производительности, но не исправит бесконечный цикл, утечку памяти и неиндексированный запрос.
Как проверить результат?
Повторите тот же сценарий и сравните CPU, память, время SQL-запросов, пакеты и Lua timing.
Заключение
Оптимизация MTA:SA должна начинаться с измерений. Используйте performancebrowser, системный мониторинг, журналы Lua и debugdb, чтобы найти конкретный ресурс, функцию или запрос.
Частые причины нагрузки — короткие таймеры, повторно добавленные обработчики, циклы по всем элементам, утечки объектов, чрезмерный setElementData, события для всех игроков и синхронные SQL-запросы.
Соединение с MySQL создавайте один раз, выборки выполняйте асинхронно, добавляйте обоснованные индексы и загружайте только необходимые столбцы и строки.
В клиентских ресурсах не создавайте текстуры, шрифты, браузеры и render target внутри onClientRender. Подготавливайте данные заранее и запускайте отрисовку только при открытом интерфейсе.
После каждого изменения повторяйте одинаковый тест. Оптимизация считается успешной только тогда, когда показатели улучшились без потери данных и ухудшения синхронизации.
Администрирование и обслуживание:
Установка Lua-ресурсов:
Настройка базы данных:
Конфигурация сервера:
Комментарии к инструкции
Обсудите решение, задайте вопрос или дополните инструкцию