RE:NODE

Руководства12 мин чтения

Базы данных FiveM и oxmysql: настройка и медленные запросы

Как сервер FiveM использует базу данных: слот в панели, строка подключения oxmysql, импорт схемы фреймворка, медленные запросы и резервные копии.

0 прочтений

Всё, что ролевой сервер FiveM помнит между перезапусками, лежит в базе данных: персонажи, деньги, транспорт, недвижимость, работы, баны. Фреймворк не обращается к ней напрямую. Он идёт через oxmysql - ресурс, который держит пул соединений и предоставляет функции запросов всем остальным ресурсам сервера. Правильная строка подключения - и вы о ней больше не вспомните. Неправильная - и симптомом будет не ошибка базы данных, а игроки, появляющиеся без персонажа, деньги, обнуляющиеся при повторном входе, или весь сервер, зависший на экране загрузки, пока один ресурс вечно ждёт запрос, на который никто не ответит.

Что на самом деле хранит сервер FiveM#

Схема фреймворка больше, чем люди ожидают. И ESX Legacy, и QBCore поставляют SQL-дамп, создающий при импорте от пятнадцати до сорока таблиц, а каждый добавляемый затем ресурс может добавить свои.

Те, что важны и на которые вы в итоге будете смотреть вручную:

  • Таблица персонажей. В ESX она называется users и ключом служит identifier. В QBCore и Qbox она называется players, ключом служит citizenid, а идентификатор лицензии игрока лежит в отдельном столбце. Одна строка на персонажа.
  • Транспорт. owned_vehicles в ESX, player_vehicles в QBCore, обе с ключом по номеру и связью с владельцем.
  • Работы и грейды. В ESX это строки базы данных в jobs и job_grades. В QBCore и Qbox это Lua, а не SQL, и это одно из практических различий, описанных в статье сравнение фреймворков FiveM.
  • Баны, логи и всё, что решили создать ваши ресурсы телефона, банка и жилья.

Главное, что нужно понять обо всех этих таблицах: интересные столбцы - это JSON-блобы. Строка players в QBCore хранит money, charinfo, job, gang, metadata и inventory как JSON-текст. ESX делает то же с accounts и inventory. Такой замысел сохраняет гибкость фреймворка и означает, что вы не можете индексировать внутри этих столбцов, не можете эффективно запросить «всех, у кого в банке больше десяти тысяч» и тянете несколько килобайт при каждом SELECT * по игроку. Это также означает, что повреждённый блоб ломает ровно одного персонажа, а не таблицу, - на такой компромисс они пошли.

Персонажи пишутся не непрерывно. Оба фреймворка сохраняют по таймеру и при отключении: qb-core выставляет интервал как Config.UpdateInterval в минутах, а в ESX есть эквивалент в конфигурации. Всё, что убивает процесс без чистого завершения, теряет до одного интервала прогресса у всех, кто был онлайн. Это лучший довод в пользу того, чтобы перезапускать сервер FiveM кнопкой Restart в панели, а не убивать процесс.

Слот базы данных и что нужно oxmysql#

oxmysql говорит на сетевом протоколе MySQL, так что ему нужен сервер, совместимый с MySQL или MariaDB. От вас он хочет пять вещей: хост, порт, пользователя, пароль и имя базы данных.

На RE:NODE игровой тариф включает один слот базы данных, который создаётся на вкладке Databases в панели. Панель сама генерирует хост, пользователя и пароль; вам не нужно ни устанавливать сервер базы данных, ни администрировать его, и нет root-аккаунта, о котором пришлось бы заботиться. Рядом со слотом кнопка Open in phpMyAdmin, которая входит с одноразовым токеном, истекающим через шестьдесят секунд: так просмотр таблиц - это кнопка, а не ещё один пароль, который надо где-то хранить. Панель не называет движок за слотом, и oxmysql это не нужно: он согласует протокол при подключении. Важно, чтобы учётные данные, которые выдаёт панель, были теми, что вы вставляете в server.cfg.

Два честных ограничения. Один слот - одна база данных: для фреймворка этого хватает, ведь и ESX, и QBCore кладут всё в одну, но не хватает, если вы хотели ещё отдельную базу для бота Discord на том же тарифе. И наша линейка хостинга баз данных - это PostgreSQL и MongoDB, а ни с той ни с другой oxmysql говорить не умеет, так что этот продукт не путь для схемы FiveM. Слот в панели - путь.

Строка подключения#

oxmysql читает одну convar. Поместите её в server.cfg выше строк ensure, используя set, а не sets: строку подключения, опубликованную в списке серверов, вытащат за считаные минуты. Разница между тремя описана в статье server.cfg FiveM простыми словами.

server.cfg
set mysql_connection_string "mysql://s7_user:PaSsW0rd@db.example.net:3306/s7_fivem?charset=utf8mb4"ensure oxmysqlensure ox_libensure qb-core

Части по порядку: пользователь, пароль, хост, порт, имя базы данных, параметры. Три вещи здесь идут не так чаще всего.

Спецсимволы в пароле. У URI есть структурные символы, и @, :, /, ?, # и % в пароле сломают разбор так, что ошибка окажется бессмысленной. Либо закодируйте их через процентное кодирование (@ становится %40), либо используйте форму с точкой с запятой, которой это безразлично:

config
set mysql_connection_string "server=db.example.net;port=3306;userid=s7_user;password=P@ss:word;database=s7_fivem"

Хост - не `localhost`. База данных - отдельная служба, до которой добираются по сети, поэтому 127.0.0.1 не подключается ни к чему. Скопируйте хост в точности так, как его печатает панель.

У имени базы данных есть префикс. Базы, созданные панелью, обычно несут префикс сервера вроде s7_. Оставьте его. ER_BAD_DB_ERROR - это почти всегда кто-то, кто его обрезал.

Добавьте ?charset=utf8mb4 и относитесь к этому серьёзно. Без него первый игрок, у чьего персонажа в имени есть буква с диакритикой, кириллическая буква или эмодзи, запишет в столбец испорченные байты, а вы узнаете об этом через недели. Два полезных дополнения: ?connectionLimit=8 ограничивает пул, а oxmysql принимает обычные параметры драйвера в той же строке запроса.

Включайте диагностику, пока настраиваете, и выключайте, когда всё заработало:

config
set mysql_debug true                 # prints every query, very noisyset mysql_slow_query_warning 150     # milliseconds before a warning

mysql_debug принадлежит только тестовому серверу. mysql_slow_query_warning принадлежит постоянно: это самый дешёвый инструмент производительности, какой у вас есть, и он называет виновный ресурс.

MySQL.queryпул соединенийвы, вручнуюстрокиРесурсqb-garagesphpMyAdminодноразовый токенСлот базы данныххост, user, парольoxmysqlпул соединений
Как ресурс добирается до базы данных

Импорт схемы фреймворка#

Каждый фреймворк поставляет SQL-дамп. Импортируйте его до первого запуска, а не после того, как сервер десять минут печатал ER_NO_SUCH_TABLE.

  1. Найдите дамп. Обычно он лежит в основном ресурсе: qb-core/qb-core.sql для QBCore, аналогичный файл в релизе ESX. Некоторые сборки делят его на несколько файлов, и порядок важен, если есть внешние ключи.
  2. Откройте phpMyAdmin из панели, выберите свою базу данных в левой колонке и воспользуйтесь вкладкой Import.
  3. При импорте задайте кодировку utf8mb4, в соответствии с тем, что вы указали в строке подключения.
  4. После импорта проверьте число таблиц. Дамп, остановившийся на полпути, оставляет сервер, который наполовину работает, а это хуже, чем сервер, который не запускается.

Если файл слишком велик для лимита загрузки - а дампы фреймворков часто такие - сожмите его. phpMyAdmin принимает .sql.gz и .sql.zip и распаковывает на лету, что обычно уменьшает дамп в восемь-девять раз. Если импорт прервался по таймауту на полпути, на странице Import в phpMyAdmin есть поле «Skip this number of queries»: посчитайте, что уже выполнилось, и продолжите с этого места. Оба способа подробно описаны в статье импорт и экспорт в phpMyAdmin.

Запросы, которые не вредят#

oxmysql подключается к ресурсу одной строкой в fxmanifest.lua:

lua
server_scripts {    '@oxmysql/lib/MySQL.lua',    'server/main.lua',}

Это даёт MySQL.query, MySQL.single, MySQL.scalar, MySQL.insert, MySQL.update, MySQL.prepare и MySQL.transaction, у каждой из которых есть вариант .await для использования внутри корутины.

lua
-- one row, one valuelocal money = MySQL.scalar.await(    'SELECT money FROM players WHERE citizenid = ?', { citizenid })-- one row as a tablelocal row = MySQL.single.await(    'SELECT charinfo, job FROM players WHERE citizenid = ?', { citizenid })-- many statements, one round trip, all or nothingMySQL.transaction.await({    { 'UPDATE players SET money = ? WHERE citizenid = ?', { newMoney, citizenid } },    { 'INSERT INTO bank_log (citizenid, amount) VALUES (?, ?)', { citizenid, delta } },})

Всегда используйте плейсхолдеры ?, а не подставляйте значения в строку. Это быстрее, потому что сервер может переиспользовать план, и это единственная защита от игрока, который ставит кавычку в имя и переписывает ваш запрос за вас.

Четыре привычки отличают базу, простаивающую при 64 игроках, от той, что стала узким местом:

  • Никогда не делайте запросы в цикле по игрокам. Один запрос, возвращающий тридцать строк, лучше тридцати запросов, возвращающих по одной. Если вы перебираете игроков и вызываете .await внутри цикла, вы к тому же блокируете корутину на каждой итерации.
  • Никогда не делайте запросы на каждом тике. Кэшируйте значение в таблице Lua, записывайте его при изменении и при сохранении.
  • Выбирайте только нужные столбцы. SELECT * по строке players тянет по сети каждый JSON-блоб, а инвентари не малы.
  • Группируйте записи в транзакцию. Десять обновлений в одной транзакции - это один обмен и одно окно блокировки; десять отдельных обновлений - это десять того и другого.

Про сторону пула, которая кусает, когда двадцать ресурсов в один момент решают проявить хитрость, рассказано в статье пулы соединений и лимиты.

Поиск медленного запроса#

Когда задан mysql_slow_query_warning, oxmysql печатает строку с названием ресурса, затраченным временем и оператором всякий раз, когда запрос переходит порог. Чаще всего эта строка - всё расследование: ресурс, в который никто не заглядывал с 2022 года, выполняет SELECT с LIKE '%name%' по таблице, выросшей до четырёхсот тысяч строк.

Когда предупреждения недостаточно, возьмите оператор в phpMyAdmin и поставьте перед ним EXPLAIN. Ищите тип сканирования. Строка, говорящая, что запрос просмотрел большую часть таблицы ради одного результата, - это отсутствующий индекс, и исправление обычно занимает одну строку:

sql
ALTER TABLE player_vehicles ADD INDEX idx_citizenid (citizenid);ALTER TABLE owned_vehicles ADD INDEX idx_owner (owner);

Индексируйте столбцы, по которым вы фильтруете или соединяете: owner, citizenid, identifier, plate. Не индексируйте всё подряд: каждый индекс - ещё одна вещь, которую нужно записывать при каждой вставке, и таблица с двенадцатью индексами сохраняет персонажа медленнее, чем таблица с тремя.

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

sql
DELETE FROM <your log table> WHERE created_at < NOW() - INTERVAL 30 DAY;

Запустите её один раз вручную, проверьте число строк, а потом решайте, стоит ли ставить на расписание. Удаление миллиона строк одним оператором надолго удерживает блокировки, поэтому на большой таблице делайте это пакетами с LIMIT 10000 и повторяйте.

Резервные копии и то, что легко упустить#

Резервная копия вашего сервера берёт файлы: resources/, server.cfg, кэш. База данных - отдельная служба на отдельном хосте, поэтому в этот архив она не входит. Если вы восстановите резервную копию сервера, когда что-то пошло не так, вы вернёте каждый ресурс и каждого персонажа ровно в том состоянии, в каком сейчас находится база данных, а это может быть то самое состояние, от которого вы пытались уйти.

Поэтому экспортируйте и базу данных, и считайте это настоящей резервной копией:

  1. В phpMyAdmin выберите базу данных, затем Export, затем Custom.
  2. Формат SQL, вывод сжат gzip, и отметьте «Add DROP TABLE», чтобы файл чисто восстанавливался на базу, где таблицы уже есть.
  3. Скачайте его и положите куда-нибудь, что не является игровым сервером. Копия, лежащая в resources/, - не резервная копия: она умирает вместе с тем, что должна была защищать.

Делайте это перед каждым обновлением фреймворка, перед запуском любого .sql ресурса и по расписанию, которого вы действительно будете придерживаться. Затем восстановите одну копию, один раз, в пробную базу данных и войдите: проверка восстановления до того, как оно понадобится существует потому, что экспорт, который никогда не импортировали, - гипотеза. Общий порядок действий описан в статье резервные копии и восстановление баз данных, а руководства по панели подскажут, где находится слот.

Ошибки подключения и что они означают#

ОшибкаПричина
ECONNREFUSEDНеверный хост или порт, либо localhost в строке
ER_ACCESS_DENIED_ERRORНеверный пользователь или пароль, либо неэкранированный символ
ER_BAD_DB_ERRORНеверное имя базы данных, обычно отрезанный префикс
ER_NO_SUCH_TABLEСхема не импортирована или импортирована в другое место
ER_BAD_FIELD_ERRORРесурс ожидает столбец, которого нет в вашей версии схемы
ER_DATA_TOO_LONGJSON-блоб перерос тип своего столбца
ER_CON_COUNT_ERRORСлишком много соединений; уменьшите connectionLimit

Ещё две, которые ошибками не являются, но похожи на них. Connection lost: the server closed the connection на простаивающем сервере - это база данных, разрывающая застоявшееся соединение, и oxmysql открывает его заново: если это случается раз в час и ничего не ломается, игнорируйте. А первый запрос, занимающий две секунды после перезапуска, - это прогрев пула, а не медленная база данных.

Если не подключается вообще ничего, проверяйте в таком порядке: convar задана через set и написана правильно, ensure oxmysql стоит перед всем, что им пользуется, хост - это хост из панели, а не localhost, и в пароле нет неэкранированных спецсимволов. Эта последовательность решает почти все случаи.

FAQ#

Нужна ли серверу FiveM база данных?

Только если ему нужно что-то запоминать. Сервер для свободной езды или гонок без сохранения состояния прекрасно работает вообще без базы данных. Каждому ролевому фреймворку она нужна, потому что персонаж, не переживший перезапуск, - не персонаж.

Можно ли использовать PostgreSQL или MongoDB вместо этого?

Ни с oxmysql, ни с каким-либо распространённым фреймворком FiveM - нет. Они написаны под протокол MySQL и его диалект SQL. Наши линейки PostgreSQL и MongoDB предназначены для приложений, а не для ESX или QBCore.

Насколько большой становится база данных FiveM?

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

Почему после сбоя все потеряли деньги?

Потому что персонажи записываются по таймеру, а не при каждом изменении. Процесс, умерший без чистого завершения, теряет для каждого игрока онлайн всё, что произошло с последнего сохранения. Сократите интервал сохранения фреймворка, если потеря важнее нагрузки на запись.

Могут ли два сервера использовать одну базу данных?

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

Нужно ли знать SQL, чтобы вести сервер FiveM?

Достаточно, чтобы прочитать EXPLAIN, добавить индекс и экспортировать дамп. Остальное сделает дамп самого фреймворка. Всё, что описано в этой статье, - четыре-пять операторов, которые можно скопировать, и phpMyAdmin напишет большинство из них за вас.


Комментарии

Полностью анонимно: без аккаунта, без почты, без cookie. Мы храним имя, которое вы ввели, текст и время - больше ничего. Количество ссылок ограничено, разметка не отображается.

0/2000