Всем привет, меня зовут Алексей Капустин, я старший программист в Allods Team. В этой статье я расскажу о серверной архитектуре Warface — как она устроена изнутри, как мы пришли к кроссплатформенному мультиплееру, про метагейм, масштабирование и многое другое.
Устройство нашей архитектуры
Вот как устроена наша архитектура. Есть клиент игры, запущенный на одной из платформ. После запуска он идет к сервису выбора Realm, тот отдает ему список Ejabberd кластеров. После этого пользователь выбирает, к какому кластеру подключиться. Сейчас на проде мы отключили эту возможность, так как все свели в один Realm. Но для целей разработки удобно использовать список Realms, так как после запуска клиента мы можем выбрать, к какому стенду подключиться. На Xbox все устроено чуть сложнее, но об этом расскажу ближе к концу.
Мы используем Jabber для общения с нашим метагейм бэкендом, для статусов, для чатов — в целом для всего, в чем он хорош. Но у Jabber есть проблемы с безопасностью, особенно в неактуальных версиях. В частности, он не слишком устойчив к атакам Man-in-the-Middle. Поэтому мы используем запатченный Jabber. Jabber написан на Erlang — найти на рынке шарписта со знанием этого языка достаточно сложно.
Но не все общение с серверами происходит через Jabber — это было бы слишком медленно. В игровой сессии происходит общение с Dedicated сервером (он же дедик). Дедики запускаются по всему миру, чтобы расстояние между ними и игроками было меньше — для уменьшения пинга, для создания приятного игрового опыта.
Дедик — это специальная версия игры, собранная с определенными особенностями. Например, отключен рендер, так как на сервере он не нужен. При помощи дедика игроки узнают актуальную информацию о состоянии игрового мира, передвижении других игроков, разрушении объектов и так далее. Так как читеры могут вносить модификации в клиент игры, дедик выступает в качестве истины в последней инстанции при просчете игровой логики.
Предположим, у игрока пинг около 100 миллисекунд — это стандартное значение. Запрос уходит на дедик, возвращается, и в результате проходит 200 миллисекунд (время на просчет опустим). Спустя 200 миллисекунд можно отобразить информацию о совершенном действии. Это долго — игра будет выглядеть лагающей, игроки будут ругаться. Поэтому мы используем механизмы компенсации задержки.
Но не все так просто. В современных онлайн-играх часто можно увидеть, как персонажи телепортируются, выстрелы не регистрируются, предметы игрового мира ведут себя странно. Это происходит из-за рассинхронизации клиента и дедика, из-за несовершенства механизмов компенсации задержки.
Мы используем один дедик на одну игровую сессию. Пробовали переиспользовать на много сессий, но столкнулись с проблемами, что игровой мир после завершения сессии не до конца очищает свой стейт и частично сохраняет его между сессиями. А это нам не нужно. И несмотря на оверхед на запуск и завершение работы сервера, мы решили запускать один дедик на одну игру.
Но вернемся к метагейму. После того, как сообщение пришло на Jabber, оно уходит на наш Realm, где считается метагейм-логика, логика обсчета.
Рассмотрим архитектуру Realm. У нас есть сервера метагейм-логики, матчмейкинга, набор web-сервисов и специфических консольных сервисов. Сервисы между собой общаются через Rabbit, база данных — mysql, с кэшем Memcached.
Особенность геймдева в том, что количество запросов на чтение и запись зачастую сравнимо. Использование кэша в таких условиях вызывает вопросы, поэтому стараемся использовать кэш только на запросы с очевидным преобладанием чтений. Например, для чтения игровых ресурсов, параметров из базы данных. Для управления сервисами используем самописную систему, которая умеет запускать, поднимать, деплоить, мониторить сервисы и так далее.
Наши сервера метагейм-логики написаны на C#, .NET 5. Сервера матчмейкинга — аналогично. Для более мелких сервисов используем Python и Go.
Для разработки новых сервисов используем следующую идеологию. Если знаем, что сервис получится большим, с большим количеством логики (более 1-1,5 тысяч строчек кода), то мы используем C#. Если знаем, что сервис будет маленький (на несколько сотен строчек кода), используем Golang.
На изображении выше зеленым выделены блоки, которые мы умеем масштабировать. Рассказывать про масштабирование Jabber не так интересно, потому что он умеет масштабироваться из коробки. Поэтому сфокусируюсь на масштабировании серверов метагейм-логики и матчмейкинга.
Мы распределяем игроков между серверами на основании режима игры, уровня, загруженности серверов. Игрок приходит на наш Jabber, он смотрит на список серверов, подходящих этому пользователю, определяет наименее загруженный и отправляет его туда. Игрок при этом может находиться в состоянии подключения только к одному такому серверу.
Сейчас есть три основные уровневые группы игроков: новички, средние и профессионалы. И пользователи с разных уровневых групп не могут играть между собой. Иногда это создает неприятные казусы. Например, игроки 25 и 26 уровня не могут играть вместе, а игроки 26 и 27 уровня могут без проблем. То есть мы запускаем много инстансов таких серверов.
В целом для игрока характерна регулярная смена сервера. При разработке важно это учитывать, чтобы избегать багов и неприятных «гонок». Например, сервер выполняет тяжелую нетранзакционную операцию, состоящую из нескольких этапов. В середине операции игрок переключается на другой сервер. В результате операция продолжает выполняться на старом сервере, а игрок уже находится на другом.
Из этого можно сделать вывод, что по возможности не надо совершать нетранзакционные операции. Но если это необходимо, не жалейте оверхеда на дополнительную валидацию. Это позволит избежать неприятных, редких и трудноотлавливаемых багов.
Рассмотрим матчмейкинг
Мы используем один сервер под каждую уровневую группу — аналогично мастер-серверам. Но в отличие от них мы не умеем горизонтально масштабировать матчмейкинг — у нас по одному сервису на каждую уровневую группу.
Ранее матчмейкинг был написан на Python. Но это создавало проблемы с производительностью, особенно в часы пик, поэтому мы переписали на C#. Это ускорило матчмейкинг, уменьшило кодовую базу, а именно путем образования общей кодовой базы с мастер-серверами.
Также мы научились определять твинков игроков — пользователи иногда создают новые аккаунты и играют с новичками, что негативно сказывается на их опыте. Мы таких игроков распознаем, и если позволяет онлайн, ссылаем их в отдельную очередь. Если же условия не позволяют это сделать (например, ночью, когда игроков меньше), то мы отправляем их к новичкам. Мы поступаем так, потому что стараемся соблюдать баланс между временем сбора матча и качеством опыта игры. Никто не будет ждать 10-15 минут, пока матч соберется.
Также мы масштабируем матчмейкинг путем гейм-дизайна. Недавно Warface праздновал 10 лет, в честь этого мы запустили внутриигровой ивент: новый режим, карту, подарки и так далее. Под этот ивент мы сделали отдельный матчмейкинг — отдельную очередь, куда идут игроки. Это снижает нагрузку основных сервисов. Также мы используем отдельные инстансы матчмейкинга под различные сезонные режимы игры, например, под ранкед. Это дешевле, чем переписывать архитектуру, и незаметно для игроков.
Общие особенности
Realm обладает высокой отказоустойчивостью. Если начнут падать сервера метагейм-логики, то игроков будет переключать на другие сервисы. Если откажут некоторые другие сервисы, то игрок скорее всего этого не заметит.
Также у Realm достаточно высокая нагрузкоустойчивость. То есть мы можем запускать много серверов нашей метагейм-логики, не испытывая трудностей. Однако есть несколько точек отказа — если падает Jabber и база, то у нас ничего не работает.
У проекта большая кодовая база. Только серверная часть занимает более 400 тысяч строчек кода. Проекты для всех платформ мы собираем из одного большого монорепозитория.
Особенности работы с консолями
В рамках разработки сервисов нашей метагейм-логики, а точнее их интерфейсов, мы руководствуемся следующим подходом: метод должен отвечать, что он делает, а не как.
Например, у метода получения баланса на различных платформах будет разная имплементация — на ПК одна, на PlayStation вторая, на Xbox третья. Но сама идея и возвращаемый результат будет одинаковым. Поэтому на старте сервера мы определяем набор запускаемых сервисов и подставляем конкретную имплементацию. В этом помогает библиотека Ninject — удобный Dependency Injector.
Но так было до 2021 года — после мы решили объединить все консольные сервера в один большой, чтобы у игроков был кроссплатформенный мультиплеер. При разработке консольного сервиса руководствовались аналогичными принципами.
Еще у нас есть несколько консольных сервисов, которые не слишком вписываются в логику общего метагейма. Поэтому они сделаны отдельно. Рассмотрим парочку из них.
Microsoft в требованиях сертификации игр указывает, что для общения с нашим сервером должна использоваться исключительно их имплементация web-сокетов. А для других платформ мы используем TCP. Но все это было для нас неудобно — мы не хотели переписывать сервер, делать костыли, поэтому разработали отдельный сервис, к которому подключаются игроки с Xbox. Этот сервис перекладывает данные с web-сокет коннекшена в обычный TCP-коннекшен. И данные с TCP-коннекшена текут на Ejabberd, а дальше все происходит как на других платформах.
Для клиентской библиотеки мы используем Gloox. Из коробки он не умеет работать с Microsoft-сокетами. Нам пришлось его пропатчить, чтобы он мог это делать.
Такие специфические платформенные сервисы встречаются редко. Обычно мы стараемся делать универсальный сервис под все платформы, даже несмотря на их особенности. К примеру, у нас есть сервис «Кошелек» — он отвечает за взаимодействие с консольными сторами. То есть он получает список паков, умеет их продавать, отвечает за рефанды. Кстати, рефанды есть на всех платформах, даже несмотря на то, что платформодержатели и документация заявляют обратное — например, возвраты зачастую оформляет служба поддержки.
С API сторов иногда возникают проблемы, несмотря на то, что ими занимаются крупные компании (Microsoft, Sony, Nintendo). Не так давно была история, когда API одной из платформ сломалась — после определенной последовательности действий у игроков возникали безлимитные деньги. Пользователи обнаружили это и накупили предметов на много миллиардов внутриигровой валюты. Все это происходило глубокой ночью, поэтому мы не сразу заметили. Потом мы с трудом искали этих игроков и отнимали купленное. Но это стало для нас уроком. И теперь мы не так слепо верим таким API — если подобное повторится, то мы будем готовы.
Масштабировать можно не только путем горизонтального или вертикального увеличения мощностей, но и через гейм-дизайн — это зачастую дешевле, чем переписывать архитектуру.
Также стоит стремиться к универсальным консольным сервисам, так как городить каждый сервис под каждую платформу достаточно тяжело.
И по возможности не переизобретайте кубер. Мы намучались с нашей системой управления сервисами, тратим много ресурсов на поддержку. И в дальнейшем хотим отказаться от этого и перейти на что-то более современное.
Автор:
Allods_Team