For the sake of the you ppl this section written by plain old russian) Ага, сейчас я попытаюсь объяснить главные вопросы по дизайну сервера, связанные с этим технические проблемы и решения. Многие моменты тут нетривиальны, как и в целом дизайн сетевых многопользовательских систем, но, надеюсь, кое что таки станет понятнее.
Что делает сервер?
Итак, основополагающие характеристики решения:
Схема основных компонент сервера: (картинко)
Один из важных моментов дизайна - сервер является авторитетом по отношению к клиенту и принимает решения о правомерности всех команд поступающих от него. Клиент хранит некоторое, минимальное представление мира, но оно не является авторитетным. Поскольку представление клиента всегда запаздывает от актуального, желательно минимизировать этот объем, храня только то, что связанно с отображением. С другой стороны стоит задача минимизации нагрузки на сервер, т.е. частота запросов от клиента.
Очевидный конфликт - либо усложнять сервер, хранить для каждого клиента его состояние и отсылать дельту изменений (черевато рассинхронизацией, нецелевыми расходами per client на сервере ) либо отсылать все состояние (актуальное для клиента) с подвариантом - сделать клиент ведущим в этом процессе.
.. и остальное
А сейчас перейдем к обсуждению конкретных технических решений, их плюсов и минусов. К сожалению картина меняется каждый раз и все из нижеприведенных факторов так или иначе имеют значение.
Пространство принятия решения в целом - [питон / jvm]. Питон все упрощает, но, в силу производительности тяготеет в сторону распределенного stateless решения (о чем будет речь позже) когда мы легко можем под нагрузкой добавить еще дополнительные instances серверов. Ява решения, наоборот, тяготеют к одному большому statefull решению, в силу производительности и хорошей многозадачности.
В этом забеге принимают участие следующие контестанты: python и несколько гостей из мира jvm:
Питон
Несмотря на свою динамическую натуру и неторопливость имеет следующие преимущества:
Ява и ее друзья
Собственно на jvm как платформе существует уже много интересных решений. Для начала рассмотрим старую, добрую яву.
К нашему счастью на ее основе все больше появляется решений, вбирающих плюсы платформы и в тоже время компенсирущих недостатки -
Groovy/Groovy++
Гость из JVM мира, разработка codehaus. Динамический язык, делающий мир явы куда более дружелюбным. Ознакомиться можно тут: [http://goo.gl/LMc2K]
Грубо говоря Groovy++ это надстройка (точнее плагин к компилятору) для оригинального Groovy, которая вводит строгую типизацию, существенно улучшает производительность и вводит несколько полезных фич, оставляя при этом возможность использовать оригинальный язык.
Scala
То же JVM родственник но другого толка. [http://www.scala-lang.org/node/104]
C++
Добавим для полноты картины.
Собственно не архитектуры-архитектуры, а архитектуры-композициию. Концептуальные моменты. Первый вопрос - это stateless vs stateful.
Stateless vs stateful
Итак, даем определение - сервер без состояния работает целиком с представлением в базе. Не имеет состояния, хотя и может что то кэшировать r/o. Буквально - если его прибить и рестартануть, то ничего плохого не произойдет. Какие плюсы / минусы?
На другом поясе находится целиком stateful модель, которая похожа на типичный сингловый прообраз игры, с той лишь разницей что игроков там больше. Все в одном месте, все пользователи лезут туда же.
Кроме того возможны гибриды. Например в чисто stateless сервере запросто можно ввести shared cache который обеспечивает быстрый доступ к горячим данным быстрее базы. Или в stateful - присобачить базу как хранилище, хранить все у себя но сбрасывать изменения в базу по факту.
В выборе коммуникационного протокола принимает участие много переменных, в тоже время выбор не особо и велик. Имеет смысл отталкиватся от уже работающих решений из мира web 2.0 - http как основа позволяет построить решение из существующих проверенных компонент. Собственно флеш, к примеру, поддерживает свой собственный RTMP протокол для RPC но он не особенно популярен, заточен под коммерческий сервер и существенно ограничивает гибкость. Прямое raw-socket соединение, хотя и возможно, но так же сильно ограничивает reuse существующих ингредиентов.
HTTP же, по умолчанию, хорошо работает с текущей инфраструктурой сети, дружит с различными прокси и файрволами, имеет огромную базу готовых решений.
Далее, выбор между устойчивым соединением и моделью когда клиент принудительно опрашивает сервер (и плеядой промежуточных решений).
Pooling на основе http просто как две копейки. Грубо говоря клиент с некоторой периодичностью или по требованию каждый раз устанавливает новое http соединение с сервером, отсылая в http-транзакции свои команды и запросы и получая накопленные обновления от сервера. Ест-но сервер полностью пассивен в этом случае и не имеет возможности отослать клиенту данные сам по себе.
Poll / принудительный опрос
Persistent
Очевидно что второй случай более предпочтителен, хотя может быть и более затратным. Основной для него я решил выбрать WebSockets как молодой, но подходящий для нас транспортный протокол. В настоящее время его поддержка бурно развивается в клиентских и серверных решениях, он удачно маскируется под http и в тоже время является нормальным двусторонним устойчивым протоколом.
Фактически websockets представляет собой http-handshake и последующий апгрейд соединения до персистентного. Сам протокол достаточно прост, имеет родной lightweight framing и имеет уже довольно неплохую поддержку со стороны языков и платформ.
Несоменно, это тоже один из краеугольных технических аспектов. Надежность и производительность решения напрямую соотносятся с его выбором. Необходимо отметить, что в силу структуры проекта выбор не одномерный. Фактически требуется два вида БД - основная, в которой живет мир целиком и некое NoSQL решение для кэша. Словом, это не взаимоисключающие варианты.
MySQL
Классическая база, обеспечивающая приемлимую производиетельность с InnoDB хранилищем + XtraDB патчи. Транзакционность, SQL - все как везде. Местами кривоватый или скажем слишком простой оптимизатор запросов, но скорее всего это не касается нашей области применения. Можно даже заменить на Postgres, от этого сильно ничего не изменится. В принципе гарантированная производительность, поддерживает репликацию. Собственно и сказать нечего. Есть байндинги для python (правда не асинхронные либо эмулирующие асинхрононность пулом соединений), ест-но есть и для явы.
MongoDB
NoSQL document-based storage. Фактически да, ноу-скьюль, но с дружественными фичами. База живет в памяти (memory-mapped files) из за чего ограниченна по размеру адресным пространством и физической памятью (во взрослых базах disk-based базах память так же имеет не последнюю роль, стоит перейти определенный порог и все, база вместо полезной работы будет свопить данные с диска). Нет схемы, т.е. хранит “документы”, а попросту - некоторые json-структуры. Но поддерживает индексацию, быстрый поиск и подобие SQL посредством встроенного java-script'а. Весьма шустра, как минимум раза в два от скорости mysql, даже по записи. Поддерживает fail-over и репликацию в slave-базы. Если нет нужды связываться с реляционными запросами - хороший кандидат на основное хранилище. Из минусов - в связи с аморфностью (отстуствием схемы) для каждой записи хранит все целиком в BSON формате. Иначе говоря структура { name: “bubu”, level: 123 } кроме bubu и 123 хранятся так же “name” и “level” для каждой записи, мгм. Опять же есть поддержка для любой платформы.
Redis
Кандидат на эдакий общий сетевой кеш. В целом идея - тот же memcached, но с ценными фичами. Фактически хеш-таблица, которую можно писать и читать очень быстро по сети. Поддерживает так же очереди, ассоциативные списки, pub/sub. Производительность на уровне сотни тысяч запросов в секунду. Удобна для хранения данных сессии игрока, transient данных, организации внешних performance-счетчиков или скажем чата.
Работать с сетью и вводом-выводом можно двумя способами - синхронно, создавая по нити на каждое соединение и спя ожидая окончания операций и асинхронно - демультиплексируя весь ввод-вывод в одну нить или ограниченный (bounded) пул нитей-обработчиков. На самом деле, как постулирует C10K, этот вопрос даже не стоит - из за накладных расходов и нецелевой траты ресурсов обработать много одновременных соединений в синхронной модели невозможно.
С другой стороны, асинхронная модель сложнее в программировании. Фактически, без поддержки со стороны языка (coroutines, call/cc, yield, generators, привет С++) это выливается в сплошные коллбэки на каждый чих. Это похоже на закат солнца вручную или организацию кооперативной многозадачности. Простой пример - читаем с файла. Обычно мы зовем fread(…) и нить блокируется осью до окончания операции. В асинхронной модели блокировки недопустимы и вместо этого мы передаем callback-функцию - fread( …, callback ). Вызов инициирует операцию и после вызова мы продолжим исполнение до тех пор, покуда не вернемся вниз по стеку вызовов до реактора ( демультиплексора ) и тогда он, подобрав результаты операции позовет callback с результатами. В ископаемых языках за, некоторым исключением, нет возможности сохранить контекст и вывалиться вниз до нужной точки, с тем, чтоб продолжить исполнение с сохраненного контекста, без вмешательства низкоуровневой магии.
Картину дополняют вложенные, последовательные и другими способами замиксованные асинхронные вызовы, что, разумеется, доставляет. Мешанина из коллбеков это еще так радость. По счастью в питонах и других вышеобсужденных языках так или иначе наличиствуют средства для борьбы с этим произволом. (Привет лиспу и схеме с call/cc)
Но это еще не все) Дело чуть упрощается пока на jvm языках мы можем отдать отдельную нить для демультиплексора и увести все обработчики в worker-нити, в то время как на питоне нить одна и там происходит все вместе. Блокировка нити на значительное время означает задержку всего остального мира. Впрочем, там это решается отдельными instance сервера, но все же.
Сугубо прагматический вопрос, но, какбэ, напрямую связан со всем остальным. Отбрасывив вариант “соберем машину дома и повесим в сеть” для хостинга существует несколько доступных решений.
Dedicated
Выделенный сервер - аренда железа на площадке хостера. Цена вопроса, без учета траффика - где то в районе 1к-2к в год. Предоставляется полный доступ к железу и, в разумных рамках, с ним можно делать что угодно. Сам ставишь ось, сам организовываешь бэкапы итд. Если сервер поломается, ответственность за свои данные - на тебе. Достаточно дешево и сердито.
VPS
Практически тоже самое, только вместо железного сервера дается паравиртуализированный - иначе говоря кусок железного с определенными ресурсами. С некоторыми ограничениями делать с ним можно тоже самое - устанавливать свою ось, перегружать итп. Дешевле выделенного, но все определяется выделяемыми ресурсами на слайс.
Облачный хостинг
Эта расширенный вариант VPS паравиртуализации. Фактически добавляются развитые способы управления - можно склонировать готовый образ оси, установить необходимое ПО, сохранить в свой образ и потом запустить его на множестве виртуалок. Все это организуется достаточно просто, посредством веб-консоли либо предоставляемого API. Может быть дороже простого VPS и даже выделенного сервера, но это SaaS за что и цена. В целом же получается профит из за того что нет необходимости арендовать железку на год (без возможности отменить или поменять план) а наращивать ресурсы по необходимости.
Кластер серверов живет за load-balancer'ом/reverse proxy и не так важно, stateless or stateful. Дело в том, что наличие еще одного звена в цепочке до пользователя позволяет более гибоко манипулировать траффиком. В случае poll-протокола это дополнительная буферизация, уменьшение нагрузки на сервер приложения. А в целом - защита, например, от простого DDOS'а. Присылает школьнег письмо - мол, заддосю вас всем классом если сейчас не пришлете мне $200. И ведь может - правильно подобрав запрос, сервер можно нагрузить его обработкой и с довольно узким каналом. А тут мы можем запросто отсечь часть трафика совершенно бесплатно для серверов приложения.
Продолжение следует