Продолжаю повествование о разработке экономической онлайн игры. В этой части речь пойдет об истории 2016 года, когда во весь рост стал вопрос закрытия проекта.
Отправная точка
По мере развития игры игровых объектов становится все больше и больше, компании растут и обсчитывать игровую ситуацию становится все сложнее и сложнее. Транзакции повисали по таймауту и игровые объекты сохраняли свое состояние с ошибками, что приводило в свою очередь к другим ошибкам. В логах сервера с завидной регулярностью писалось о следующей проблеме:
Lock wait timeout exceeded; try restarting transaction
Google явного решения не давал, общая рекомендация заключалась в прочесывании бизнес-логики.
Ночные звонки о проблемах, бессонные ночи, сорванные выходные. В какой-то момент мы дошли до состояния перманентной тревоги, и перестали удивляться происходящим ошибкам. Также на некоторые действия игроков сервер реагировал непозволительно долго.
Данная ситуация провоцировало логичное негодование игроков, это приводило к постепенному оттоку игроков и падению выручки.
В общем — ситуацию надо было спасать. Засучив рукава, мы начали с чистого листа искать решение.
Обновляем инструментарий
Для этого пришлось проапгрейдить MySQL до версии 5.7, чтобы полноценно работала performance_schema, она позволяет в реальном времени собирать статистику и оперативно отслеживать влияние изменений. Не могу сказать, что апгрейд прошел гладко, требуется терпение. Кстати с 5.5. на 5.7 сразу перейти нельзя, сначала надо 5.6 поставить. Далее устанавливаем MySQL Workbench — удобный интерфейс для анализа БД. Затем инсталлируем performance_schema в разделе Performance
Первая настройка
Смотрим данные и проверяем, насколько работа базы данных соответствует бизнес-логике, нет ли повышенной нагрузки там, где ее быть не должно, проверяем потенциально узкие места. Используя отчет Top File I/O File by Time было открыто, что розничная статистика каждый раз запрашивается из базы. Т.к. данные не меняются в игровой день и одинаковы для всех игроков необходимо эти данные закэшировать. Эти запросы выпали из статистики самых частотных, аналитика стала выдаваться многовенно и вообще все вроде бы должно стать ок.
Все равно медленно
Нагрузка снизилась, но по какой-то причине интерфейс работает с трудом. Вероятно возникает Lock на MyISAM таблицах, пытаемся поймать по show processlist — но безуспешно. Ситуация возникает несколько раз в день, без серьезных симптомов. Пишем простой код для того, чтобы логировать возникающие Lock:
Таблица для сохранения данных.
create table processlist_stat like information_schema.processlist
alter table processlist_stat add column datetime datetime;
Делаем Thread, который будет постоянно писать в базу о проблемах.
public static class ProcessListThread extends Thread {
private long timeout;
public ProcessListThread(long timeout) {
super("ProcessListThread");
this.timeout = timeout;
}
@Override
public void run() {
if (timeout > 0) {
while (!interrupted()) {
try {
sleep(timeout);
} catch (InterruptedException ie) {
System.out.println("Interrupted!!!");
return;
}
if (processListThreadStopped) {
Thread.currentThread().interrupt();
System.out.println("SelfDestruction");
}
try {
saveProcessListStat();
} catch (Throwable e) {
Logger.error(e);
}
}
}
}
}
Сохраняем данные в базу
private static void saveProcessListStat() throws ru.plazma.db.DBException {
Executor e = GameObject.getExecutor();
Query rs = e.getConnection().newQuery();
rs.select("id,user,host,db,command,time,state,info", "information_schema.processlist", "info is not null and info not like '%info is not null%'");
rs.calculate("sysdate() as datetime");
Statement st = e.newStatement();
st.insert("processlist_stat", "id,user,host,db,command,time,state,info,datetime", rs);
st.execute();
e.commit();
}
Пуск/Запуск
public static void runProcesslistLog() throws DBException {
if (processListThread == null) {
processListThread = new ProcessListThread(5 * 1000);
}
processListThreadStopped = false;
processListThread.run();
}
public static void stopProcesslistLog() {
if (processListThread != null) {
processListThreadStopped = true;
processListThread.interrupt();
}
}
Данную идею можно реализовать на триггерах самой базы данных, но при его запуске у меня напрочь завалился сервер, так что выбран был именно описанный выше путь.
Бинго
Смотрим в таблицу, созданную выше, куда каждый 5 сек. пишется состояние запросов.
Вот же они… Waiting for table level lock длительностью по 60 секунд… оказалось что ключевая таблица sales запирается из-за того, что подозрительно долго идут запросы к ней. Попутно всплыли еще какие-то запросы, которые тоже идут слишком долго.
Решаемся на двойной удар — очистить таблицы от старых данных (стирали недостаточно), прочесать бизнес-логику и убрать ненужные обращения к таблице.
Стало лучше, но не сильно. В чем же причина… Индексы стоят, вроде все должно быть ок — но нет. Долго.
Ох уж эти индексы.
Оказалась, что висящие запросы делают full scan… а Primary Key не задействуют… все дело в конструкции between. Фильтр по составному PK, если используется between почему-то не активируется. Делаем дополнительные индекс… и вауля — все полетело.
Итоги
Эта переделка вдохнула новую жизнь в проект, и дала жизненно важное время на разработку нового функционала, который мы через несколько месяцев запустили, не растеряв аудиторию.
Автор: qdreadknight