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

Антон Темербеков, автор Brawl Town и статьи на Хабре под именем Temer85, рассказал, как перестраивал серверные механизмы проекта на Go. В нагрузочном тесте один компьютер с процессором i7-7700K обслуживал до 2500 клиентов на игровой улице шириной 3600 пикселей. Это результат конкретного стенда, на котором одновременно работали сервер и генератор нагрузки. Как подчёркивает автор, тест не измеряет отдельный предел сервера.

Что именно проверял автор

Это авторский инженерный разбор, а не рецензированная научная работа. Точная дата публикации неизвестна: на странице указано «18 часов назад». Автор сравнивал старую и новую версии сервера на одном компьютере, используя один генератор нагрузки. Каждый прогон длился 30 секунд. В стенд входил i7-7700K с четырьмя ядрами и восемью потоками, поэтому сервер и генератор конкурировали за процессорное время.

Генератор запускал клиентов, подключённых по WebSocket. Сначала они отправляли случайные команды, но почти не вступали в драки и редко создавали эффекты. Чтобы смоделировать плотную боевую сцену, автор изменил их поведение: клиенты подходили к ближайшей цели, выравнивались по глубине, били с паузами, блокировали атаки и прыгали. Такой сценарий нагружал вычисления сильнее, чем простое создание соединений.

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

Меньше данных каждому клиенту

В первой версии сервер передавал полные записи о 220 ближайших бойцах. В плотной арене такой набор занимал 3116 байт, а остальных персонажей клиент не видел. В новой схеме сервер отправлял изменения: если поле не менялось, его можно было пропустить. Идентификаторы бойцов сократили с 32 до 16 бит, имена убрали из записей, а состояние толпы стали передавать в более простом формате и реже.

Частота обновления зависела от расстояния до игрока. Для 64 ближайших бойцов сервер отправлял полные записи 20 раз в секунду, для следующих 160 — 10 раз, а для остальных — облегчённые обновления 5 раз в секунду. В тесте плотной арены новый формат позволил передать сведения о 1118 бойцах в записи размером 1485 байт вместо данных о 220 бойцах в 3116 байтах. На прогоне с 1000 клиентами исходящий трафик, по данным автора, снизился с 65 до 37–40 МБ/с, хотя каждый клиент стал видеть больше персонажей.

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

Пространственный поиск зависит от формы мира

Улица в Brawl Town вытянута по горизонтали, и видимую область автор определял по координате x. Каждый игровой такт сервер сортировал бойцов в компактный массив пар «координата, идентификатор». Затем двумя бинарными поисками находил границы окна обзора и выбирал бойцов из нужного участка массива. Этот метод рассчитан на геометрию конкретной игры и не заменяет пространственные индексы во всех задачах.

Для поиска соседей и предварительного отбора кандидатов при столкновениях автор использовал равномерную сетку. В плотной арене в ячейке размером 200 пикселей могло находиться около 50 бойцов. Сетка с ячейками по 48 пикселей сокращала число кандидатов для расталкивания примерно до 15.

Изменили и данные, которые хранились в ячейке. Раньше сервер обращался к полной сущности размером около 400 байт, хотя на первом этапе проверки требовались только координаты и несколько признаков состояния. Теперь простые проверки отсекали большинство кандидатов, а полную сущность сервер считывал лишь для тех, кто мог оказаться ближе найденной цели или попасть под удар. В одном бенчмарке шага мира на арене с 2150 бойцами время сократилось с 2,39 до 1,88 мс. В таблице микробенчмарков для того же числа бойцов приведено другое сравнение: с 3,3 до 1,14 мс. Статья не проясняет условия этих двух измерений, поэтому считать их одним результатом нельзя.

Состояние клиента тоже стоит процессорного времени

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

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

Что означает результат «2500»

На улице шириной 3600 пикселей при 2500 нагрузочных клиентах игровой такт занимал 28–31 мс, укладываясь в бюджет 50 мс. При 3000 клиентах он вырос примерно до 55 мс, а сервер и генератор вместе исчерпали ресурсы компьютера. В сценарии с бойцами, собранными на одном экране, такт для 2500 клиентов занимал 28–48 мс; компьютер также был полностью загружен.

Важно учитывать, что процессорное время делили сервер и генератор. Сам генератор тоже нагружал компьютер: при 2000 клиентах его предыдущая версия использовала 4,5 ядра, а после оптимизации — 1,9. Поэтому общий предел машины нельзя приписать одному серверу. Отдельный компьютер мог бы дать серверу другой запас ресурсов, но измерения не показывают, насколько изменился бы результат.

Нагрузочные клиенты не равны реальным игрокам. Тест не проверял плавность игры на пользовательских устройствах, качество связи через настоящие сети и поведение сервера при разнообразных действиях людей. Автор отдельно описывает проверку согласованности состояний: в тесте с плотной ареной 24 клиента в течение 400 тактов сравнивали декодированные состояния с состоянием игрового мира. Это полезная проверка корректности, но она не заменяет независимое повторение нагрузочного теста.

Статья убедительна как отчёт об оптимизации на конкретном стенде: автор описывает сценарий, сравнивает версии и приводит измерения. Но без исходных данных, полного набора сценариев и внешнего воспроизведения результат нельзя считать универсальной характеристикой Go или доказанным пределом сервера. Оценка автора, что на более слабой боевой виртуальной машине сервер выдержит около 1000 игроков, тоже остаётся экстраполяцией, а не результатом измерения.

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

Источник: Антон Темербеков, «Как один сервер на Go держит 2500 бойцов в браузерной драке», полный текст на Хабре. Это авторское техническое описание проекта Brawl Town; дата публикации в доступном тексте указана относительно, точная дата не установлена.

Источники и литература

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

Фото: Yan Krukau / Pexels