![](https://habrastorage.org/webt/dx/ww/5l/dxww5lpt9l-3gqdwrvmc0bmpo2e.png)
По воле абсудрных обстоятельств, которые сможет понять лишь тот, чьё хобби полностью совпадает с основной работой, недавно я оказался вовлечён в разработку MMO-игры.
Несмотря на то что это приложение идеально вписывается в концепцию «распределённых архитектур», конкретные детали (как большие, так и малые) превращают, казалось бы, простой для любого грамотного инженера процесс проектирования в невероятную головную боль.
Задержки, состояния гонки, синхронность и доступность – это то, с чем сталкивается любой архитектор ПО изо дня в день. Тем не менее почти в каждом случае решением оказывается пересмотр функциональных и технических требований (устроит ли нас задержка менее 10 секунд?), так что мы редко прибегаем к проектированию полных решений ввиду их сложности.
Но мир многопользовательских игр устроен немного иначе. Пространственная сложность в нём устремляется к бесконечности, а временну́ю мы стремимся сократить до минимума. Мы работаем со множеством строго детерминированных модулей, которые можем легко обслуживать и развёртывать параллельно. И рано или поздно мы сталкиваемся со злом, которое кроется за кулисами любой MMO – проблемами ввода-вывода в базе данных.
Основная цель этой статьи – изучить ограничения, которые возникают на уровне ввода-вывода данных при проектировании MMO-архитектур, взаимодействия системы с данными, вытекающие из всего этого проблемы и их решения.
И здесь мы столкнёмся с когнитивным диссонансом в его чистейшей форме, поскольку решения, которые применяются в MMO-системах, представляют явный нонсенс для корпоративной среды (e-commerce?).
Интересно рассмотреть, как различные решения определяются самой проблемой и требованиями системы, чтобы оценить эту красоту программной архитектуры.
Источником истины игрового мира является НЕ база данных.
Этот принцип будет сложно принять тем из нас, кто пришёл из корпоративной среды, но так оно и есть. В онлайн-играх источник истины для состояния игрового мира находится в памяти, а не в базе данных.
В таких случаях мы рассматриваем БД как средство хранения информации, а не источник истины, который на самом деле всегда находится в памяти.
«Что за инакомыслие!», подумают некоторые. Но это не совсем так. Давайте немного разберём этот принцип, используя простейшую математику.
Предположим, у нас есть MMORPG вроде World of Warcraft с 1000 игроков. Её мир поделён на зоны, а значит, мы можем реализовать шардинг, но самое интересное в том, что все игроки живут вместе в одной игровой среде.
Им нужна возможность видеть друг друга, уровень других игроков, общаться, обмениваться предметами и так далее. Если оперировать логикой данных, то для того, чтобы всё это работало должным образом, состояние мира должно быть уникальным.
Допустим, что игроки просто ходят по окрестностям и убивают диких кабанов. Также, допустим, что наш клиент игры, чтобы сильно не нагружать инфраструктуру, отправляет действия игрока на сервер раз в секунду. Получается, что при количестве игроков N в секунду будет отправляться N сообщений об обновлении позиции.
И это только для отслеживания позиций игроков в мире! А ещё надо добавить получение опыта, атаки оружием, сообщения чата и так далее…
Если взять за источник истины нашу базу данных, то ей придётся сохранять всю эту информацию, для чего потребуется N записей в секунду, и это только для позиций игроков.
![](https://habrastorage.org/webt/ns/ca/iu/nscaiumldbe8lfo3oxeuonnioge.png)
Удачи!
Но решение для такой проблемы окажется относительно простым, когда мы избавимся от идеи использования в качестве источника истины базы данных.
Кэш, много кэша
Когда я сказал, что источник истины состояния игрового мира находится в памяти, то это и имел в виду, хотя не в самом прямом смысле.
В таких случаях перед нами возникает немного требований, но сложных:
- между игровыми сервисами и состоянием мира должна быть установлена прямая связь с низкой задержкой;
- состояние мира должно сохраняться, чтобы можно было его воссоздавать либо восстанавливать после ошибок или сбоев в работе;
- нужна возможность масштабирования;
- необходимо избегать состояний гонки.
Чтобы выполнить все эти требования, мы обычно используем шаблон «брокер данных». Мы создаём сервис с прямым подключением к базе данных, который будет отслеживать всё состояние в памяти и подключаться к сервисам игрового мира через RPC (Remote Procedure Call, удалённый вызов процедур). При этом он выступает в роли своеобразного кэша базы данных.
![](https://habrastorage.org/webt/tj/rz/n4/tjrzn4b7am3ezhjtzzap3w5gof4.png)
Но как это помогает выполнить требования? Разберём всё по частям.
Прямое соединение с низкой задержкой между игровыми сервисами и состоянием мира
Это наверняка простейшая часть. Сервисы нашего игрового мира подключаются через RPC к сервису, обслуживающему состояние игры, и выполняют некие действия.
И здесь возникает важный момент, который хоть и выходит за тему статьи, но упоминания стоит.
Эти команды RPC должны быть специфичными для нашей игровой логики. Никаких SQL или непонятных запросов, связанных с долговременным хранением или самим понятием «данные».
grantPlayerExperience
, playerChangingZone
и так далее.За реализацию этого API отвечает сервис данных, чтобы при получении команды
grantPlayerExperience
он добавлял N очков опыта игроку X.Таким образом, мы сохраним наши слои раздельными. Мы отделим API данных от реализации, изолировав тем самым логику игры.
Нужно сохранять состояние мира для его воссоздания или восстановления после ошибок/перебоев работы
Понятно, что нужно сохранять, но «Что?» и «Когда?» Эта больше философская работа, которая относится скорее к дизайну продукта, чем к проектированию архитектуры, но всё же давайте попробуем этот нюанс разобрать.
Первый вопрос в том, насколько точно нужно сохранять состояние? Порой мы склонны считать, что сохранять нужно даже малейшее движение, хотя зачастую это не так.
Возьмём, например, игру League of Legends. Что бы мы в ней сохраняли и когда?
Лично я бы сохранял финальное состояние игры после её завершения (в этом примере мы игнорируем переигрывания, возможность наблюдения и прочее).
И причина тому проста. В подобных системах сохранение нам нужно для того, чтобы иметь возможность восстановить их на случай проблем, а не в качестве бизнес-аспекта.
Предположим, что мы сохраняем каждое изменение в процессе игры, и наш инстанс взрывается, отключается от сети или поглощается космической бездной. Сможем ли мы восстановиться после этой ошибки? Сможем восстановить последнее сохранённое состояние?
Нет, не сможем.
Скорее всего, к моменту, когда мы восстановим состояние, половина игроков уже не будут подключены.
Зачем же нам тогда сохранять все эти изменения в базе данных?
Как я писал выше, вся суть в том, Что сохранять и Когда.
В случае нашего примера, что мы сохраняем? Собственно, характеристики персонажа в конце игры и немного других данных. Когда? В конце игры.
В более сложном случае вроде World of Warcraft мы можем сохранять, например, инвентарь, когда предмет сменяет владельца или используется; позицию персонажа через каждый определённый промежуток времени (возможно, 30 секунд?), новую зону при каждом переходе в неё и так далее.
Суть в том, чтобы максимально уменьшить число возможных записей, фиксируя только то, что будет важно для восстановления.
Всё остальное мы сохраняем в памяти в сервисе игрового состояния.
Нужна масштабируемость
Это, пожалуй, один из наиболее каверзных вопросов. В плане масштабируемости использование сервиса, который будет удерживать в памяти состояние всего мира, пожалуй, станет не лучшим вариантом, поскольку легко масштабировать его вертикально, а вот горизонтальное масштабирование ведёт к ряду проблем.
Помимо того, что такое решение представляет единую точку отказа, оно ещё и создаёт серьёзную сложность, о которой в этой статье я говорить не буду.
Относительно простым решением может стать использование системы Redis «издатель-подписчик» для синхронизации сервисов состояния.
Опять же, давайте подумаем об этом с такой позиции. Если у вас есть 100 сервисов игрового мира, взаимодействующих с одним сервисом состояний, то мы получаем 100 открытых сокетов, что несопоставимо с тысячами команд базы данных, которые бы у нас были, проигнорируй мы брокера?
Нужно избегать состояний гонки
Это, пожалуй, самый простой пункт, так как если мы поддерживаем в нашем сервисе данных однопоточную архитектуру, то практически гарантированно не получим ощутимых состояний гонки.
Тем не менее работая с распределёнными данными или в многопоточных средах у нас, есть отличный друг.
▍ CAS
Каждую операцию записи для наших сервисов данных можно выполнять с помощью инструкции CAS (Compare and Swap, сравнение и перестановка), чтобы эти записи точно выполнялись асинхронно.
Делается это просто. Вы получаете хэш состояния (его часть) перед началом операции, которую мы назовём версионирование хэша.
Далее вы подготавливаете новое состояние, генерируете для него хэш и сохраняете его только, если он совпадает с тем, который вы получили в начале.
![](https://habrastorage.org/webt/g3/u-/ys/g3u-ysumvjx0barrhzjcvqwp7dq.png)
Простая система контроля версий
А что происходит, если CAS даёт сбой? Делаем повтор N раз (сколько сочтёте нужным), и если выполнить её так и не получится, возвращаем ошибку.
Понимание состояния вашего игрового мира изменчиво, и умение определить точные данные, которые следует сохранить, скорее, вырабатывается с опытом, нежели передаётся с обучением.
Если говорить в целом, то при работе в распределённых системах с высоким трафиком, где множество агентов постоянно изменяют динамические данные, рассмотрение базы данных в качестве источника истины довольно быстро ведёт к возникновению узких мест и проблем доступности.
Типичным решением, принимаемым в индустрии MMO, как правило, являются стратегии, которые для тех, кто работал в сфере корпоративных архитектур, могут показаться не интуитивными или неестественными. Хотя и они имеют свои проблемы и слабости.
Хорошее планирование обновлений и окон сохранения данных вкупе с тщательным анализом сценариев нашей системы и шаблоном «брокер данных» позволяет освободить базу данных от лишних операций записи, а наши бюджеты от огромных счетов.
Узнавайте о новых акциях и промокодах первыми из нашего Telegram-канала ?
![](https://habrastorage.org/webt/os/g2/ms/osg2msglmcba81cfkh0vl0nlnu4.jpeg)