Пересаживаем высоконагруженный игровой проект с Photon на кастомные решения
Photon — это целый ворох решений для создания многопользовательских игр. Они позволяют тратить меньше времени на разработку типичных вещей вроде матчмейкинга и балансировки и сосредоточиться на геймплее.
Но, как это часто бывает, с развитием продукта универсальные решения требуют обработки напильником. А ведь War Robots существует уже почти восемь лет — инфраструктура серверов за это время менялась неоднократно по мере масштабирования проекта, который сейчас уже перешагнул через порог 200 млн установок.
В нашем случае такая обработка вылилась в собственные реализации тех или иных компонентов. Матчмейкинг и социальные фичи перекочевали в отдельные сервисы, новые игровые механики реализовывались на сервере для лучшей согласованности. В итоге от Photon остался транспорт, прослойка PUN на стороне клиента и некоторые сопутствующие расходы в виде лицензии, привязки к Windows и .Net Framework и чрезмерных аллокаций на клиенте.
Стало понятно, что затраты на фреймворк превышают его ценность, и надо тiкать.
Снимаем сову с глобуса
Итак, что мы имели по структуре серверов на проекте. У нас был Master Server, который занимался балансировкой, и Game Server, который, помимо того, что обрабатывал бои, также позволял делать API-запросы в микросервисы через специальную «ангарную» комнату. С тех пор эта комната переместилась в отдельный сервис, но не перестала выглядеть чем-то инородным.
Схема работы была примерно такой:
Обратите внимание на две балансировки: Master Server распределяет клиентов по API серверам, а выбор Game Server лежит на Matchmaking Server. На диаграмме специально не отображен процесс подключения к специальным комнатам в Master и API Server, иначе она была бы ещё больше.
Тут следует сделать небольшое отступление. Насколько бы ни была хорошо изолирована бизнес-логика, перевод такого проекта, как War Robots, на новый транспорт — это довольно долгий процесс. Помимо самого рефакторинга, необходимо:
- провести тестирование,
- поменять процесс развертывания,
- подготовить тестовые окружения,
- закупить или перенастроить новые сервера,
- обновить документацию,
- настроить дэшборды в системе мониторинга и т. д. и т. п.
Такой объем не поместится ни в один спринт, а ценность подобного рефакторинга для бизнеса, прямо скажем, неочевидная. Но если разбить переход на этапы так, чтобы в конце каждого был ощутимый результат, протолкнуть идею будет проще, ведь между этапами можно будет заниматься монетизируемыми фичами, а рефакторинг не будет лежать подолгу в отдельной ветке, в которую нужно постоянно подливать master.
Принимая во внимание вышесказанное, первым делом было решено избавляться от Photon в API Server.
Его перевели на TCP и protobuf. До этого, как и на игровых серверах, там были RUDP и сериализация Photon. Это позволило не переписывать Master Server, а просто удалить его и перейти на балансировку с помощью HAProxy.
Конечно, нельзя просто заменить транспорт на клиенте и отдать его игрокам: ошибки могут возникнуть в неожиданных местах, а релиз новой версии приложения в сторах может занимать часы и даже дни. Поэтому нужно было сохранить возможность быстрого переключения на старый, проверенный транспорт.
Для этого на сервере добавилось абстракций, и он разделился на две реализации: работающую с Photon и новую. Таким образом, после окончательного перехода оставалось только удалить ненужный проект из солюшена, а не заниматься точечным вычищением кода.
Код, отвечающий за взаимодействие с Photon, не изменился ни на сервере, ни на клиенте, поэтому можно было не опасаться, что что-то внезапно пойдет не так.
В переходный период Profile Server на запрос списка Master Server отдавал либо адреса Photon-овских мастеров, либо HAProxy — в зависимости от настройки. И после окончательного перехода схема стала выглядеть так:
Рутина
Следующим шагом стал отказ от Photon на гейме. Алгоритм действий — такой же, как и в случае с API Server:
- написали бенчмарк для сравнения двух библиотек, реализующих RUDP,
- выбрали наиболее производительную библиотеку,
- провели рефакторинг,
- разделили сервер на два сервиса, работающих с разными транспортами,
- добавили переключатель,
- отдали в отдел QA.
Спустя несколько итераций тестирования и полировки мы были готовы обновлять боевые сервера.
На проекте War Robots есть практика проведения так называемого внешнего тестирования — это когда игроки могут скачать отдельную версию игры и посмотреть контент, который ещё находится в разработке, а мы можем собрать фидбек и метрики, не боясь сломать что-то на продакшене.
И конечно, перед релизом, мы решили проверить работоспособность нового транспорта в рамках такого тестирования.
Неприятности
На внешнем тестировании нас ждал сюрприз. Всё было очень плохо: роботы телепортировались, урон не наносился, игроков выкидывало из комнаты. Понятно, что дело было в нагрузке, ведь в процессе внутренних тестов подобных симптомов не наблюдалось. Но что же произошло — бенчмарк же показал, что всё хорошо?
А корень зла был в самом бенчмарке. Дело в том, что помимо смены транспорта планировалась переделка протокола. Не вдаваясь в подробности, скажу, что сообщений должно было стать меньше, они должны были быть больше, но не превышать MTU. Конечно, согласно методологии, переход на новый протокол должен был состояться в следующей итерации, но бенчмарк проектировался с учетом нового протокола, а не текущего. В текущем же было много мелких сообщений, а выбранная библиотека не поддерживала их склейку в один UDP-пакет, преследуя цель как можно быстрее отправить данные. Слишком частые вызовы отправки влияли на пропускную способность, что и приводило к проблемам.
К счастью, интерфейсы библиотек были очень похожи, и смена на более подходящую для нашего протокола заняла не больше часа. Мы провели очередной сеанс тестирования и убедились, что всё работает, как надо.
Оставалось упаковать всё в образы для Docker. Тут тоже был прикол. Сервисы использовали ServerGC, но в .net был баг, из-за которого в контейнерах сборщик мусора вообще не вызывался, что приводило к перезагрузке. Впервые мы столкнулись с этим когда завернули в образ один из вспомогательных сервисов и запустили его в k8s. Конечно, мы были не первые, кто столкнулся с этой проблемой. В этой статье есть подробности.
Для вспомогательного сервиса мы просто перешли на WorkstationGC. Он крутится на отдельной машине, в принципе потребляет немного ресурсов и никак не влияет на игроков. Но с Game Server всё сложнее: там лишние вызовы сборщика могли сказаться на пользовательском опыте.
Нам повезло: к тому моменту, когда мы были готовы развернуть новые сервисы на проде, вышла стабильная версия .net 6, в которой баг был пофикшен. Поэтому мы просто переделали базовые образы, провели несколько плейтестов и стали ждать релиз.
Релиз
Вкратце: всё прошло гладко. Первое время мы включали новый транспорт на время рабочего дня и переводили на старые рельсы на ночь, чтобы иметь возможность оперативно отреагировать в случае «пожара». И уже спустя пару недель можно было подвести итоги и сравнить текущие графики с историческими.
Количество неудачных подключений к API Server снизилось:
Было:
Стало:
Основная метрика, по которой мы отслеживаем влияние сервера на качество пользовательского опыта, тоже улучшилась. Сразу оговорюсь, что непосредственно транспорт на неё не влияет, т. к. она показывает время прохождения команды, содержащей позицию робота, через сервер. Тем не менее, бонус приятный.
.999 квантиль до:
.999 квантиль после:
.9999 квантиль до:
.9999 квантиль после:
CCU до:
CCU после:
Надо признать, что сравнение не очень честное, ведь конфигурация машин изменилась: частота выросла с 3.6 ГГц до 4 ГГц, хотя и количество фичей (читай: число RPC) тоже возросло.
Количество RPC до:
Количество RPC после:
Также заметно снизилось время подключения к бою:
Вместо заключения
Все указанные улучшения производительности не были самоцелью. Странно было бы ожидать кардинальных изменений: всё-таки основными задачами были уход от Windows, возможность использовать новые версии .net, отказ от лицензий.
Photon как сетевая библиотека — прекрасное решение даже для высоконагруженных игр, а если использовать все предоставляемые разработчиками фичи — то и, считай, незаменимое. Но такие комплексные фреймворки всегда тянут за собой оверхэд в том или ином виде, и на определенном этапе развития проекта можно выиграть, отказавшись от них в пользу собственных или просто более узкоспециализированных решений.