Как спасти проект от закрытия, разобравшись с MySQL

в 17:46, , рубрики: mysql, mysql performance, mysql performance tricks, mysql workbench, разработка игр

Продолжаю повествование о разработке экономической онлайн игры. В этой части речь пойдет об истории 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js