Как стать автором
Обновить

Комментарии 33

интересно почему бы не использовать среды ЯП где нет проблем с процессами и потоками.
в таких где процессы и потоки проработаны они могут давать дополнительные возможности.

Согласен, использование Python для параллельного программирования, а уж тем более для создания сервера real-time игры, где важна производительность - дело довольно сомнительное, так как Python в его дефолтной CPython реализации использует только одно ядро.
Однако, в виду моего большого опыта работы с Python, мне изначально хотелось сделать все с его помощью, я начал изучать такие библиотеки как pygame и pymunk и пробовать сделать свои первые клиент и сервер.
И действительно на Python я не смог далеко уехать. И проблема пришла не на сервере, клиент перестал справлятся со спрайтами, стал долго их загружать, а создавать эффекты, анимации с помощью сил Python стали огромным ударом по производительности, в итоге ради клиента перешлось перейти на Untiy C#, который облегчил разработку клиента и увеличил производительность в разы. А вот сервер со своими задачами всё ещё справлялся, но оно и понятно, наш проект - это 2D экшн с небольшой ареной до 3-х игроков, было бы что-то посложнее и тут бы Python не справился, пришлось бы также переходить на другой язык, но архитектура, описанная в статье, осталась бы той же.
Я пробовал перейти на реализации, где нет GIL, такие как Jython, однако тут возникли проблемы с установкой нужных мне библиотек, поэтому остался на CPython.

а чем например не устроил Godot в качестве клиента для 2D, что склони в пользу Untiy конкретно в вашем случаи?
Godot GDScript считается вроде как похожим на Python.

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

  1. У нас на проекте есть саунд дизайнер и мы решили использовать Wwise как audio middleware, чтобы мне, как программисту, не писать логику затухания звуков, лупов и прочего, чтобы этим занимался сам саунд дизайнер. И у того же Wwise из коробки легкая интеграция с Unity (дополняется интерфейс в Unity, автоматически создаются необходимые файлы и сам Wwise проект).

  2. Есть Unity Asset Store, где можно купить такие решения как лаунчер. Так как мы собираемся дистрибьютить игру через Game Jolt/Itch.io, то нужно как-то самим позаботиться об обновлении клиента (не скачивать же игрокам сотни мегабайт из-за каждого малого патча), а самим лаунчер изобретать тоже дело затратное по времени. В итоге просто приобрел готовое решение в Unity Asset Store и легко встроил его в свой проект. Благодаря большому коммьюнити выбор таких решений довольно велик.

Не очень понял, если на сервере уже есть очереди/сокеты для взаимодействия между потоками, почему не выделить эти потоки в процессы, и нет больше проблемы GIL?

Абсолютно верное замечание. Действительно через трюк с распаралелливанием самих процессов можно обойти GIL ограничение в Python и добиться более высокой производительности CPU.
Я попытался в какой-то момент заменить использование multithreading на multiprocessing и получилось довольно успешно, однако всплыли другие моменты. Когда используешь multiprocessing, то для организации общения процессов через очереди нужен отдельный процесс, который эти очереди будет контроллировать - multiprocessing.Manager(), то есть в итоге мы получаем 5 процессов (игра, сокет для 1-го игрока, 2-го, 3-го и менеджер очередей, чтобы процессы могли обмениваться информацией). Однако каждый процесс - это целый интерпретатор питона, а это гораздо большее потребление как CPU, так и оперативной памяти, поэтому в целях экономии ресурсов (чтобы арендовать более слабые виртуальные машины) я остался на multithreading, поскольку оптимизации заметной глазу не получил, однако эксперимент сам удался, поэтому если бы сервер перестал справлятся, а с Python не хотелось бы уходить, то именно такой способ мог бы помочь, всё верно.

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

Тут возможно небольшое недопонимание. Сейчас в-принципе так оно и есть, на каждую сессию создается свой отдельный процесс описанный в "Архитектура игровой сессии: общая картина" и в нём, всё верно, 4 потока (3 io-bound сокета и сама cpu-bound игра). Процессы с сессиями никак друг с другом не общаются, они просто кладут информацию о себе в DynamoDB. Оркестрацией же этих процессов с сессиями занимается другой master процесс - main server, он хранит информацию о каждом процессе с сессией (её pid в системе) и спавнит новый процесс, если игрок просит новую сессию, или же даёт игроку выгрузку о всех сессиях с DynamoDB, если того интересует список текущих сессий. И вот эта вот связка - master процесс и процессы с сессиями работает на каждой машине в Auto Scaling группе.

Да, перечитал еще раз и стало понятно, спасибо. Тогда по идее GIL и не должен оказывать большого влияния на такую архитектуру...

Сервис AWS, представляющий защиту от DDoS, AWS Shield, доступен по подписке за 3000 долларов в месяц

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

Без сарказма было бы более информативно, пока я так и не понял, в чём именно тут упрёк.

и мы попросим повара не использовать просроченные продукты

Это какой-то намёк на то, что AWS Shield уже стал deprecated и есть решения получше?

Я понимаю, что защита от DDoS тема довольно сложная и есть разные способы защитить себя от разных видов атак в AWS. По идее базовая защита уже должна быть включена благодаря AWS Shield Standard (https://docs.aws.amazon.com/waf/latest/developerguide/ddos-standard-summary.html) "All AWS customers benefit from the automatic protection of Shield Standard, at no additional charge. Shield Standard defends against the most common, frequently occurring network and transport layer DDoS attacks that target your website or applications". Вроде бы то, что нужно, так как мой игровой сервер как раз работает на TCP протоколе и должен покрываться данной защитой, однако неизвестно, когда эта защита работает, как она работает и когда именно она меня защитит. На странице AWS Shield Pricing (https://aws.amazon.com/shield/pricing/) можно и вовсе увидеть, что "AWS Shield Standard is automatically enabled when you use AWS services like Elastic Load Balancing (ELB), Application Load Balancer, Amazon CloudFront and Amazon Route 53.", то есть я даже не могу быть уверен, что данная защита покрывает мои EC2 машины, к которым у меня происходит прямое подключение, не через ELB. Поэтому для явной защиты от DDoS я все-таки пока могу полагаться только на AWS Shield Advanced, который и подключается по подписке за 3000 долларов в месяц, отправляет свою активность в CloudWatch и явно защищает EC2 машины (https://aws.amazon.com/shield/pricing/) "AWS Shield Advanced is a paid service that provides additional protections for internet-facing applications running on Amazon Elastic Compute Cloud (EC2), Elastic Load Balancing (ELB), Amazon CloudFront, AWS Global Accelerator, and Amazon Route 53.".

Я не знал, что есть бесплатный AWS Shield Standard и платный AWS Shield Advanced. Учитывая, что DDoS это, в том числе, дополнительные расходы на трафик и процессорное время, что выгодно Амазону и не выгодно пользователю, показалось немного цинично не предоставлять хотя-бы базовую функциональность для защины от такого рода атак.

Используйте протокол TCP, а не UDP

в сегодняшних реалий почти все обеспечены более менее хорошим интернетом и TCP

  1. На мобилках это не так

  2. Много регионов с хреновым инетом (привет Южной Америке)

Есть вещи, для которых желательно использовать TCP (например, отправка важных действий), а для передачи движения вполне ок использовать UDP. Reliable и unreliable сообщения и всё такое.

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

А в какую сумму DynamoDB выходит? Если используете его чисто для сессий, то не проще бы было на какой-то тачке поднять тот же Редис?

DynamoDB выходит бесплатно, AWS предоставляет 25 GB storage в рамках free tier (https://aws.amazon.com/dynamodb/).
Действительно можно было самим поднять Redis, однако тогда я был бы ответственнен за его состояние и пришлось бы ему выделять отдельную машину, в то время как DynamoDB - это serverless сервис, его легко поднять (просто создать таблицу) и данные в нём легко можно посмотреть/изменить в интерфейсе AWS, это делает его очень удобным и быстрым решением в нашем случае.

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

EVE Online вон [на python2.7 работает](https://www.eveonline.com/news/view/stackless-python-2.7) и до 50 тысяч игроков онлайн в одном мире (правда у них отдельный сервер на каждую одну или несколько систем) держат как-то. Используется Stackless Python c микро-потоками (зелёные потоки) чтобы обойти GIL.

@Silver3310 отличный пост, видно что вы горели своей игрой что и привело к законченному результату.

Позвольте добавить пару мыслей для следующего сетевого проекта:
- go подойдёт гораздо лучше для разработки сервера, но если оставатся в контексте Python, то
- изучите библиотеку asyncio и вообще асинхронное программирование. Это весело и упрощает многие вещи, в вашем примере можно например от очередей отказаться.
- вместо передачи строки "110000" можно передавать int (binary) и парсить его битовыми операциями, которые в разы быстрее строковых split()
- как уже указали выше, для данной конкретной задачи (где не требуется передавать много сложных данных) UDP отлично бы подошёл, стоило бы с этим поиграться.

Спасибо огромное! Обязательно почитаю про Stackless Python и спасибо за наводку касаемо Go. По поводу UDP также согласен, стоило и его попробовать перед TCP.

По поводу asyncio, я пробовал писать сервера на асинхронных Python фреймворках (aiohttp и FastAPI) и, если честно, могу согласится, что это весело, но затрудняюсь сказать, что это упрощает вещи, писать везде async/await и думать над каждый раз над тем, где корутина должна вернуть управление в event loop как-раз таки делали процесс разработки тяжелее и менее очевидным (как по мне). Ну и плюс тут нужно будет все библиотеки на asyncio переделать, может с built-in сокетами такое и получится, а вот на счёт того же pygame/pymunk не очень уверен. Да и как кстати от очередей в этом случае можно отказаться для меня тоже пока не очень очевидно. Впрочем, об использовании asyncio в данном проекте я в-прицнипе даже не задумывался, но как эксперимент - звучит весело.

На счёт того, чтобы отправлять binary вместо строки, я так понимаю тут идёт речь о допустим передаче b'110000' вместо '110000'? Как идея интересная, это получается не нужно будет декодить сообщение по получению, а можно сразу его использовать. Хотя вы уточнили, что речь идёт об int (binary) и я не очень понял, что тут имелось в виду, если передавать чисто цифры, то допустим в случае, когда серверу нужно передать состояние игры клиенту, там ведь много данных, там есть и float с плавающей точкой, и разделители между значениями и группой значений, тут непонятно, как такое обработать.

почитаю про Stackless Python

Имхо, Stackless Python и фреймворки типа Tornado ретроспективно были лишь этапами к async/await встроенным в язык, поэтому почитать про них можно, но применять уже бессмысленно, asyncio делает то же самое гораздо менее многословно.

нужно будет все библиотеки на asyncio переделать

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

на счёт того же pygame/pymunk не очень уверен

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

Да и как кстати от очередей в этом случае можно отказаться для меня тоже пока не очень очевидно.

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

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

Хотя вы уточнили, что речь идёт об int (binary) и я не очень понял, что
тут имелось в виду, если передавать чисто цифры, то допустим в случае, когда серверу нужно передать состояние игры клиенту, там ведь много данных, там есть и float с плавающей точкой, и разделители между значениями и группой значений, тут непонятно, как такое обработать.

я имел в виду конкретно ваше сжатие информации от клиента из JSON в строку "110000", а ведь это просто битовое представление цифры 48.

Но вообще решение представлять данные как binary или text зависит от многих факторов и от вашего транспорта в том числе, например, пока посылка данных влазит в размер TCP пакета, лично я не вижу смысла её далее сжимать.

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

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

Ну, это скорее потоки, нежели чем процессы (процессом тут является сама игровая сессия, в которой потоки игры и игроков, а вернее их сокетов крутятся), но сама мысль верная, да.

ну и все корутины имеют общую память и очередь не нужна

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

я имел в виду конкретно ваше сжатие информации от клиента из JSON в строку "110000", а ведь это просто битовое представление цифры 48.

Аа, ну да, интересный способ :) Но это то, что клиент передает серверу (нажатие клавиш), эта строка и так очень маленькая. А вот сервер передаёт клиенту уже большой массив данных (`1.2.0.0.800.0.10.20.0.5;1.3.1200.0.2000.0.10.20.0.5;1.4.0.1200.2000.1200.10.20.0.5;...), это координаты объектов, их радиус, наклон, айди раскраски и прочее, и я вот думал, как такую строку представить в виде int (binary) :D

Не воспринимайте меня как специалиста в этой области, я просто люблю асинхронное программирование.

Оно и видно :) Если решите написать свой игровой сервер с асинхронным подходом, желаю только успеха!

почему категорично только go?
есть такие варианты как node.js, erlang/elixir и др.
у всех есть свои как недостатки и достоинства.
например я легко представляю как просто было бы это реализовать на erlang.

В EVE Online Питон довольно странно используется, т. к. они много всего перенесли именно в сишную часть.

А почему не gRPC/protobuf? Вроде из коробки и TCP, и оптимизированный бинарный протокол, и библиотеки как для Unity так и для python.

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

Надо помнить что для Python реализация grpc оч тормозная.

Ну гугловая реализация и так на c/c++, но похоже они оч старались.

Может оч надо было обосновать почему надо все перепиасать на какой нить golang ?

каждый генерирует свой мир и вычисляет свою физику, из-за чего каждый игрок видит разную картину.

Почему физика не детерминирована? Там генератор псевдослучайных чисел выбирает куда улетит объект?

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

Я считаю что создавать для каждого коннекта отдельный процесс или thread параллели как либо это не нужной задачей.

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

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

Я рекомендую делать 2 процесса - первый на websocket соединения, второй для расчета команд из очереди (если у вас там npc есть - там же будут расчитываться вместе со всей физикой)

Я делаю так в своем проекте с 2D открытым миром

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории