Главная » Хабрахабр » Архитектура мета-сервера мобильного онлайн-шутера Tacticool

Архитектура мета-сервера мобильного онлайн-шутера Tacticool

Еще один доклад с Pixonic DevGAMM Talks — на этот раз от наших коллег из PanzerDog. Lead Software Engineer компании Павел Платто разобрал мета-сервер игры с сервисно-ориентированной архитектурой, рассказал, какие решения и технологии были выбраны, что и как у них масштабируется, и с какими трудностями пришлось столкнуться. Текст доклада, слайды и ссылки на другие выступления с митапа, как всегда, под катом.

Для начала хочу продемонстрировать небольшой трейлер нашей игры:

Доклад будет состоять из 3-х частей. В первой я расскажу о том, какие технологии мы выбрали и почему, во второй — о том, как устроен наш мета-сервер, а в третей расскажу о различной вспомогательной инфраструктуре, которую мы используем, и о том, как мы реализовали обновление без даунтайма.


Технологический стек

Это функциональный язык программирования с акторной моделью вычислений. Мета-сервер хостится на Amazon и написан на языке Elixir. Так как у нас нет Ops'ов, оперированием занимаются программисты, и большая часть инфраструктуры описана в виде кода с помощью Terraform от HashiCorp.

Давайте посмотрим, с чего все начиналось. На данный момент Tacticool находится на стадии открытого бета-теста, мета-сервер находится в разработке чуть больше года и в эксплуатации почти год.

Данная реализация имела определенные проблемы. Когда я пришел в компанию, мы уже имели базовую функциональность, реализованную в виде монолита на смеси С/С++ и хранимках PostageSQL.

Например, у некоторых игроков намертво зависал матчмейкинг из-за некорректного обнуления массива перед его повторном использовании. Во-первых, из-за низкоуровневости С было довольно много трудноуловимых багов. И так как в коде повсеместно модифицировалось состояние из нескольких потоков, не обошлось без Race conditions. Разумеется, найти взаимосвязь этих двух событий было довольно сложно.

И даже если забыть об этих блокирующих запросах, сервис начинал рассыпаться на паре сотен соединений, которые не выполняли никаких операций, кроме пинга. О параллельной обработке большого количества задач тоже не могло быть и речи, потому что сервер запускал на старте около 10 процессов-воркеров, которые блокировались при запросах к Амазону или базе данных. К тому же сервис невозможно было горизонтально масштабировать.

Через пару недель, потраченных на поиск и исправление наиболее критических багов, мы решили, что проще переписать все с нуля, чем пытаться исправить все недостатки текущего решения.

У нас было три кандидата: А когда начинаешь с нуля, есть смысл попробовать подобрать язык, который поможет избежать часть предыдущих проблем.

  • C#;
  • Go;
  • Elixir.

клиент и игровой сервер у нас написаны на Unity и больше всего опыта в команде было именно с этим языком программирования. C# попал в список «по знакомству», т.к. Go и Elixir рассматривали, потому что это современные и достаточно популярные языки, созданные для разработки серверных приложений.

Проблемы предыдущей итерации помогли нам определить критерии для оценки кандидатов.

В C# удобная работа с асинхронными операциями появилась не с первой попытки. Первым критерием было удобство работы с асинхронными операциями. В Go и Elixir данная проблема была учтена при проектировании этих языков, они оба используют легковесные потоки (в Go — это горутины, в Elixir — процессы). Это привело к тому, что мы имеем «зоопарк» решений, которые, на мой взгляд, все равно стоят немного сбоку. Эти потоки имеют намного меньший оверхед, чем системные, и так как мы можем создавать их десятками и сотнями тысяч, то нам не жалко их заблокировать.

C# из коробки не предлагает ничего другого, кроме тредпулов и общей памяти, доступ к которой нужно защищать с помощью различных примитивов синхронизаций. Вторым критерием были возможности по работе с конкурентными процессами. Elixir же предлагает акторную модель без разделяемой памяти с общением посредством обмена сообщениями. Go имеет менее подверженную к ошибкам модель в виде горутин и каналов. Отсутствие разделяемой памяти позволило реализовать в рантайме такие полезные для конкурентной среды исполнения технологии, как честная вымещающая многозадачность и сборка мусора без остановок мира.

Весь мой опыт разработки показал, что довольно большая часть багов связаны с некорректным изменением данных. Третьим критерием являлась доступность инструментов для работы с неизменяемыми типами данных. В C# такие типы данных можно создавать, но ценой тонны бойлерплейта. Решение для этого существует давным-давно — неизменяемые типы данных. А в Elixir все типы данных являются неизменяемыми. В Go это вообще невозможно.

Тут результаты очевидны. И последним критерием было количество специалистов. В конечном итоге мы остановили свой выбор на Elixir.

Игровые сервера у нас уже хостились в Amazon GameLift, кроме того Amazon предлагает большое количество сервисов, которые позволили бы нам сократить время на разработку. С выбором хостинга все было значительно проще.

На мой взгляд, это единственное решение для небольшой команды, которая хочет разрабатывать онлайн-игру, а не инфраструктуру для нее. Мы полностью «сдались» облаку и не разворачиваем сами никаких сторонних решений — базы данных, очереди сообщений — всем этим за нас управляет Amazon.

С выбором технологий разобрались, перейдем к тому, как работает мета-сервер.

Но фронтенд и бэкенед общаются опосредованно, через очереди сообщений. В общих чертах: клиенты подключаются к балансировщику нагрузки в Amazon посредством веб-сокетных соединений; эти соединения балансировщик раскидывает между несколькими инстансами фронтенда, фронтенд отдает запросы клиентов бэкендам. Для каждого типа сообщений существует отдельная очередь и фронтенд по типу сообщений определяет, куда его записать, а бэкенды слушают эти очереди.

И в каждом запросе бэкенд получает идентификатор фронтенда, чтобы определить, в какую очередь нужно записать ответ. Чтобы бэкенд мог отправить на клиент ответ на запрос, либо какое-то событие, каждому фронтенду прикреплена отдельная очередь (специально для него выделенная). Если ему нужно послать событие, то он обращается к базе данных, чтобы узнать, к какому инстансу фронтенда подключен клиент.

С общей схемой всё, перейдем к деталям.

Мы используем свой бинарный протокол, потому что он достаточно эффективный и позволяет экономить трафик. Во-первых, расскажу о некоторых особенностях клиент-серверного взаимодействия. Это чуть менее эффективно, но все равно занимает не так много места и значительно упрощает нам жизнь как на клиенте, так и на сервере. Во-вторых, при любых операциях с аккаунтом, которые его изменяют, сервер отсылает на клиент не эти изменения, а полную (обновленную) версию данного аккаунта. Это позволяет отлавливать баги на клиенте, например, когда он переходит на другой экран до того, как игрок увидит результат выполнения предыдущей операции. Также фронтенд следит за тем, чтобы клиент выполнял не более одного запроса за раз.

Теперь немного о том, как устроен фронтенд.

Для каждой сессии создается по два процесса. Фронтенд это, по сути, веб-сервер, который слушает веб-сокетные соединения. На основе этого состояния он определяет допустимость запросов от клиента. Первый процесс обслуживает само веб-сокетное соединение, а второй является стейт-машиной, которая описывает текущее состояние клиента. Так как на фронтенде нет никакого состояния, кроме этих сессий, то очень легко добавлять новые инстансы фронтенда, но немного сложнее удалять старые. Например, почти все запросы нельзя выполнять до тех пор, пока не завершится авторизация. Перед удалением нужно дать всем клиентам завершить свои текущие запросы и попросить их переподключиться к другому инстансу.

На данный момент он состоит из пяти сервисов. Теперь о том, как выглядит бэкенд.

Второй работает со всем, что связано с матчами — он напрямую взаимодействует с GameLift’ом и игровыми серверами. Первых занимается всем, что связано с аккаунтами — от покупок за внутриигровую валюту до выполнения квестов. Четвертый и пятый отвечают за социальные взаимодействия — один за друзей, другой за игру в пати. Третий сервис занимается покупками за реальные деньги.

Они представляют из себя набор пайплайнов, каждый из которых обрабатывает один тип сообщений. Каждый из бэкенд-сервисов с архитектурной точки зрения выглядит абсолютно идентично. Пайплайн состоит из двух элементов: producer и consumer.

Поэтому он реализован полностью в общем виде и для каждого пайплайна нам нужно только указать, сколько есть producer’ов, из какой очереди читать и сколько consumer’ов будет обслуживать каждый producer. Единственная задача producer’а — вычитывать сообщения из очереди. Также producer реализует back pressure, чтобы при резком возрастании количества сообщений не произошло перегрузки, и запрашивает сообщений не больше, чем у него есть свободных consumer’ов. Consumer же реализуется для каждого пайплайна отдельно и представляет из себя модуль с единственной обязательной функцией, которая принимает одно сообщение, выполняет всю необходимую работу и возвращает список сообщений, которые нужно отправить в другие сервисы клиенту, либо на игровой сервер.

Единственное, что нужно сделать перед удалением, это попросить producer’ов перестать вычитывать новые сообщения и дать consumer’ам немного времени закончить обработку активных сообщений. Бэкенд-сервисы не содержат никакого состояния, поэтому нам легко добавлять и удалять старые инстансы.

GameLift состоит из нескольких составных частей. Как происходит взаимодействие с GameLift’ом. Из тех, что используем мы, это матчмейкер FlexMatch, очередь размещений, которая определяет, в каком конкретно регионе разместить игровую сессию с данными игроками, и сами флиты, состоящие из игровых серверов.

Мета напрямую общается только с матчмейкером, отсылает ему запросы на поиск матча. Как происходит это взаимодействие. И как только он находит подходящую группу игроков, чтобы начать матч, он отправляет заявку в очередь размещений, которая в свою очередь подбирает для них сервер. А он уведомляет мету обо всех событиях в ходе матчмейкинга через те же самые очереди сообщений.

Игровому серверу нужна информация об аккаунтах, ботах и карте, и всю эту информацию мета отсылает в очередь, созданную специально для этого матча в единственном сообщении. Взаимодействие меты с игровым сервером предельно простое.

В конце матча он отсылает его результаты в общую очередь, которую слушает мета. А игровой сервер при активации начинает слушать эту очередь и получает все необходимые ему данные.

Теперь перейдем к дополнительной инфраструктуре, которую мы используем.

Все они работают в docker-контейнерах, а для оркестрации мы используем Amazon ECS. Развертывание сервисов происходит достаточно просто. А именно: масштабирование сервисов и rolling-релизы, когда нам нужно залить какой-нибудь багфикс. Он значительно проще, чем Kubernetes, разумеется, менее навороченный, но те задачи, которые нам от него нужны, он выполняет.

Он избавляет нас от необходимости самостоятельно управлять кластером машин, на которых запускаются наши docker-контейнеры. И последний сервис, который мы также используем — это AWS Fargate.

В первую очередь мы выбрали ее за то, что ее очень легко эксплуатировать и масштабировать. В качестве основного хранилища мы используем DynamoDB. Его мы используем для задачи глобального рейтинга игроков и для кэширования основных данных об аккаунте в тех ситуациях, когда нам нужно вернуть на клиент данные сразу о сотнях игровых аккаунтов (например, в той же таблице рейтингов, либо в списке друзей). Также в качестве дополнительного хранилища мы используем Redis, посредством управляемого сервиса Amazon ElasiCache.

мы используем JSON-файл, который подкладываем в образы сервисов, которым он нужен. Для хранения конфигов, мета-геймплейных механик, описания оружия, героев и т.д. Потому что нам значительно проще выкатить новую версию сервиса с обновлёнными данными (если какой-то баг обнаружился), чем делать решение, которое будет динамически в рантайме обновлять эти данные из какого-то внешнего хранилища.

Для логирования и мониторинга мы используем довольно много сервисов.

Это сервис мониторинга, в который стекаются метрики со всех амазоновских сервисов. Начнем с CloudWatch. А для логирования мы используем общий подход и на клиенте и на игровом сервере и на мета-сервере. Поэтому мы решили туда же слать метрики с нашего мета-сервера. Все логи мы шлем в амазоновский сервис Kinesis Firehose, который в свою очередь перекладывает их в Elasticseach и S3.

В S3 лежат все исторические данные и используем мы их посредством сервиса Athena, который предоставляет SQL-интерфейс поверх данных в S3. В Elasticseach мы храним только относительно свежие данные и с помощью Kibana ищем ошибки, решаем часть задач игровой аналитики и строим операционные дашборды, например, с графиком CCU и количество новых установок.

Теперь немного о том, как мы используем Terraform.

Таким образом, имея единое описание, мы получаем практически идентичное окружение для staging и production. Terraform — это инструмент, который позволяет декларативно описать инфраструктуру и при каком-либо изменении описания, он автоматически определяет те действия, которые необходимо выполнить, чтобы привести вашу инфраструктуру к обновленному виду. Единственным существенным недостатком Terraform для нас является неполная поддержка GameLift. Также эти окружения полностью изолированы, потому что они разворачиваются под разными аккаунтами.

Еще расскажу о том, как мы реализовали обновление без даунтайма.

И те игроки, которые скачают новую версию игры, будут подключаться к этому обновленному кластеру. Когда мы выпускаем обновления, мы поднимаем копию большинства ресурсов: сервисов, очередей сообщений, некоторых табличек в базе данных. Но те игроки, которые еще не обновились, могут продолжать некоторое время играть на старой версии игры, подключаясь к старому кластеру.

Во-первых, с помощью механизма модулей в Terraform. Как мы это реализовали. И эти модули можно импортировать несколько раз, с разными параметрами. Мы выделили модуль, в котором описали все версионируемые ресурсы. Так же нам помогло отсутствие схемы в DynamoDB, что дает возможность выполнять миграции данных не во время апдейта, а откладывать их для каждого аккаунта до тех пор, пока его владелец не залогинится в новой версии игры. Соответственно, для каждой версии мы импортируем этот модуль, указав номер этой версии. А в балансировщике мы просто указываем для каждой версии правила, чтобы он знал, куда роутить игроков с разными версиями.

Во-первых, конфигурация всей инфраструктуры должна быть автоматизирована. Напоследок пара вещей, которым мы научились. некоторые вещи мы какое-то время настраивали руками, но рано или поздно ошибались в настройках, из-за чего случались факапы. Т.е.

И если для чего-то это не сделать, то именно эта вещь нас когда-нибудь подведет. И последнее — нужно для каждого элемента вашей инфраструктуры иметь либо реплику, либо запасную копию.

Вопросы из зала

— А вас не смущает, что автоскейлинг может заскейлиться слишком сильно вверх из-за какой-то ошибки и вы попадете на очень большие деньги?

Мы не будем ставить слишком большой лимит, чтобы не попасть на большие деньги. — Для автоскейлинга все равно выставляются лимиты. Можно выставить алерты, если что-то заскейлилось слишком сильно. Это основное решение + мониторинг.

Относительно текущей инфраструктуры в процентном соотношении. — В данный момент у вас какие лимиты?

Сейчас инфраструктура слишком overprovisioned для того количества людей, которые у нас есть. — Сейчас у нас этап открытого бета-теста в 11 странах, поэтому не такой большой CCU, чтобы хоть как-то оценивать.

— И лимитов пока нет?

Меньше не сделать. — Есть, просто они в 10-100 раз больше, чем ССU у нас.

Почему не на прямую? — Вы рассказали, что у вас очереди между фронт- и бэкендом — это очень необычно.

Также, например, когда обработчик фейлится, очередь выдаст это же самое сообщение другому обработчику — возможно у него что-то получится. — Мы хотели stateless-сервисы, чтобы легко реализовать механизм бэкпреше, чтобы сервис не запрашивал больше сообщений, чем у него есть свободных обработчиков.

— А очередь персистится как-то?

Это амазоновский сервис SQS. — Да.

У вас на каждый матч какое-то количество каналов? — По поводу очередей: сколько у вас создается каналов во время игры?

Большая часть очередей, например, очереди запросов — статические. — Создается относительно немного. Из динамически создаваемых очередей у нас есть только очереди для каждого фронтенда (он при запуске создает для входящих сообщений для клиентов) и для каждого матча мы создаем по одной очереди. Есть очередь запросов на авторизацию, есть очередь на старт матча. Т.е. В этом сервисе это почти ничего не стоит, у них любой запрос биллится одинаково. А то, что они существуют, нам ничего не стоит. любой запрос к SQS (создать очередь, прочитать из нее что-то) стоит одинаково и при этом мы для экономии не удаляем эти очереди, они сами потом удалятся.

— В данной архитектуре это для вас лимитом не будет являться?

— Нет.

Еще доклады с Pixonic DevGAMM Talks


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

[Перевод] Введение в ptrace или инъекция кода в sshd ради веселья

Конечно, это несколько искусственная задача, так как есть множество других, более эффективных, способов достичь желаемого (и с гораздо меньшей вероятностью получить SEGV), однако, мне показалось клёвым сделать именно так. Цель, которой я задался, была весьма проста: узнать введённый в sshd ...

Дайджест свежих материалов из мира фронтенда за последнюю неделю №339 (12 — 18 ноября 2018)

Предлагаем вашему вниманию подборку с ссылками на новые материалы из области фронтенда и около него.     Медиа    |    Веб-разработка    |    CSS    |    Javascript    |    Браузеры    |    Занимательное Медиа • Подкаст «Frontend Weekend» #79 – Олег Поляков об основании CodeDojo и о том, как это стало основным местом работы• Подкаст «Пятиминутка React» ...