Сервер считает ход игрока за 5–10 миллисекунд. Пока он занят одним, остальные ждут своей очереди. При сотне игроков последний получит ответ почти через секунду — для него игра превратится в слайд-шоу. Эта часть о том, как обслуживать всех одновременно и чем за это платит железо.

Очередь, которая растёт
Хорошим пингом в играх считается 60 миллисекунд. В этот бюджет входит всё: дорога пакета до сервера, само вычисление и дорога обратно. Последовательная обработка съедает бюджет уже на десятке игроков.

Выход один — считать параллельно. Тогда время ответа перестаёт зависеть от числа игроков онлайн.
Два режима PHP
PHP работает в двух режимах, и у каждого своя роль в игровом сервере.
FPM — режим коротких процессов. Управляющий процесс поднимает дочерние по мере запросов, каждый отвечает и умирает. Между запросами он ничего не помнит.
CLI — режим долгоживущего процесса. Вопреки названию, командная строка на экране для него не нужна: скрипт работает демоном в фоне. Именно здесь живут WebSocket-сервер, который держит игроков, и NPC, которым нужно ходить и атаковать своей жизнью.

Режимы вызывают друг друга. Из FPM можно запустить CLI-процесс функциями семейства exec — это стоит около 30 миллисекунд. Из CLI можно постучаться прямо в сокет FPM и поднять процесс там, эмулируя обычный запрос.
Чем за это платят
Процессорное время. Даже ничего не делающий CLI-процесс занимает около 0,7% процессора одного ядра. Сотня NPC, разведённых по отдельным процессам, съедает машину целиком.
Память. PHP резервирует под процесс примерно на 50% больше, чем тот фактически использует, — это видно через memory_get_usage. Плюс при каждом запуске в память заново грузятся все файлы фреймворка. Умножьте на сотню процессов.
Кеш врозь. Библиотеки вроде APCu кешируют объекты и массивы прямо в памяти без сериализации, но кеш FPM и кеш CLI не видят друг друга: в FPM он живёт до перезапуска, в CLI — до конца работы демона и только внутри него.
Обойтись одним режимом тоже нельзя. Поднимать CLI-процесс на каждый запрос игрока — это те самые 30 миллисекунд, половина всего бюджета пинга.
Что с этим делать
Opcache. Фреймворк компилируется и кладётся в общую память один раз, вместо загрузки в каждый процесс заново. На моём сервере это дало трёхкратное сокращение расхода памяти.
Пулы. Вместо процесса на каждого NPC — один процесс на карту, который синхронно ведёт всех её обитателей. Процессор тратит те же 0,7% на пул из сотни NPC, что тратил бы на одного бездельничающего демона.

Общая память. Раз APCu между режимами не работает, обмен данными между демонами приходится строить на других инструментах — например, на shmop и подобных расширениях для разделяемой памяти.
Что было дальше
Пулы и разделяемая память закрыли первый рубеж, но открытый мир на несколько физических серверов потребовал другой архитектуры. Как это устроено сейчас — в части про открытый бесшовный мир, она в списке ниже.
А какой предел одновременных игроков вы упирались на своём стеке и что оказалось узким местом — процессор, память или сеть?
История:
Масштабируемость и асинхронность
Выбор технологий, протокола и архитектурный шаблон Entity Component System
FPS, Ping, паузы между командами, интерполяция и экстраполяция
Event-driven паттерн, JSON-RPC и почему не сервисная (SOA) архитектура
Сетевая карта и задержка кадра (Latency frame) по RFC 2544 (1242)
Конвейер контента для MMO: импорт анимации, экипировка и обновление игры без патчей