10-11 апреля наша команда приняла участие в крупнейшей в регионах России ИТ-конференции «Стачка», которая прошла в четвертый раз в Ульяновске. ИТ-компании представили свои стенды, где можно было познакомиться с их продуктами, узнать про вакансии, принять участие в конкурсах.

В этом году XIMAD также решил представить стенд со своими продуктами на конференции в связи с развитием мобильной секции. Это профильное для нас направление. Мы прослушали доклады, обменялись опытом с коллегами и ответили на множество вопросов о технологиях, используемых в наших играх.

За два дня на 8 площадках выступили 130 экспертов и в общей сложности прозвучало 150 актуальных докладов по различным направлениям. Наш разработчик Алексей Ключников представил свой доклад на тему “Делаем интерактив в мобильных играх или как избавиться от persistent database зависимости” на примере нашего флагмана Magic Jigsaw Puzzles (2M MAU, 600K DAU и до 50K онлайн-игроков c интерактивным взаимодействием).

Ниже приведен текст его выступления:


В чем основная проблема с серверной частью геймдева? А то, что это hiload! Это не hiload только в одном случае, если проект не будет доведен до конца. Если игра выйдет на маркет, пойдет реклама, пойдет поток игроков и первые несколько дней это будет, пусть небольшой, но вполне настоящий hiload. А если вас настигнет успех…

Посетив на конференции доклады, посвященные высоким нагрузкам, стало ясно, что все работают примерно в одном направлении. Первое, во что упирается любой высоконагруженный проект — это данные, и большинство докладчиков повествовали о том, как реплицировать, шардить, денормализировать и т.д. Не избежали этой участи и мы, но путь выбран несколько другой. Предлагается минимизировать работу с БД. По сути, избавится от нее. Как это сделать? А очень просто.

Идея
Пишем сервер, в который игрок будет логиниться, для игрока запускается выделенный процесс, в него загружается из БД профиль и далее все действия игрока происходят в этом процессе. Как игрок не проявляет активности некоторое время, пусть 10 минут, сохраняем профиль в базу и завершаем процесс. В результате мы имеем одно чтение из БД при логине и одну запись в БД при логауте и все!

Чего мы хотим?
Мы получили одно чтение и одну запись на одну игровую сессию. А значит, можем рассчитывать и прогнозировать, сколько игроков наше решение потянет. Думаю, многие могут прикинуть, сколько может выдать простейшая ключ/значение табличка, например в mysql, операций чтения записи в секунду. И сколько игроков протянет такая база в сутки. Число получится внушительное, и вот на это число мы себя огородили от проблем с БД. Что может быть лучше?

Реализация
Для реализации берем Erlang, так как в нем хорошо работать с процессами и… и все.
Что нам дает Erlang: процессы из коробки, может их стартовать, останавливать и посылать между ними сообщения. Это значит, что один процесс игрока может послать сообщение другому процессу другого игрока. И по тому же принципу взаимодействовать с процессами, обеспечивающими игровую логику. Интерактивность в таком случае также получается практически из коробки.

Разберемся по порядку с нюансами.

  • Адресация

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

  • Регистратор процессов

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

  • Старт процессов

Как уже говорилось выше, когда приходит сообщение игроку, мы обращаемся к регистратору с вопросом, на какой Pid слать сообщение, если для такого игрока нет процесса — нужно его запустить. Каждая операция занимает пусть небольшое, но время и возможна ситуация, когда к одному и тому же игроку придет два сообщения, в примерно одно время оба они получат отрицательный ответ от регистратора и попробуют стартовать процессы. В результате один из них стартует первым, а второй получит exception и сообщение будет потеряно. Мы не можем стартовать процессы асинхронно и должны организовать очередь для их старта. Для этого заводим процесс, в который будем направлять все наши обращения к регистратору и который будет стартовать процессы. Но получим узкое место, поэтому нужен не один такой “process_starting_worker”, а пулл, например из 100 вокеров, между которыми распределить все обращения к id пользователей любым удобным алгоритмом, хоть остатком от деления.

  • Остановка процессов

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

  • Кеширование и дальнейшая денормализация

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

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

  • Мысли о масштабировании

Во-первых, можно прикинуть нашу нагрузку и получить, что сервер под БД + сервер под наш код протянет несколько сотен тысяч игроков в сутки. Erlang достаточно честно использует память, поэтому если использовать в среднем 100кб под профиль игрока, то чтобы обслужить 50тыс игроков понадобится 5гб оперативной памяти. Иными словами, берем сервер на 32-64гб и с большой вероятностью забываем о необходимости масштабирования, до оглушительного успеха проекта.

Во-вторых, если оглушительный успех все-таки настал, то ничего не мешает “расшардить” БД по id игрока и распределить игроков при помощи ДНС по разным нодам Erlang`а. Проблема здесь лишь с нашим регистратором, он должен уметь работать в кластерном режиме. Gproc умеет, но как показали тесты — не до конца. Все что нужно — это немного его пропатчить или взять другой регистратор, но это отдельная тема, возможно для отдельной статьи.

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

Но самый главный вывод из применения такой архитектуры, что мы впихнули невпихиваемое и получили работоспособный сервис. Что было бы невозможно, используя классическую реализацию каждого чиха игрока с запросом в БД.

Комментарии (23)


  1. denis_g
    03.05.2015 00:49
    +4

    Спасибо за статью, интересная. Есть вопрос: почему бы в качестве промежуточного звена не использовать, к примеру, Redis — он тоже хранит данные в памяти, их можно один раз туда загрузить и потом по таймауту выгрузить?


    1. xmoonlight
      03.05.2015 05:20

      denis_g +100500
      Двумя руками — ЗА!


    1. KluchnikovAleksey
      16.05.2015 10:35
      +1

      На каждое действие пользователя надо будет достать профиль из редиса, раскодировать, сделать полезную работу, закодировать и положить в редис. Получаются большие накладные расходы


      1. denis_g
        16.05.2015 14:22

        Ну, тут уже все зависит от конкретной реализации профиля — если там многоуровневая JSON-структура, то тогда да, расходы большие, но если там обычный словарик, который записывается в Redis'овский hash или бинарная структура, которая хранится просто как последовательность байт, то накладные расходы не должны быть слишком уж большими. Кроме того, Redis умеет делать атомарные инкременты, что позволяет производить, например, учет статистики вообще без чтения из БД, а самое главное — поддерживает server-side scripting, который также может избавить от необходимости гонять данные туда-сюда в некоторых случаях.


        1. doom369
          16.05.2015 16:37

          Часто 1 реквест по сети стоит дороже чем вся бизнес логика. Если производительность для вас важна конечно.


          1. denis_g
            16.05.2015 19:40

            Зачем по сети? Не надо по сети. Можно Redis на той же машинке поднять и ходить к нему через unix domain socket.


            1. doom369
              16.05.2015 19:42

              Зачем тогда использовать редис, если он будет стоять на той же машине где и испольняемая программа?


              1. denis_g
                16.05.2015 19:50

                Например, чтобы избежать такого варианта развития событий. Плюс все-таки потенциальная возможность работы по сети (если нужно будет ее использовать в ущерб производительности).

                Собственно, мой первоначальный вопрос сводился не к утверждению «технология А лучше технологии Б, надо было вам использовать технологию А», а к вопросу «рассматривали ли вы технологию А и если да, то почему не стали ее использовать?». Т.е. интересно было узнать, почему именно Erlang, а не Redis, memcached или еще какая-нибудь in-memory БД.


                1. doom369
                  16.05.2015 22:36
                  +1

                  Например, чтобы избежать такого варианта развития событий


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

                  потенциальная возможность работы по сети

                  Ну это уже premature optimization.


                1. KluchnikovAleksey
                  17.05.2015 08:21
                  +1

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


  1. jonic
    03.05.2015 02:44
    +1

    я правильно понял что для 50 тыс онлайн пользователей нужно 50 тыс процесов? ОСь не повесится?


    1. gBear
      03.05.2015 05:38
      +4

      50 тыс. процессов Erlang.


    1. MaximChistov
      03.05.2015 10:55

      магия Erlang'a :)


  1. nikitinsm
    03.05.2015 07:15

    Выглядит красиво, до тех пор пока не начнете реализовывать. Процесс взаимодействия игроков выглядит не совсем так. Что будет например если игрок А хочет что-то сделать с игроком Б, когда тот в оффлайне. Поднимать весь процесс игрока Б для одного действия? А если нужен внутре-игровой рынок предметов, личные сообщения, форум, фиксация онлайн, топы (отдельный проект требующий БД ?). 50 000 тыс онлайн на одном сервере — допустим, а если 500 000 тыс? Я не в курсе насчет ерланга, но что будет при взаимодействие двух процессов находящиеся на разных узлах (физ. машинах), не плохо если сервера находятся в одной стойке, а если в разных ДЦ? Вы пробовали использовать что нибудь сетевое-асинхроное-джиттируемое + сервис очередей + масштабируемые-из-коробки БД типа (couchbase (не couchDB), mongo, redis и т.д.), хотелось бы узнать какие проблемы могут поджидать при таком подходе?


    1. mx2000
      03.05.2015 15:29
      +1

      Некоторы вообще не убивают процессы оффлайновых игроков. Ну висит себе игрок со статусом offline, и висит. память жрет, CPU нет. Зато работать с ним очень удобно.

      > Я не в курсе насчет ерланга

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


      1. nikitinsm
        03.05.2015 18:28
        +1

        Еще хочу спросить, вы игровую механику тоже на ерланге описываете? И сколько кодеров задействовано в этом?


        1. KluchnikovAleksey
          16.05.2015 10:38

          На эрланге логику писать дольше, но разница не многократная, по субъективным ощущениям даже не в два раза дольше, чем например на питоне.


  1. barsulka
    03.05.2015 07:51
    +14

    Как игрок не проявляет активности некоторое время, пусть 10 минут, сохраняем профиль в базу и завершаем процесс

    Как я понимаю, если сервер в течение этих 10 минут приляжет отдохнуть, то весь мой игровой прогресс, как и прогресс N тысяч других игроков, уйдет в небытие?


    1. KluchnikovAleksey
      16.05.2015 10:39

      Да. Но ничто не мешает делать внеочередные сейвы профилей на важные игровые события, например если игрок за что то платит реальными деньгами.


  1. doom369
    03.05.2015 16:45

    Раз так, то зачем вам база вообще? Если уж решили уйти от ынтерпрайза, то до конца. Профиль юзера можно просто в файл скидывать. Хоть в джейсоне, хоть в бинарном формате.

    Выбор Ерланга тоже совсем не оправдан. У меня такой же подход. 2.5к человек онлайн создают нагурзку 20к рек-сек… Все это на виртуалке с 2-мя 2.2Гц ядрами и на «тормозной яве» с асинхронными сокетами.


  1. kozzztik
    03.05.2015 17:49
    -1

    А можно поподробнее о базе данных? Что в итоге взяли, и какое DAU/MAU оно вытягивает, и как при этом загружено?


  1. seriyPS
    04.05.2015 17:11

    Насчет того, что Erlang «достаточно честно использует память» это совсем неправда. У Erlang довольно сложный многослойный аллокатор памяти, так что фрагментация может съедать до 50%.
    Например у меня сейчас запущено приложение, работает около 15 дней, в HTOP отображается 153Мб, изнутри же

    1> memory(total).
    61631088
    

    Т.е. 56Мб всего «полезной нагрузки». Это всё конечно можно подтюнить, но верным ваше утверждение от этого не станет.


    1. KluchnikovAleksey
      16.05.2015 10:41

      15 дней, 150 снаружи 50 внутри, по моему это «приемлемая честность», на сервере с 32гб оперативы.