User Tools

Site Tools


kg:dic_disdic_server_common

About the Server

For the sake of the you ppl this section written by plain old russian) Ага, сейчас я попытаюсь объяснить главные вопросы по дизайну сервера, связанные с этим технические проблемы и решения. Многие моменты тут нетривиальны, как и в целом дизайн сетевых многопользовательских систем, но, надеюсь, кое что таки станет понятнее.

Функционал

Что делает сервер?

  1. Т1 - Хранит состояние мира и базу пользователей
  2. Т2 - Обеспечивает возможность сетевого соеднинения с клиентом (инициируемое клиентом же)
  3. Т3 - Принимает и обрабатывает команды от клиентов
  4. Т4 - Отсылает клиенту изменения в мире
  5. Т5 - Обрабатывает периодические события в мире
  6. Т6 - Обеспечивает общение клиентов друг с другом посредством чата

Итак, основополагающие характеристики решения:

  1. Р1 - Соответствие технического решения условиям запланированной нагрузки
  2. Р2 - Масштабируемость решения (вертикальная, горизонтальная, динамическая), предсказуемость
  3. Р3 - Надежность решения (failover, availability)
  4. Р4 - Управляемость (deployment, maintainability)
  5. Р5 - Доступность (минимизация простоя)
  6. Р6 - Мониторинг (отслеживание процессов)
  7. Р7 - Простота разработки

Схема основных компонент сервера: (картинко)

Общий сценарий взаимодействия клиента и сервера

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

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

  • Старт
    • Загрузка конфигурации, automatic-discovery
    • Валидация данных, самодиагностика
    • Загрузка резидентных данных
  • Клиент устанавливает соединение с сервером
  • → Сервер создает сессию для клиента, отсылает базовую информацию клиенту о аккаунте
  • Клиент запрашивает обновление видимой части мира
  • → Сервер (проверив правомернось) отсылает видимый срез статической и динамической карты
  • Клиент отсылает команды-действия
  • → Сервер проверяет, подтверждает и сохраняет действия в очередь команд
  • Клиент отсылает информационные запросы, не изменяющие состояние
  • → Сервер отсылает актуальные данные мира
  • Сервер обрабатывает очередь команд от всех клиентов, отсылает клиенту обновления
  • → Клиент принимает и отображает результаты выполнения команд
  • Сервер обновляет мир с течением времени, отсылает нотификации о изменениях
  • → Клиент принимает к сведению, запрашивает обновления

Наброски прикладного протокола

  1. Клиент: Authenticate (credentials)
  2. Сервер: Auth-result (client_id)
  1. Клиент: GetPlayerData(client_id)
  2. Сервер: PlayerData(…)
.. и остальное

Обсуждение технических решений

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

Пространство принятия решения в целом - [питон / jvm]. Питон все упрощает, но, в силу производительности тяготеет в сторону распределенного stateless решения (о чем будет речь позже) когда мы легко можем под нагрузкой добавить еще дополнительные instances серверов. Ява решения, наоборот, тяготеют к одному большому statefull решению, в силу производительности и хорошей многозадачности.

Платформа для разработки

В этом забеге принимают участие следующие контестанты: python и несколько гостей из мира jvm:

Питон

Несмотря на свою динамическую натуру и неторопливость имеет следующие преимущества:

  1. +существуют библиотеки практически для всего
  2. +простая установка и кроссплатформенность
  3. +простота разработки и прототипирования
  4. +сила и простота языка )
  5. -+все же он интерпретируем, хотя и есть pypy который tracing jit
  6. -+непривычность для некоторых слабо-типизируемых языков, необходимость покрытия тестами функционала
  7. -GIL и сложности с многопоточностью (обычно решается в рамках отдельных процессов, что увеличивает нагрузку на систему)
  8. -относительная неэффективность работы с большим объемом (бинарных) данных

Ява и ее друзья

Собственно на jvm как платформе существует уже много интересных решений. Для начала рассмотрим старую, добрую яву.

  1. +отличная поддержка в виде сторонних библиотек для всего
  2. +простота для типичного разработчика из мира C++
  3. +eclipse и другие ide c морем функционала
  4. +производительность - где то не слишком далеко от c/c++
  5. +кроссплатформенность - работает практически везде. даже в SIM-карте телефона
  6. -verbosity - это один из самых страшно многословных языков. На каждый чих - класс, отдельный файл. Без IDE работать с таким объемом кода невозможно.
  7. -энтерпрайзнутость - сама среда существования языка тяготеет к энтерпрайз решениям, over-engineering в чистом виде, мегабайтам кода, фреймворкам размером с динозавра
  8. -требует компиляции и дружбы с такими вещами как ant и maven

К нашему счастью на ее основе все больше появляется решений, вбирающих плюсы платформы и в тоже время компенсирущих недостатки -

Groovy/Groovy++

Гость из JVM мира, разработка codehaus. Динамический язык, делающий мир явы куда более дружелюбным. Ознакомиться можно тут: [http://goo.gl/LMc2K]

  1. +простой и внятный язык с хорошим отношением количества текста к несомому функционалу
  2. +reuse всего того что есть для явы
  3. +элементарная разработка своих DSL
  4. -+медленный как питон, но дело исправляет разработка нашего соотечественника Groovy++ (http://code.google.com/p/groovypptest/)
  5. -+нетипизированный, но опять же groovy++
  6. -+поддерживается в eclipse, но не так хорошо как скажем родная ява
  7. +замечательные свои библиотеки для многих аспектов серверной разработки
  8. +упрощенный deployment

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

Scala

То же JVM родственник но другого толка. [http://www.scala-lang.org/node/104]

  1. +По verbosity ближе к groovy, точно не ява, хотя и выглядит местами похоже
  2. -+Желание авторов языка сделать комбайн
  3. +опять же работает со всем что есть для явы
  4. +поддерживается эклипсом, т.е. де-факто IDE
  5. +строгая, очень строгая типизация и упор на функциональщину
  6. -строгая, строгая типизация. Иной раз за этими типами прячется программа
  7. +производительность ~ яве
  8. +опять же свои DSL из коробки
  9. -+доступен для обычного C++ разработчика, практически шаблоны, классы, перегружаемые операторы, типы. После некоторого начального этапа с++ программист покрывается испариной и просится домой.

C++

Добавим для полноты картины.

  1. +Знаком с детства)
  2. +Фактически наилучшее решение по железной производительности
  3. +Привычно типизирован
  4. +Половину насущных задач решает boost
  5. -Verbose и полон управления памятью
  6. -Рай для развития и прогрессирования синдрома Non Invented Here
  7. -Из за бедноты стандартной библиотеки и отсутствия систематики спотыкается на месте и требует индивидуального подхода к каждому стороннему модулю
  8. -Переносимость требует медитации над платформенно-зависимым кодом
  9. -Отсутствует нормальная кросс-платформенная система сборки. Make, CMake - не предлагать. Waf - еще куда как, но это прощай VStudio.
  10. -Нет ничего из нормального языкового инструментария. Последний стандарт пока еще толком не поддерживается а без него язык - динозавр соревнующийся по древности с коболом и фортраном.
  11. -Коллекция Undefined Behaviors и неудачной, отданной на откуп железу модели памяти.
  12. -Low level API уровня оси везде оставляют желать лучшего и скорейшей кончины их разработчикам

Вопросы архитектуры

Собственно не архитектуры-архитектуры, а архитектуры-композициию. Концептуальные моменты. Первый вопрос - это stateless vs stateful.

Stateless vs stateful

Итак, даем определение - сервер без состояния работает целиком с представлением в базе. Не имеет состояния, хотя и может что то кэшировать r/o. Буквально - если его прибить и рестартануть, то ничего плохого не произойдет. Какие плюсы / минусы?

  1. +Горизонтальная масштабируемость инстансами - если capacity перевыполняется, просто спавним еще один сервер, а load-balancer перенаправляет часть нагрузки на него
  2. +Надежность - все сложности с потерей данных, крешами итп уходят целиком на базу. А они, сюрприз-сюрприз, с этой мыслью и создаются.
  3. +Дружелюбность к облачному хостингу - создав образ диска с настроенным сервером их можно поднимать и убивать в паравиртуализации как душе угодно. Более того, в том же Amazon EC2 есть возможность делать это автоматически в зависимости от нагрузки или по расписанию. И платить только за потребляемые ресурсы.
  4. +Состояние локализовано, через стандартный интерфейс базы доступно для интроспекции
  5. +Легкая миграция без остановки: изменился код - последовательно перезапускаем сервера (часть клиентов дисконнектится, но клиент сам восстанавливает соединение), поменялся формат базы(!) - оок, в новый сервер добавляем код миграции, если в новой базе нет - читаем со старой, пишем в новую. Заменяем последовательно сервера на новые, они же, в процессе работы перелопачивают базу в новый актуальный формат. Потом лишний код можно выпилить.
  6. -Нетрадиционность разработки - stateless модель это больше из мира веба и энтерпрайзнутой опердени. База становится главной осью вокруг которой все вращается. Изменить объект - достать из базы, покрутить, покласть обратно. Ест-но количество операций которое можно себе позволить - ограниченно.
  7. -База попадает под перекрестный обстрел - нужно знать/уметь как ее масштабировать, оптимизировать запросы, делать репликацию, шардирование.
  8. -Считается, что плохо сочетается с игровой логикой какую привыкли писать в десктоп сингле. Нельзя взять вот всеее эти объекты и сделать с ними что то осмысленное силами БД (да если и можно средствами хранимых процедур, не всегда разумно) - т.е. скажем пошаговые стратегии хорошо, а все интерактивное уже никак.
  9. -Латентность - время реакции системы велико из за протяженности маршрута обработки. Но все что рейал-тайм дизайнится совсем по другим принципам.

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

  1. +Относительная простота для заблудших в онлайн программистов
  2. +Данные локальны и неважно насколько часто их требуется “трогать” - ставь мьютекс и делай что хочешь
  3. +Все в одном, база не нужна - можно прочесть состояние с диска при запуске и записать при выходе. До какого то порога проще предсказать боттлнеки
  4. +Все шустро, маленькая латентность по отношению к клиенту
  5. -В минусы можно записать практически все плюсы stateless архитектуры
  6. -Нельзя перезапустить или мигрировать без полного останова
  7. -Масштабирование упирается в один большой сервер и дальше - остается только надеяться найти забытый где то в коде sleep
  8. -Надежность - может упасть со всем актуальным содержимым целиком. И привет.

Кроме того возможны гибриды. Например в чисто stateless сервере запросто можно ввести shared cache который обеспечивает быстрый доступ к горячим данным быстрее базы. Или в stateful - присобачить базу как хранилище, хранить все у себя но сбрасывать изменения в базу по факту.

Соединение с клиентом (коммуникационный протокол)

В выборе коммуникационного протокола принимает участие много переменных, в тоже время выбор не особо и велик. Имеет смысл отталкиватся от уже работающих решений из мира web 2.0 - http как основа позволяет построить решение из существующих проверенных компонент. Собственно флеш, к примеру, поддерживает свой собственный RTMP протокол для RPC но он не особенно популярен, заточен под коммерческий сервер и существенно ограничивает гибкость. Прямое raw-socket соединение, хотя и возможно, но так же сильно ограничивает reuse существующих ингредиентов.

HTTP же, по умолчанию, хорошо работает с текущей инфраструктурой сети, дружит с различными прокси и файрволами, имеет огромную базу готовых решений.

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

Pooling на основе http просто как две копейки. Грубо говоря клиент с некоторой периодичностью или по требованию каждый раз устанавливает новое http соединение с сервером, отсылая в http-транзакции свои команды и запросы и получая накопленные обновления от сервера. Ест-но сервер полностью пассивен в этом случае и не имеет возможности отослать клиенту данные сам по себе.

Poll / принудительный опрос

  1. +Еще лучше интегрируется с интернетом, собственно ничем не отличается от того что делает браузер.
  2. +Технически прост (по крайней мере со стороны клиента)
  3. -Сервер вынужден сам имитировать транспортную сессию (т.к. мы не имеем постоянного соединения), ее expiration в случае внезапного отсоединения клиента
  4. -Сервер должен заново обрабатывать установку соединения, все эти http-заголовки итп каждый раз когда клиент отсылает команду
  5. +При низкой активности клиентов (количестве действий в секунду) мы экономим на сервере число активных подсоединений

Persistent

  1. +Нормальный двунаправленный протокол, каждый может отослать peer'у что он хочет.
  2. -Сервер должен уметь держать соединения по количеству *всех* пользователей, даже если они не выполняют активных действий.
  3. +Сервер имеет обычную транспортную сессию для клиента благодаря устойчивому соединению, не нужно ее проверять на каждый запрос от клиента.
  4. +Экономим траффик

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

Фактически websockets представляет собой http-handshake и последующий апгрейд соединения до персистентного. Сам протокол достаточно прост, имеет родной lightweight framing и имеет уже довольно неплохую поддержку со стороны языков и платформ.

DB backend

Несоменно, это тоже один из краеугольных технических аспектов. Надежность и производительность решения напрямую соотносятся с его выбором. Необходимо отметить, что в силу структуры проекта выбор не одномерный. Фактически требуется два вида БД - основная, в которой живет мир целиком и некое 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 сервера, но все же.

Хостинг и maintenance

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

Dedicated

Выделенный сервер - аренда железа на площадке хостера. Цена вопроса, без учета траффика - где то в районе 1к-2к в год. Предоставляется полный доступ к железу и, в разумных рамках, с ним можно делать что угодно. Сам ставишь ось, сам организовываешь бэкапы итд. Если сервер поломается, ответственность за свои данные - на тебе. Достаточно дешево и сердито.

VPS

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

Облачный хостинг

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

Сетевая топология

Кластер серверов живет за load-balancer'ом/reverse proxy и не так важно, stateless or stateful. Дело в том, что наличие еще одного звена в цепочке до пользователя позволяет более гибоко манипулировать траффиком. В случае poll-протокола это дополнительная буферизация, уменьшение нагрузки на сервер приложения. А в целом - защита, например, от простого DDOS'а. Присылает школьнег письмо - мол, заддосю вас всем классом если сейчас не пришлете мне $200. И ведь может - правильно подобрав запрос, сервер можно нагрузить его обработкой и с довольно узким каналом. А тут мы можем запросто отсечь часть трафика совершенно бесплатно для серверов приложения.

 Продолжение следует
kg/dic_disdic_server_common.txt · Last modified: 2022/04/07 13:12 (external edit)