Всем привет!
Сегодня я хочу поделиться с вами решением задачи, с которой мы столкнулись при разработке социальной игры. Игровой клиент был написан на flash, а для back-end был выбран php. Игра относится к тайм-менеджмент играм.
Схема работы была выбрана следующая:
- игрок совершает действие на клиенте
- клиент проверяет возможность совершения действия
- отсылает команду на сервер
- сервер проверяет возможность совершения действия, выполняет команду, производя изменения в базе
- клиент уведомляется о том, что все ок или информируется об ошибке
Все работало отлично, пока не происходило резкое возрастание количества игроков.
Сначала начались тормоза со стороны php. Основная проблема данной реализации заключалась в том, что на каждое действие игрока дергается сервер, который производит довольно много вычислений по обсчету объектов на карте перед выполнением команды. Эта проблема была решена путем добавления дополнительных серверов с обработчиками php.
Потом мы уперлись в производительность mysql. Было слишком много запросов. Так как шардинг не был заложен в систему, то выкручивались как могли. Что-то перенесли в mongodb, где-то улучшили работу с кэшем.
Кстати, mongodb оказался не таким простым хранилищем, как может показаться на первый взгляд. Учитывая то, что у нас было включено шардирование и стояли правильные индексы, мы все равно словили там тормоза, с которыми не смогли разобраться на тот момент. Периодически просто высыпалась пачка запросов в log, которые вдруг затупили. Хотя через секунду те же запросы работали нормально при том же количестве запросов. Но это тема отдельного поста.
Собственно для нового аналогичного проекта было принято решение использовать другую схему взаимодействия клиента и сервера.
Именно об это я и хочу рассказать.
Принцип работы следующий:
- при запуске игра получает полное игровое состояние игрока и актуальный игровой баланс
- игрок совершает действия на клиенте
- клиент валидирует их, но не отправляет на сервер, а накапливает у себя
- по достижении какого-либо условия клиент отправляет на сервер все эти команды пачкой
- сервер проверяет, что команды имеют верный формат и просто сообщает клиенту, что все ок
- при этом все команды сохраняются в очередь
- на сервере так же запущен демон, который периодически вытаскивает пачки команд из этой очереди и выполняет их
Получается схема с отложенной обработкой команд, которая имеет следующие плюсы:
- намного уменьшается количество запросов к серверу
- уменьшается количество обращений в mysql при обработке команд, так как сохранение в базу происходит лишь после обработки всех команд из пачки
Есть конечно и минусы, которые являются скорее решаемыми задачами. И тут уже нужно смотреть на требования проекта.
У нас список был следующий:
- получение актуального игрового состояния в момент, пока не вся очередь для данного пользователя обработана
- некоторые команды требуют выполнения в realtime (к примеру покупка реала)
- читерство игроков на клиенте (команду удалось выполнить на клиенте, но на сервере она не выполнилась)
- обработка в несколько потоков
В качестве сервера очередей на проекте используется Gearman. В остальном все стандартно: php + mysql + memcached.
В текущую реализацию закладывается шардинг для mysql и memcached (одного сервера memcached бывает мало).
Давайте расскажу о том, как решаются вышеперечисленные задачи.
На сервер ушли пачки команд. Игрок закрыл игру и тут же запустил вновь. Очередь еще не обработана.
Так как Gearman это не бд и он не позволяет как-то искать по данным, которые хранятся внутри него, то было написан модуль на php, который позволяет подключить к обработке команд базу и всегда знать, в каком состоянии находится обработка конкретного пользователя: сколько пачек команд в обработке и, были ли ошибки в обработке.
Когда клиент запрашивает профиль игрока, для которого разобрана еще не вся очередь, он получает в ответ сообщение с предложением подождать и запросить профиль через 10 секунд. Так повторяется до тех пор, пока профиль не будет получен.
По примеркам потребуется до 20 секунд для того, чтобы выполнить все команды игрока, что допустимо.
Выполнение определенных команд в realtime
Это скорее даже не проблема. Просто реализуется возможность определенные команды выполнять тут и сейчас. Нужно для того, чтобы игрок не потерял деньги, которые он вносит в игру в случае проблем в обработке очереди.
Читерство игрока на клиенте. Ошибки из-за разницы в логике на сервере и клиенте
Ситуация: игрок подкрутил клиент и накинул себе денег. Клиент после этого позволяет ему купить здание. Затем игрок совершает какие-то действия со зданием. Создается цепочка событий, которые не могли произойти, так как денег на самом деле у игрока нет.
Сервер получает 4 пачки команд. Во второй как раз и содержится команда покупка здания.
Начинается обработка очереди. Первая пачка команд обрабатывается успешно, а во второй будет логическая ошибка, которая приводит к тому, что здание купить неудается.
В этот момент для этого пользователя в базе ставится метка времени, когда произошла ошибка. Так как во всех пачках команд содержится информация о том, когда они пришли, то в момент, когда обработчик получит 3 и 4 пачки он их пропустит, так как их время создания меньше времени ошибки.
В этот момент клиент отсылает 5 пачку команд, но вместо ответа, что все ок получает запрос на перезапуск игрового состояния. Мы знаем, когда клиент последний раз получал игровое состояние. И если это время меньше времени последней ошибки, то не сервер отказывается принимать команды на обработку.
Обработка в несколько потоков
У нас есть 3 потока. И 3 пачки команд в очереди для разных пользователей. Обработчики просто одновременно выбирают эти пачки и обрабатывают. Все отлично.
Ситуация усложняется, когда у нас в очереди 3 пачки для одного пользователя и более 1 свободного обработчика. Чтобы не возникло ситуации, в которой начинается параллельная обработка двух и более пачек команд одного игрока (их нужно обрабатывать последовательно) реализуется своеобразный светофор, который позволяет сказать, что пользователь в данный момент обрабатывается. И если это так, то второй обработчик положит данные обратно в очередь, но уже с повышенным приоритетом. Приоритет меняется для того, чтобы 2 задача осталась впереди 3. Иначе после обработки 1 пачки обработчик получит 3 пачку. Управлением этого светофора занимаются обработчики. В момент после получения команд говорим, что пользователь processed, а по окончанию обработки — ready.
Это собственно все, что хотелось рассказать. Не хочется углубляться в реализацию, так как там нет ничего нетривиального.
В качестве фреймворка используется Yii framework. Для демона используется команда для yiic, которая запускается следующим образом
nohup ./yiic que work > /dev/null &
Команда следит за количеством потомков. Запускает новых, если они по каким-то причинам падают.
Потомки регистрируют GearmanWork на разбор очереди.
Спасибо за внимание! Ставьте большие пальцы вверх и подписывайтесь :)
Автор: anonimizer_me