Создание сервера для Российских онлайн ММО игр ч. 12 — Очереди и параллельное программирование на CPU

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

Будет затронута тема очередей, асинхронного логирования, параллельного программирования на CPU и использования каналов (сhannel) для взаимодействия между процессами (thread - ветками) на языке программирования PHP (аналогичный функционал есть в языке GO).

Создание сервера для Российских онлайн ММО игр ч. 12 — Очереди и параллельное программирование на CPU

Два сервера в одном

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

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

Работает это по следующему принципу: после того как WebSocket сервер получил команды от игрока нужно эти команды обработать. Если мы будем делать этот в текущем процессе это будет медленно т.к. расчетам будут постоянно мешать приходящие новые команды игроков которые будут обрабатываться асинхронно - по простому говоря когда мы будем обрабатывать команды (например рассчитывать поиск пути к точке которой игрок желает пройти).

Для этого у нас есть параллельно работающий с Websocket сервером процесс обмен данных с которым осуществляете благодаря библиотеке написанной на С++ - Parallel, функции обмена между этими процессами строятся на базе каналов (аналог Channel в языке программирования GO) на скорости 1.600.000 запросов в секунду из одного процесса в другой (без учета времени на исполнения этих запросов) согласно проводимых мною замерам. Такие цифры достигаются за счет обмена в оперативной памяти.

Создание сервера для Российских онлайн ММО игр ч. 12 — Очереди и параллельное программирование на CPU

Подход на рисунке выше дает нам, в том числе некоторое подобие Socket IO в той части, где один пакет данных (с игрового сервер на сторону WebSocket) отправляется всем подключенным к Websocket серверу игрокам, а так же позволяет перезапускать сам игровой сервер(и тем самым обновляя его код) без потери соединений игроков (в одной из статей я рассказал что Socket IO это не прерогатива языка Node JS, а лишь подход) .

Например: мы отправили команду идти вверх на websocket, он передал в игровой сервер, в очередном кадре игрового сервера последний проверил можно ли нам пройти в указанную клетку (с кем мы столкнёмся, будем ли ранены из-за этого и т.п.), после чего добавит в пакет на передачу обратно в websocket новые координаты нашего героя и в конце кадра (цикла) сервера одной командой отправит все назад в websocket (в этот момент игровой сервер блокируется, но как я писал выше это 1.600.000 пакетов в секунду, да и на это время уменьшится время паузы между кадрами сервера - в настоящий момент я экспериментирую на 1000 кадров сервера в секунду, аналог 1000 FPS и Fixed Update в Unity), а websocket сервер разошлет всем игрокам данные не блокируя игровой сервер (сам он может иметь несколько worker процессов наподобие того как их имеют веб сервера Apache, Nginx, Swoole и т.п. что бы была возможность принимать и отправлять еще больше пакетов от игроков, однако все эти worker должны иметь "подписку" на данные что рассылает игровой сервер).

Что еще можно распараллелить?

Создание сервера для Российских онлайн ММО игр ч. 12 — Очереди и параллельное программирование на CPU

Процессы выше способны работать параллельно на разных ядрах CPU, иметь внутри себя свой загруженный код и обмениваться не только текстовыми данными, но и анонимными функциями (Closure - замыкания). В процессе разработки можно перекладывать расчеты и выполнения части код на другой параллельный процесс, проверять его состояние и ошибки. Один из таких долгих процессов - это логирование которое по результатам моих исследований находится в районе 80.000 записей в секунду запись Hello World. Можно улучшить этот показатель в 3 раза если нам не важна последовательность записей (писать в фаилы асинхронно, например благодаря корутинам библиотеки Swoole), однако при разработке ММО игры это достаточно "узкое горлышко". И нам даже не обязательно иметь 3 ядра ведь так или иначе websocket сервер или игровой сервер будет иметь окошко когда логирование сможет вклиниться в очередь исполнение команд процессором (FIFO) и тем самым у нас появился сервер логирования (в дополнение можно создать сервера для вычисления сложных механик, например поиска пути, но для этого лучше использовать видео карту и программировать на ней - на GPU, но об этом в следующих статьях)

Создание сервера для Российских онлайн ММО игр ч. 12 — Очереди и параллельное программирование на CPU

Для справки: максимальное количество одновременно работающих параллельных потоков (thread, нитей) на CPU равно сумме количества логических процессов всех ядер CPU (одно ядро может иметь несколько логических процессов (нитей thread) которое указано в спецификации к процессору).

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

Популярные очереди

Возможно многим покажется избыточным использование в PHP такой специфический для языка функционал параллельного программирования (что более популярно в программировании на GO) и покажется более практичным использование очередей и даже разделить WebSocket сервер и игровой сервер на разные физические машины.

Когда мы используем очереди мы преследуем 3 основные цели (поправьте если я не прав):

  1. Очередность выполнения команд - очереди позволяют нам вести учет какие команды пришли раньше
  2. Масштабирование - т.е. переложить выполнение части функционала на другое приложение (сервис), которое чаще всего находится на другой физической машине (сюда же добавляется гарантированная доставка в случае сбоя)
  3. Шина обменена данными - создание некого канала обмена данными между процессами на одной машине (когда мы используем ядра процессора), а так и между разными физическими машинами (когда у нас некая масштабированная сервисная архитектура) которые выполняют вычисления

Из популярных систем очередей можно выделить RabbitMQ и Redis Pub-Sub (некоторым можем показаться это странным, но чаще он используется как шина обмена данных между сервисами, например для масштабированного кеше данных из БД и его обновления). Их показатели скорости (perfomance) на рисунках ниже:

Полагаю можно использовать несколько запущенных экземпляров (instant - "инстант") Redis и RabbitMQ, однако проблема заключается в том что когда речь идет о скорости в среднем +-60.000 запросов в секунду на запись - это 1.6мс и это много по сравнению со скоростями работ игровых механик и команд от игроков (я их называю RPS игрового сервера) скорость которых лежит в районе сотый долей миллисекунды и достигает скорости обработки 1.000.000 и пропускной способностью WebSocket сервера которая может достигать ~2.000.000 обработки запросов в секунду (исключительно на прием и отправку пакетов, без выполнения какой-либо логики т.к. у нас всю логику выполняет другой процесс. Такая скорость достигается так же из-за наличия большого количества worker процессов) согласно публичным тестам

И если WebSocket сервер благодаря множеству запущенных экземпляров (workers) еще может масштабировать запись в разные instance Redis (например) данные, то игровой сервер одной определенной локации - он один на одну локацию (у него уже нет workers как у websocket сервера) и его дробление (масштабирование) это уже другая локаций и постоянно блокировать его работу на 1.6 мс. при поступлении данных будет являться узким горлышком и снизит показатели игрового сервера до порога меньше этих 60.000 запросов в секунду (т.к. еще есть и игровые механики которые нужно обработать)

Однако признаюсь изначально я работал именно с Redis в качестве шины и в одном из своих старых видео я показал результаты. Как говорят "Опыт - сын ошибок трудных"

Я предполагаю что "узким горлышком" в Redis и RabbitMQ является тот факт что они работают на базе TCP протокола (Redis может работать на сокете если клиент и сервер находятся на одном железе что немного быстрее) и наличие некой базы данных в которую пишется эта очередь как сделано например в очередях Laravel (там очередь пишется в базу данных Mysql). Эта база нужна, в том числе и для гарантированной доставки в случаях сбоя и может быть завязана на файловом хранилище т.к. диапазон скорости записи очень похож на скорость записи в файлы - 80 000 в секунду о которой я писал выше, хотя могут быть и другие причины) .

Собственная очередь в оперативной памяти

Оперативная память компьютера (ОЗУ, RAM) - самый быстрый способ обмена данных. При разработке непосредственно ММО игр нам в целом не важна гарантированная доставка попавшей в очередь команды игрока (в играх бывают и платежи и операции связанные с риском потерей ценной информации, но такие операции обычно выносят в отдельные сервисы с которыми взаимодействие идет по HTTP и вот там уже используют RabbitMQ и пр.)

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

Использование библиотеки Parall и Channel, речь о которых была в начале статьи, поможет нам обмениваться данными между процессами так же в оперативной памяти как шина между процессами (например мы можем передавать из websocket сервера список команд игроков на исполнение игровому серверу, передавать текст логов на запись в сервер логов, передавать координаты для расчета оптимального пути в сервер для расчета поиска пути и т.д.) - это значит что шина обмена данными так же присутствует.

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

Заключение

Документация и примеры игр данного проекта доступны на моем сайте http://mmogick.ru (есть бесплатный демо доступ в панель администратора) . Проект является моим стартапом - вы можете следить за выпуском статей подписавшись на мой профиль (пишу только о проекте), а так же делаю ролики в Youtube

Надеюсь что мои исследования и архитектурные идеи помогут людям в разработке их проектов онлайн игр т.к. информации в интернете практически нет (именно как писать свой авторитарный сервер без использования каких либо сервисов типа Photon Engine, Mirrot и т.д.), еще меньше проектов которые в итоге заработали и всего лишь пару статей на русском.

История:

2727
21 комментарий

Комментарий недоступен

Комментарий недоступен

Если переложить на этот проект то выходит следующее (поправьте если не прав):

Producer 1,2,3 - это игровые клиенты

Event manager - это websocket сервер куда поступают команды игроков (Event A,B,C)

Broker - это экземпляры websocket сервера (workers процессы которые принимают пакеты и соединения из "кучи" параллельно)

Consumer - это выходит игровой сервер что физику считает ? Но на одну локацию он один (он крутится в бесконечном цикле обрабатывая всех на локиции и npc в том числе). Их может быть много, но каждый идет со своим Event Manager (websocket сервером) на который он подписан . Правда есть одна оговорка - у меня бесшовный мир и другой Consumer может быть подписчиком и своего Event Manager и другого (в игре я просто вижу что происзодит на соседней локации и переходя переключаюсь на другой игровой сервер)

Полагаю если я все верно переложил у меня как на рисунке только Consumer 1 всегда подписан на все Event и отправляет результат вычислений назад и все Producer его получают

Полезные статьи пишешь, спасибо. Инфы действительно мало об этом

Спасибо. По итогам будет рабочая mmo rpg в 2д на пк, мобилтники и браузеры.

С прокачкой, инвентарем, нейросетью и тд. Все о чем пишу - реализовывается в ней.