Привет! Меня зовут Андрей Михеев, я занимаюсь развитием бэкенда War Robots (это мобильный PvP-шутер, в котором с помощью больших роботов можно выяснить, кто круче). Игре уже почти 9 лет, и за это время мы повидали всякого.
Круто, когда у вас в команде есть большой опыт в разработке конкретной задачи, архитектура выверена, библиотеки и фреймворки отлажены. Но что делать, если опыта не хватает, готовых решений нет, проект — потенциальный highload, а запуститься желательно было бы уже вчера? Мы как раз оказались в такой ситуации. Об этом и поговорим — а заодно о выводах, которые тут можно сделать.
Для начала, что такое игровой бэкенд? Если просто, это набор сервисов, которые обеспечивают работоспособность игры. А их много, и условно их можно разделить на две большие части.
-
Кор-часть — это все, что касается активного геймплея, то есть игровой мир, в котором перемещаются игроки, стреляют друг в друга, что-то добывают, выполняют какие-то задания.
-
Мета-часть — это все, что обслуживает игру. Эти сервисы занимаются хранением информации об игроке, его вещах, каких-то там танках, роботах, мечах — обо всем, что у него есть. Они занимаются работой с деньгами. И здесь также очень важна социальная составляющая. Сервисы, которые обеспечивают работу чатов, кланов, лиг и прочие активности игроков.
Мы начинали проект в 2013 году. На чем тогда можно было сделать игру? В принципе, если у вас медленный геймплей — условно, шашки, шахматы или какая-нибудь пошаговая стратегия, выбор технологий огромен. Во многих случаях вам подойдет любая технология, которая позволит написать классический веб-сервис поверх HTTP протокола, который обменивается json-ками. Однако если мы говорим про шутер, то это, как правило, свой бинарный протокол, повышенные требования к производительности — и тут выбор резко сужается. На тот момент классическим вариантом для построения бэкенда были Java или C# на бэкенде для написания сервисов, плюс какая-то реляционная база данных для хранения информации.
Когда вы начинаете проект, то не знаете, «выстрелит» он и станет ли популярным, так что на старте хочется сэкономить. Поэтому многие на этом этапе выбирают облачное решение Photon — это такие готовые сервисы, в которых не надо писать код, они предоставляют какую-то функциональность для вашего бэкенда. На 2013 год классический стек для старта игры был таким: Unity в качестве игрового движка, Java или C# в качестве платформы разработки бэкенда, Photon для каких-то частей игрового сервиса и Postgres или MySQL для хранения информации. Но мы выбрали Cassandra.
C «шарпом» и «джавой» все понятно, — это популярные технологии, индустрия по ним накопила огромнейший опыт — но почему «кассандра»? Во-первых, это полностью децентрализованное решение. То есть там нет какой-то мастер-ноды или мастер-сервера, проблема с которым может сказаться на всем кластере. Cassandra дает повышенную надежность и масштабируемость. Особенно отлично все с последним: Cassandra может масштабироваться практически линейно, можно добавлять ноды и даже версию «кассандры» можно обновить, не останавливая кластер. Компания Netflix проводила эксперимент: они масштабировали кластер Cassandra от одной ноды до более чем 300 — и на всем протяжении эксперимента получили практически линейный рост производительности.
К плюсам «кассандры» также можно отнести то, что она написана на Java — а по этой технологии, я уже говорил, накоплен огромный опыт. То есть, это удобно как девопсам, так и разработчикам: понятно, как запускать, как тюнить, JVM, логи, ошибки и все прочее. Еще к плюсам «кассандры» можно отнести то, что это опенсорс, то есть Java-разработчик всегда может залезть в код и уточнить какие-то нюансы, это часто бывает полезно. Но Cassandra — конечно же, не идеальное решение, у нее тоже есть свои минусы.
Первое, с чем сталкивается разработчик, использующий Cassandra — там свой собственный язык запросов CQL. Да, он похож на SQL внешне, и это создает ложное ощущение, что вы уже умеете строить запросы, но нет. Придется потратить время на изучение нового языка, пусть даже он не очень обширный. Второй момент, который хочется выделить: «кассандра» не предназначена для каких-то сложных выборок, то есть, если есть большое количество связей между таблицами, такой вариант не подойдет. Еще у «кассандры» нет полноценных транзакций, нет полноценной поддержки ACID. Да, там есть какие-то свои гарантии, но это не то, к чему мы привыкли в классических базах типа Postgres. И не забудьте, что Cassandra написана на Java — то есть имеет все минусы Java-приложений.
На старте проекта нам хотелось запуститься побыстрее и не тратить много ресурсов, поэтому мы выбрали облачное решение Photon для кор-части, которая обеспечивает геймплей, а мета-часть решили писать сами. Из мета части на старте нужен был только сервис профиля игрока. Для этого мы выбрали Java — долго думать тут не пришлось, потому что в команде уже были джависты. Разработкой сервера тогда занимались всего два человека.
Пару слов хочется сказать о том, почему мы выбрали Photon. Это полностью готовое облачное решение, которое не требует разработки, соответственно оно дает максимально быстрый старт в проекте. Нам от него, конечно, нужно было не все: мы использовали комнаты для объединения игроков и синхронизации состояния между ними, обмен сообщениями между пользователями. Photon, как и любой полностью готовый облачный сервис, дает какое-то определенное количество боли и в чем-то ограничивает. Для нас самым неудобным было то, что невозможно написать свою сложную бизнес-логику для комнат «фотона». То есть мы не могли загрузить туда какой-то код, который будет выполняться в комнате, и, как следствие, не могли обеспечить полностью честную игру — для этого нужен авторитарный сервер, который занимается обсчетом игровой ситуации.
На старте архитектура выглядела минималистично и просто: наш сервис профилей, который занимался хранением информации об игроках, и Photon-сервер для боев. Клиент на тот момент общался поверх HTTP обычными xml-ками, а запускали профиль вместе с нодами «кассандры» на сервере, и это простое решение отлично работало. Внимательные читатели сейчас, наверное, уже заметили, что я сказал «фотон не позволяет написать свою игровую логику», профиль для этого вообще не предназначен, а на схеме выше отсутствует сервис, который обсчитывает игровую логику. Возникает резонный вопрос: а где же она тогда? Кто занимается ее обсчетом? Здесь нужен какой-то третий участник, кто бы это все делал.
Придумать тут можно разное, но мы выбрали один из популярных вариантов — мастер-клиент. В этом подходе один из клиентов во время игры выполняет часть функций сервера для других участников. Он занимается определением начала и окончания матча, занимается определением точек спавна (это места, где появляются игроки на карте), а также определяет победу и поражение команд. И он же занимается матчмейкингом.
Матчмейкинг — опытные игроки точно знают — это просто процесс набора игроков в команды. Наверняка многие из вас помнят: в школе учитель вызывает двух учеников, которые становятся капитанами, и вот они по очереди набирают себе игроков в команды по разным критериям: самый быстрый, самый ловкий и так далее. Как и в жизни, в игре критерии отбора могут быть самые разные, вплоть до каких-то вариантов машинного обучения, которое сможет подобрать идеальных партнеров в игру, чтобы было интересно, не сильно легко и не сильно сложно.
Как вообще может работать матчмейкинг на клиенте, ведь это же, по логике, тема исключительно для сервера? Очень просто. Первый игрок, который попадает на сервер, становится мастер-клиентом и начинает набирать к себе приходящих игроков. Как только он набрал команды, выбирает самого старого клиента на сервере, передает ему эстафету матчмейкинга, а потом вместе с командами уходит в бой и там выполняет функции сервера. Эта история повторяется для следующего боя.
Кажется, что такая схема идеальна: мы можем круто сэкономить на серверах, перекладывая часть работы на клиента. На тот момент у нас могло быть порядка 12 игроков на одной карте, поэтому нагрузка на клиента была небольшой. И вроде бы всем удобно, но, на самом деле, нет. Проблем в этой схеме хватает. Первое — из-за того, что у нас не два участника (клиент и сервер), а три, возникали проблемы с синхронизацией стейта игроков. Например, в игре есть такой режим, в котором по карте разбросаны флаги, а игроки должны их захватывать и удерживать. Чья команда удерживает большее количество флагов, та и победила. И были такие проблемы, что разные игроки внутри одного матча наблюдали разное состояние флагов.
Также в теме шутеров невозможно не сказать про стрельбу. Это вообще критически важный момент, а у нас тогда попадание регистрировал сам клиент. Например, клиент шлет сообщения серверу: «Эй, запиши, такой-то игрок нанес мне 3000 урона». Сервер такой, угу, записал. При этом каждый клиент, естественно, вел свой собственный лог боя, куда записывал все, что с ним происходит.
В конце игры они скидывали это профилю, профиль собирал информацию со всех игроков и решал, кому что начислить, кому награды какие выдать и так далее. Понимаете ситуацию, да? Все джентльмены и верят на слово. Этот подход порождал много багов: то флаги не захватываются, то убить кого-то нельзя, то что-то не начислили.
Тут стоит упомянуть еще тот факт, что сеть в мобильных играх не особо-то и надежна. Да, мобильный интернет постоянно улучшается, у нас есть 4G, 5G, но история мобильного интернета — это, по сути, история боли, то есть там и проблемы с пингами, и с потерями какими-то.
Сюда нужно еще добавить, что и игроки тоже ненадежны — никто не может заставить игрока доиграть бой. Он может в любой момент психануть, закрыть игру, вообще ее удалить в два клика. Или заехать в какой-нибудь туннель, где связь прервется. Или у него просто телефон разрядится. Вот представьте себе, классическая ситуация: игрок заходит в игру, становится мастер-клиентом, набирает себе команду, уходит в бой… и тут ему звонят. Он, естественно, берет трубку, игра сворачивается со всеми вытекающими проблемами для остальных участников. Кажется, что такая система вообще не может работать, слишком уж много проблем. Однако же она работала, причем неплохо, саппорты тех времён говорили, что было даже терпимо.
Уверен, сейчас многие из вас подумали: да, конечно, все мы понимаем, набрали джунов по объявлениям, а они там на коленке по туториалам из интернетов что-то понаделали — чего тут еще ждать. Но нет, поверьте, глупые люди у нас не работают, все было просчитано заранее. В тот момент нам нужны были максимально простые и быстрые решения. Ведь никто не знал, что игра не то, что станет хитом, а вообще наберет хоть какую-нибудь популярность.
В игровых проектах очень большая конкуренция, а игроки довольно требовательные. Здесь важно уметь быстро двигаться, быстро находить свою нишу, свои механики. Быстрая проверка гипотез — наверное, ключевой момент, который двигал нашими решениями в тот момент. Важно уметь не только быстро двигаться, важно правильно выбирать направление и уметь быстро его поменять, если вы ошиблись. Нужно было нащупать свою аудиторию со своими игровыми механиками.
На тот момент у нас было порядка 15-20 тысяч игроков, и стали появляться проблемы с матчмейкингом. По идее, любой человек, который давно занимается разработкой, скажет: вот у вас есть проблема, нужно садиться ее решать, разрабатывать архитектуру, составлять план работ. А если компания большая, то нужно собирать какой-нибудь архитектурный комитет, чтоб сначала выбрать, что мы вообще решаем, и так далее.
Только вот у нас на все это не было времени. Вылезла проблема с матчмейкингом — ее надо решать оперативно. Мы не стали выдумывать какие-то сложные схемы. Ведь Unity позволяет запустить клиент не только на мобилках, но и на десктопе. Так что мы взяли Windows-клиент нашей игры, запустили его на офисном компьютере и запретили ему входить в бой.
В результате этот клиент занимался исключительно матчмейкингом: он очень быстро становился самым старым игроком на сервере, у него идеальное подключение, проводное, с минимальным пингом, он никогда не ливнет из игры, и в целом, все хорошо. И это решение отлично работало полгода! А когда нам понадобилось его масштабировать, мы просто запустили несколько таких клиентов на офисных машинах.
Время шло, игра набирала популярность, на тот момент в ней было уже около около 300 тысяч человек. Тогда мы ещё получили фичеринг от Google, запустили маркетинговую компании и получили, в итоге, кратный прирост игроков. Конечно, проявились проблемы с нашей архитектурой и с читерами (в любом популярном проекте очень много желающих использовать нечестные приемы, с этим тоже нужно что-то делать).
Больше о том, как мы боремся с читерами, можно прочесть в этом материале.
Большой проблемой в тот момент стал дата-центр: появились проблемы с железом и сетью. Стало понятно, что нужно куда-то перемещаться. Тогда у нас состоялся первый переезд между дата-центрами (всего их будет два) — и нам очень сильно помогла Cassandra со своей децентрализацией. Конечно, тема переезда довольно обширна, но давайте пару слов о том, как нам удалось это сделать без простоев.
Мы запустили новый бэкенд в новом дата-центре, запустили там новое кольцо «кассандры» (кольцом называют кластер в одном дата-центре). Синхронизировали старое и новое кольцо, так что у нас «кассандра» стала единым кластером, и переключили на старом бэкенде серверы в режим, когда там новые игры не создаются. Получилось, что игроки доигрывали на старом бэкенде уже запущенные бои, а новые бои уже запускались на новом бэкенде. Так мы плавно переехали из одного дата-центра в другой, а пользователи даже не заметили, что в процессе игры они «переехали» из одной страны в другую.
Первый переезд прошел гладко, чего нельзя сказать про второй (об этом чуть позже). И здесь же стало понятно, что Photon Cloud не позволяет нам развивать проект так, как хотелось бы, а потому нужно разрабатывать свое собственное игровое решение. Первое, о чем мы подумали — конечно же, Unity. Он позволяет запустить сервер, максимально переиспользуя код игры. Но на тот момент он был не очень хорош, у него были проблемы с производительностью, память текла, его нужно было регулярно перезапускать. Мы обратили внимание на Photon SDK.
У «фотона», помимо облачных сервисов, есть свой SDK, с помощью которого можно разработать свое игровое решение и запустить его на своих серверах, получив авторитарный сервер. Весь SDK нам был не нужен, так что мы взяли какие-то отдельные части и разработали свой игровой сервер. Так у нас получилась вторая версия архитектуры, в которой помимо профиля появился core-сервер, обсчитывающий игровую логику. Мастер-сервер здесь занимается балансировкой, а гейм-сервисы просчитывают саму игру.
Все стало намного лучше, но была одна старая проблема: игрок все еще регистрировал попадание по себе сам. Учитывая большое количество игроков и наличие читеров, стало понятно, что эту проблему надо решать очень быстро. Здесь мы тоже не стали изобретать грандиозных решений, потому что для полноценного сервера, который позволил бы обсчитывать стрельбу, пришлось бы так же переписывать половину клиента.
Мы придумали другое решение, что-то среднее между авторитарным сервером и расчетом урона на клиенте, и изменили всего одну вещь в нашей схеме: теперь игрок регистрирует попадание не сам по себе. Вместо этого все игроки в комнате регистрируют попадание по всем другим игрокам в комнате и отсылают эту информацию на сервер. У того появляется кворум-решение от всех участников, и они, соответственно, уже не могут так просто читерить и говорить, что по ним нанесен какой-то там мифический урон или не нанесен вообще.
Глядя на график, можно заметить, как после этого снизилось количество жалоб на читеров. Там был еще небольшой хотфикс с неубивашками, когда игроки с помощью манипуляций сетевыми пакетами добивались того, что в какой-то момент их нельзя было убить, но в итоге все было исправлено.
В любом игровом проекте (да и в любом развивающемся проекте в принципе), важно помнить про про мониторинг — нужно всегда знать, что происходит с игрой, что делают игроки. Тут мы подложили себе соломку под названием AppMetr — это наш собственный проект, который собирает игровую аналитику и построен на Cassandra. Сейчас в War Robots за сутки набирается порядка одного миллиарда событий: это и бизнес-события, и технические события, и от игровой логики.
Игра продолжала развиваться, и стало понятно, что от нашего решения на базе Photon SDK нужно отказываться: были проблемы с производительностью, плохо использовалось железо, были проблемы с многопоточкой. Тогда мы решили разработать все сервисы, которые нам нужны, полностью с нуля сами. Для гейм-сервера мы выбрали Akka.Net.
Затронув сервисную архитектуру и бурно развивающийся проект, невозможно не затронуть тему CICD. Конечно же, игрокам не нравится, когда они заходят в игру а там табличка: «Подождите. Мы находимся на обслуживании». Это не нравится и бизнесу, потому что простои — это прямые убытки.
Ключевым моментом релизов без простоев является обратная совместимость API, кода и конфигураций, потому что нельзя в один момент зарелизить и сервер, и клиент. Всегда будет какой-то лаг между ними — и речь даже не про то, что в AppStore и Google Play невозможно «быстренько» выкатить релиз, зачастую это все занимает дни. Я скорее о том, что вот у вас 200 миллионов игроков: как быстро они обновят свои клиенты, как быстро все это произойдет? Так что сервер должен уметь работать как со старой версией клиента, так и с новой.
Как мы этого добиваемся? Да, у нас есть план и мы его придерживаемся. Не просто план релизов в целом, а план на каждую версию релиза, куда мы заносим все, что надо сделать — изменить конфиг, настройку, запустить какой-то скрипт, что-то еще. У каждого плана каждой версии есть свой ответственный за релиз, который занимается только этим. И мы также следим за чистотой кода, постоянно удаляя старые ненужные части: прямо в коде ставим аннотацию deprecated, указывая код, который нужно удалить, и ссылку на задачу в Jira. Задача попадает в план, и там с ней уже разбираются.
Также мы серьезно продумываем миграцию данных в этом процессе, и у нас есть несколько уровней тестирования: юнит-тест, автоматические тесты, ручное тестирование. В ручном мы проверяем работу и со старыми, и с новыми версиями. Во всем этом мониторинг ошибок играет очень важную роль.
В итоге всех переездов и разработок мы получили архитектуру 3.0. Изменений здесь не так много. Появились новые необходимые нам сервисы, которые обеспечивают работу игры. Появился PostgreSQL — для некоторых наших сервисов он подходит гораздо лучше: у них сложные связи между объектами, нужно строить сложные запросы, да и данных там намного меньше, чем в Cassandra.
Разумеется, были и факапы. Первое, что мы сделали — за DDoSили сами себя. Выкатили новый релиз, он делал довольно жирненькие запросы бэкенду, а тот не выдержал. Ну и, конечно же, всё это происходило по-классике. Пятничный релиз, вечером у нас корпоратив, мы там буквально разливаем шампанское, и мониторинг нам такой: «Ребята, а вы не хотите посмотреть, у вас там сервера складываются». Выводом из этого стало то, что у нас появилось такое понятие, как фича-флаг.
Новая фича разрабатывается с учетом того, что у нас есть рубильник, которым мы можем ее включить и выключить. Раскатывается она в релизе в выключенном состоянии, мы проверяем, что все по-старому хорошо работает, а потом включаем ее. Причем у нас есть диммер, мы можем включить фичу на 10, 15, 20 процентов игроков. А когда видим, что все хорошо, то запускаем уже для всех.
Еще один инцидент у нас случился в 2018 году во время второго переезда в дата-центр. Здесь уже не обошлось без проблем, были ошибки в конфигурировании, что привело к потере части данных. Тогда нам пришлось восстанавливаться из бэкапа. После этого мы стали гораздо тщательнее прорабатывать схемы миграции данных.
Подробнее о том, как мы переезжали в новый дата-центр — вот тут.
Сейчас у нас больше 170 серверов, разбросанных по всему миру, больше 200 миллионов игроков, три с половиной миллиона игроков играют каждый месяц, а полмиллиона — каждый день. В кластере «кассандры» больше 80 терабайт данных на 65 нодах, и мы обеспечиваем 45 релизов в год без простоев. В команде серверной разработки сейчас порядка 10 человек.
Архитектура на данный момент выглядит вот так:
Внешне изменения не особо большие, все самое важное произошло внутри профиля — мы его сильно перефакторили и теперь разрабатываем его с применением DDD, CQRS, Event Sourcing. И вот вроде тут можно было бы остановиться и сосредоточиться на развитии только игровых фич, но мы наметили для себя определенные точки роста в плане архитектуры.
Первое: из-за того, что в Cassandra нет транзакций, у нас происходит много пессимистичных блокировок. Это тормозит систему, хотелось бы облегчить и улучшить. Также у нас много межсервисного взаимодействия, и хотя мы над ним постоянно работаем (вначале это был XML, сейчас у нас MessagePack), здесь все равно еще есть что улучшать: например, мы переходим на gRPC и хотим оптимизировать количество межсервисных взаимодействий. Также внутри профиля (а это самый нагруженный сервис) мы хотим перейти на akka-кластер: это позволит уменьшить время вызовов, приблизить данные к непосредственно вызывающему коду.
Также мы оптимизируем игровой протокол. Вначале у нас были довольно жирные запросы между клиентом и сервером, а сейчас мы переходим к ситуации, когда состояние между клиентом и сервером синхронизируется только изменениями между ними, небольшими сжатыми дельтами — это очень серьезно оптимизирует работу с сетью. Плюс, мы перевозим часть логики с клиента на сервер, как это сейчас модно говорить, «для улучшения игрового опыта».
На картинке ниже — архитектура, к которой мы стремимся. Архитектура 5.0, как мы ее условно называем.
Главное, что изменилось в центральной части — появилась Kafka. Для межсервисного общения появился akka-кластер на профиле, а протокол заменяется на gRPC. Других каких-то принципиальных отличий от предыдущей версии здесь нет, улучшения в основном внутренние.
Какие выводы можно сделать из нашего опыта? Первый и самый банальный: вы не Google. Вовсе не обязательно строить свой проект с самого начала с расчетом на то, что завтра с утра к вам придет миллиард игроков, а вы будете к этому готовы. Не придет. Хотя, с другой стороны, если вы Google и знаете, что делаете, игнорируйте это утверждение.
Второй важный вывод из нашего опыта переездов и развития бекэнда: сложнее всего менять данные. С кодом вы достаточно просто можете что-то пофиксить, что-то улучшить, можете даже предыдущую версию выкатить в некоторых случаях, если что-то пошло не так. С данными так быстро не получится: если вы зафакапите их, это практически гарантирует простои. Не говоря уже о том, что всегда есть шанс потерять данные совсем. И даже если у вас есть бэкапы… представьте, вот у вас 100 ТБ бэкапа. Как быстро он накатится на ваш кластер, сколько времени пройдет, сколько будут занимать простои? Как следствие, всегда нужно очень тщательно перепроверять конфиги, схемы, миграции.
Ну и, конечно же, не существует каких-то абсолютно верных универсальных решений, которые вы можете просто брать и сразу применять не задумываясь. Всегда нужно переосмысливать для вашей ситуации то, что вы читаете, не всегда стоит опираться на какие-то готовые решения из учебников. Мы в своей работе всегда стараемся исходить из наших конкретных задач и потребностей.
А впрочем, вы можете не использовать наш опыт, а выбрать свой путь, набить свои шишки и собрать свои грабли. Это, конечно, порой бывает болезненно, но зато очень интересно. Удачи!