Наконец-то доделал лимиты на код игроков

Наконец-то доделал лимиты на код игроков

s373r

Ранее я очень радовался, когда сделал параллельный запуск кода игроков (см. пост), т.к. получил феноменальные цифры: симулированные 4000 игроков с кодом длительностью 100 мс каждый выполнялся одновременно всего за ~1 секунду. Вау 🤩

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


Что за лимиты?


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


На данный момент реализовано два лимита:

- по CPU (или времени выполнения, условно 25 мс)

- по RAM (или не больше условно 2Мб памяти (в JS куче игрока))


Что например может быть в игровом коде?

- Из-за багов можно легко запустить бесконечный цикл (while (true) {}), который и выест процессорное время

- Или наплодит тонны объектов, которые съедят всю память сервера, например let giganticString = 'a'.repeat(50).repeat(1024).repeat(1024) (и это уже более 50Мб памяти в утиль).


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

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


На самом деле, учитывая технологии, которые используются, серверное приложение слабо в этом смысле отличается от главного приложения: браузера. Если вкладке не хватает памяти, браузер её "прибивает". Если вкладка (её JS код) активно что-то вычисляет, что влияет на отзывчивость интерфейса, то другие вкладки от этого не страдают.


В чём тогда в итоге подвох?


Чтобы точно считать время выполнения (без погрешности на переключение контекста процессором), пришлось ограничить количество выполняемых задач в момент времени. Например, у меня 8 логических ядер, т.е. 8 потоков сервер может выполнять в один момент. Простыми словами, пока выполняется код одних игроков, другие ждут.


Звучит как-то медленно, но так ли это? На самом деле нет. Время выполнения стало максимально прогнозируемым исходя из железа. Например: есть 500 игроков и код каждого выполняется 5 миллисекунд. Получается прогнозируемое (идеальное) время серверного тика:


Почему идеальное? Всё-таки помимо кода игроков, существует еще и код самого сервера, который обслуживает эти сотни/тысячи задач и сообщений, которые гуляют между потоками... и все остальные программы, запущенные в ОС


В моём случае, тик выходит 630 мс в среднем, и где-то 650-680 мс на холодном старте

Один из тестовых запусков


Итоги


Несмотря на ограничение в выполняемых параллельно задачах очень доволен что это всё вместе получилось реализовать. Бывали дни, когда я по 2-3 раза переписывал разные способы коммуникации между этим огромным количеством параллельных задач, лишь бы оно и работало правильно, и работало быстро. Но всё-таки иногда приходится выбирать.


Далее, можно начинать делать первый пользовательский API, например, создания юнитов. Или может начать с генерации случайных процедурных (для начала простых) карт. Наконец-то!


Вопросы

  • С чего посоветуете начать?
  • Какие-то еще ограничения для кода игроков, которые вы посоветуете сделать?
  • Стоит ли углубиться в какие-то детали реализации?

Напишите в комментариях к посту

Report Page