Привет! Меня зовут Brent «Brentmeister» Randall (Брент Рэндалл). Я — инженер из команды Gameplay Integrity, которая занимается игрой VALORANT. В сферу нашей ответственности входит система сборки игры, фреймворки, используемые для автоматизации различных задач, производительность игрового клиента и серверов. Именно последнему пункту этого списка и посвящена данная статья. Я поделюсь с вами историей поиска подходов, позволивших вывести производительность наших серверов на оптимальный уровень.

На самых ранних этапах разработки проекта мы уже знали о том, что VALORANT отличается весьма жёсткими требования к производительности игровых серверов. Надеюсь, мне удастся дать вам некоторое представление о том, почему это так, и о том, как были достигнуты наши амбициозные цели. В самом начале серверный кадр (server frame, цикл обработки данных на сервере) длился 50 мс. А после завершения оптимизации нам удалось сократить это время до менее чем 2 мс. Всё это сделано благодаря анализу и оптимизации кода проекта, а так же — благодаря подстройке «железа» и тюнингу ОС.
О важности сетевого кода
В онлайн-шутерах (и даже в VALORANT) имеется более или менее ярко выраженная проблема, известная как «преимущество нападающего» (peeker’s advantage). Мы записали видео о сетевом коде игры и о преимуществе нападающего. У нас — в блоге, посвящённом техническим вопросам, вышла статья на эту тему.
Если рассказать об этом в двух словах, то получится, что ключевой аспект геймплея VALORANT заключается в том, чтобы занять стратегическую позицию и её удерживать. Удержание позиции способно оказаться невозможным в том случае, если другие игроки смогут, перемещаясь по карте и выглядывая из-за укрытий, убивать тех, кто пытается защищать позиции, не давая им шанса отреагировать на угрозу. Виноваты в этом задержки. Они отчасти зависят от особенностей передачи данных по сети, а отчасти — от тикрейта (tickrate, частота обновления) сервера. Мы выяснили: чтобы дать защитникам время, необходимое на то, чтобы отреагировать на действия нападающих, VALORANT нужны серверы с тикрейтом 128. Если вам интересно узнать о том, как именно мы нашли это значение — вот статья, в которой мы глубоко исследуем тему преимущества нападающего.
Оптимизация кода
Когда мы поразмышляли о хостинге 128-тиковых игровых серверов, оказалось, что главной нашей проблемой являются ресурсы CPU. Мы должны быть способны обработать весь кадр за 7,8125 мс, но если так и сделать, то один сеанс игры полностью загрузит всё процессорное ядро!
На следующей схеме показано, сколько сеансов игры можно выполнять на одном ядре.

Если для обработки одного сеанса игры полностью выделяется 1 ядро — хостинг серверов в условиях роста интереса к игре оказывается непомерно дорогим удовольствием. При этом мы изначально собирались предлагать возможность игры на 128-тиковых серверах бесплатно, а не в виде некоей премиум-опции, за которую игрокам необходимо платить. После того, как мы всё посчитали, стало понятно, что нам нужен показатель игр на ядро (games per core, GPC), превышающий 3. Чтобы было понятнее то, о каких масштабах идёт речь — поясню: обычно мы применяем серверы с 36 процессорными ядрами, поэтому каждому физическому игровому серверу нужно обеспечивать работу 108 экземпляров игры, или — 1080 игроков. Даже показатель в 3 GPC означал серьёзные траты на инфраструктуру, но руководство Riot понимало и поддерживало наши цели, связанные с производительностью серверов.
Возьмём вышеупомянутые 7,8125 мс, разделим их на 3 GPC, и получим 2,6 мс. Правда, тут нельзя забывать о необходимости зарезервировать 10% ресурсов на разные служебные нагрузки, в число которых входит работа ОС, планирование ресурсов, другие программы, работающие на том же хосте, что и игра. После вычислений мы вышли на целевой показатель в 2,34 мс на серверный кадр. А когда мы взглянули на исходные характеристики VALORANT, оказалось, что этот показатель у нас равняется 50 мс. Это означало, что нам предстоит пройти долгий путь. Выглядело это всё как проект, для реализации которого понадобится привлечь всю команду разработки игры.
Разбиение задачи на части
«Сделать сервер в 20 раз быстрее» — это задача, к которой не так то легко подступиться. Поэтому мы применили лучший инструмент инженера-программиста: разбиение большой и страшной задачи на более мелкие подзадачи, вполне поддающиеся решению. Для начала нужно было выяснить то, где именно тратится время, складывающееся, в итоге, в 50 мс, что позволило бы нам постепенно это время сокращать. Мы собрали технических руководителей VALORANT и поговорили о том, на что, вероятнее всего, тратится больше всего процессорного времени. В результате получился следующий список:
Синхронизация данных (replication).
«Туман войны» (FoW).
Сеть (network).
Анимация (animation).
Игровой процесс (gameplay).
Передвижение игроков (movement).
Снаряжение (equippable).
Персонаж (character).
Физика (physics).
Другое (other).
Вооружённые этим списком, мы создали систему, которая позволяла программистам помечать код игры и распределять его участки по разным категориям. Каждая строчка выполняемого кода приписана к одной из вышеперечисленных категорий с использованием системы макрокоманд. Мы, кроме того, применили концепцию подсистем, которая позволила детальнее анализировать более крупные системы. Мы назвали эту систему ValSubsystemTelemetry.

Когда игра выполняется, мы собираем сведения о коде, который относится к разным категориям. Это позволяет узнать о том, сколько времени система тратит на обработку кода из каждой категории.
Эффективное использование технологий Riot: Analytics Platform
Теперь, когда мы разбили код на категории и начали собирать сведения о производительности, возникает вполне закономерный вопрос: что со всем этим делать?
Один из аспектов работы в достаточно большой компании, занимающейся разработкой игр, вроде Riot, заключается в том, что одни команды могут задействовать существующие инструменты, которые создаются и поддерживаются другими командами. В нашем случае, например, центральная команда Riot создала проект, который называется Analytics Platform (аналитическая платформа). Это — инструмент, который позволяет программистам, работающим в Riot, отправлять материалы в хранилище больших данных, а потом разными способами эти материалы визуализировать.
Вот несколько примеров того, как мы визуализировали данные о производительности VALORANT.


Не пользуясь подобными данными, мы легко можем попасть в ситуацию, когда неэффективные изменения, касающиеся кода или контента игры, могут незаметно попасть в игру. Подобное может продолжаться неделями — до тех пор, пока не наберётся критическая масса таких вот неудачных изменений, после чего замедление игры станет заметно разработчикам или игрокам.
Искать причины подобных замедлений, разбирая списки изменений за несколько недель — это долго и дорого. А вот когда применяется постоянный анализ производительности игры — это значительно облегчает решение подобных задач. Так, на втором из вышеприведённых рисунков можно видеть проблему, возникшую где-то между изменениями с номерами 445887 и 446832, которая привела к увеличению времени, необходимого на синхронизацию данных (оранжевая линия). Подобные визуализации позволяют нам, при возникновении проблемы, находить её причину, анализируя сравнительно небольшое количество изменений, и быстро приступать к её исправлению.
Бюджеты производительности подсистем
Теперь, благодаря системе визуализации и проверки данных, у нас появилась возможность назначить подсистемам бюджеты производительности (performance budget) и следить за нарушениями этих бюджетов. Технические руководители VALORANT собрались снова и обсудили то, каких показателей разумно будет ожидать от различных подсистем. Выбор целевых показателей был, во многом, основан на том, в каком состоянии подсистемы были на момент обсуждения, и на том, какие именно возможности для их оптимизации видели эксперты. После этого перед каждой командой и перед каждым экспертом стояла чёткая цель, что позволило нам вместе работать над тем, чтобы довести производительность игры до уровня, на котором она будет готова к релизу.
Ниже показаны данные о текущей и целевой производительности вышеописанных подсистем, собранные на одном из ранних этапов работы.

Рассмотрим два конкретных раздела, чтобы наглядно продемонстрировать то, как именно мы оптимизировали производительность. Мы, в первую очередь, разберёмся с синхронизацией данных, так как эта подсистема нуждалась в самых серьёзных изменениях. Затем мы обратим внимание на подсистему анимации, так как те изменения, которые мы в неё внесли, весьма характерно иллюстрируют и то, как мы улучшали другие подсистемы.
Подсистема синхронизации данных
В Unreal Engine 4 (UE4) имеется система, предназначенная для синхронизации состояния клиентов и сервера по сети. Там она называется Property Replication, а мы, для краткости, называем её просто «replication». Это — замечательная система, позволяющая быстро создавать прототипы персонажей, способностей, возможностей, для функционирования которых необходимо взаимодействие клиента и сервера. Речь идёт о том, что разработчик может просто пометить переменную как replicated, после чего она автоматически будет синхронизирована между сервером и клиентами.
К сожалению, система эта работает достаточно медленно. Для её функционирования необходимо, при обработке каждого серверного кадра, сканировать все replicated-переменные. Затем — нужно сравнивать их значения с их последними известными значениями для каждого из 10 клиентов. И наконец — нужно подготовить и упаковать данные для отправки клиентам. В ходе выполнения синхронизации, по сути, осуществляется чтение случайных участков памяти. Делается это медленно и создаёт большую нагрузку на кеш. Независимо от того, изменилось ли синхронизируемое состояние, или нет, производятся проверки переменных. Я считаю подобные системы, основанные на «опросе» неких сущностей, примером анти-паттерна высокопроизводительной разработки.
Решением этой проблемы стало использование другого сетевого инструмента UE4. Это — Remote Procedure Calls (RPCs, вызов удалённых процедур). Механизм RPCs позволяет серверу вызывать по сети функции, которые выполняются на одном или на нескольких клиентах. Использование RPCs при реализации игровых событий, меняющих состояние системы, ограничило нагрузку на серверные кадры, в которых происходят изменения. Такая вот «push»-модель синхронизации данных гораздо производительнее той, что использовалась раньше. Правда, у неё есть и минусы. В частности, при её использовании проектировщики и программисты уже не могут полностью положиться на автоматику. Им приходится тщательно планировать устройство RPCs и заниматься обработкой таких ситуаций, как повторное подключение клиента к серверу. Но мы выяснили, что во многих случаях переход от Property Replication к RPCs даёт улучшение производительности в масштабе от 100 до 10000 раз!
В качестве примера рассмотрим сведения о здоровье игрока. Один из способов организации синхронизации этого значения между клиентами и сервером заключается в пометке соответствующей переменной с помощью replicated. При таком подходе игровой сервер, обрабатывая каждый кадр, будет проверять значение и, если оно изменилось, будет уведомлять об этом клиентов, которым нужны эти данные. При использовании RPCs, если в игрока выстрелили, мы, вероятно, просто отправим клиентам событие ShotHit со сведениями об уроне. Клиенты будут синхронизированы, самостоятельно вычитая из здоровья урон, нанесённый игроку.
Подсистема анимации
Анимация создавала огромную нагрузку на наши серверы. Для того чтобы точно понять — достиг ли выстрел цели — нам нужно было выполнять на сервере ту же анимацию, которую игроки видели на своих клиентских системах. Регистрация удачных выстрелов в VALORANT работает на основе буфера исторических данных, в котором сохраняется позиция игрока и состояние анимации. Когда сервер получает от клиента пакет со сведениями о выстреле, он «перематывает» буфер исторических данных, восстанавливая сведения о позиции игрока и об анимации на момент выстрела, и решает, было ли попадание по цели. Изначально мы проводили вычисления, относящиеся к анимации, и заполняли этот буфер для каждого кадра. Но, после тщательного тестирования и сравнения результатов, мы обнаружили, что можно анимировать лишь каждый 4-й кадр. А при «перемотке» буфера можно интерполировать (lerp, Linear Interpolation) сохранённые анимации. Это привело к снижению нагрузки на систему, создаваемой анимацией, на 75%.
Ещё одной важной находкой стал тот факт, что, если говорить о нагрузках больших масштабов, самым важным типом производительности является амортизированная производительность (amortized performance) серверов. Представьте себе сервер VALORANT, на котором выполняется примерно 150 игровых матчей. В любое время игроки примерно 50 из этих матчей будут пребывать в состоянии покупки снаряжения. Игроков в это время будут защищать барьеры, непроницаемые для выстрелов. Мы поняли, что нам, на стадии покупки снаряжения, вообще не нужно считать на сервере что-либо, относящееся к анимации. Эти вычисления можно просто отключить. Если в этот момент посмотреть на матч с точки зрения сервера, то окажется, что игроки, во время покупки снаряжения, находятся в позе ожидания. Это помогло снизить нагрузку, создаваемую анимацией на систему в течение одного раунда, примерно на 33%!

Производительность игры в реальном мире
Теперь вы вполне представляете себе то, как мы оптимизировали код. Но производительность — это гораздо больше, чем код. Производительность — это ещё и платформа, на которой выполняется программа. Поэтому сейчас поговорим о том, что вызывало огромные проблемы с производительностью — об операционной системе и о «железе».
Для того чтобы адекватно проверить то, как игра будет работать в реальных условиях, нужно было разработать нагрузочный тест. Нам надо было знать о том, как будет чувствовать себя сервер, на CPU которого выполняется более 100 экземпляров игры. Создание подобного нагрузочного теста было чрезвычайно важно для последующего успешного выпуска VALORANT. Это позволило нам спрогнозировать то, сколько именно процессорных ядер понадобится на одного игрока, и позволило решить массу проблем, которые возникают только в условиях высокой нагрузки на сервер. Оказалось, что всё не так уж и просто, что реальный мир описывается более сложными цифрами, чем те 7,8 мс и 3 игры на ядро, о которых я упоминал выше.
(Примечание редактора: подробности о нагрузочном тестировании VALORANT можно найти здесь!).
Для начала взглянем на следующий график. На его оси Y показано время кадра, а на оси X — количество экземпляров игры.

Великолепные 1,5 мс достигнуты лишь при запуске 1 экземпляра игры. А вот 168 экземпляров игры доводят этот показатель до примерно 5,7 мс.
Что ж тут такое происходит? Для того чтобы разобраться в том, почему это случилось, и в том, как мы с этим справились, сначала надо поговорить об архитектуре современных CPU.
Архитектура CPU

Если внимательно посмотреть на предыдущую схему — можно заметить несколько важных моментов. А именно — у каждого ядра имеется собственный L1- и L2-кеш, а вот L3-кеш, более объёмный, ядра делят друг с другом. Когда на хосте работает лишь один игровой сервер, он сам пользуется всем объёмом L3-кеша, что приводит к меньшему количеству промахов кеша, а это значит, что процессорные ядра тратят меньше времени на ожидание выполнения запросов к памяти. Именно поэтому в тестовых сценариях с низкой нагрузкой наши серверы работают невероятно быстро, а при росте нагрузки начинают замедляться. Дело в том, что по мере роста количества сеансов всё новые и новые экземпляры игры добавляют в кеш свои данные. Мы, вместе с командой специалистов по облачным технологиям из Intel, провели некоторые измерения, направленные на проверку того, что мы тут не столкнулись с перегревом процессоров, или с другими факторами. В итоге оказалось, что в нашем случае всё упирается в производительность кеша.
Сотрудничество с Intel
К нашей удаче в рукаве у Intel было несколько тузов — средств для измерения показателей различных платформ и аналитических инструментов. Мы тогда всё ещё пользовались не самыми новыми процессорами Intel Xeon E5, в которых применялся инклюзивный кеш. Смысл тут заключается в том, что каждая строка кеша, которая присутствует в L2-кеше, обязательно должна быть представлена и в L3-кеше. Если строка вытесняется из L3-кеша, то она должна исчезнуть и из L2-кеша! Это значит, что несмотря на то, что каждое ядро имеет собственный L2-кеш, возможна ситуация, в которой ядра друг с другом «сражаются», вытесняя из L3-кеша чужие данные, что приводит и к их вытеснению из чужих L2-кешей.
Не кажется ли вам, что это совершенно несправедливо? В процессорах Intel Xeon Scalable компания Intel перешла на использование неинклюзивного кеша, что полностью решило вышеописанную проблему. Переход на более современные процессоры Xeon дал значительное улучшение производительности наших серверных приложений. Мы всё ещё сталкиваемся с эффектом конкуренции за ресурсы L3-кеша, но при этом, даже используя почти такие же, как раньше, тактовые частоты, мы видим рост производительности примерно на 30%.
NUMA
Нам хотелось ещё сильнее повысить производительность подсистемы памяти. Чтобы разглядеть путь к этому улучшению, сначала надо разобраться с ещё одним аспектом архитектуры современных CPU — с NUMA (Non-Uniform Memory Access, неравномерный доступ к памяти). В серверных архитектурах часто применяется два (или большее количество) процессорных сокетов. У каждого из сокетов есть прямой доступ к некоторой части системной RAM, они могут обмениваться данными по межкомпонентному соединению. Это позволяет повысить пропускную способность памяти (в 2 раза больше линий связи), но узким местом тут может стать как раз межкомпонентное соединение. Если вернуться к вышеприведённой схеме архитектуры процессора, можно увидеть, что там показан упрощённый пример архитектуры NUMA с двумя сокетами. Если бы операционная система могла бы выделять память и процессорные ресурсы так, чтобы не слишком нагружать межкомпонентное соединение…
Как оказалось, многие современные ОС знают о NUMA и могут поступать именно так. Например, один из способов сделать это в Linux — применить при запуске процесса numactl. В VALORANT мы запускаем экземпляры игровых серверов на различных узлах, пользуясь командой такого вида:
numactl --cpunodebind={gameid % 2} --membind={gameid % 2} ShooterGameServer
Полноценное использование этой архитектуры позволило нам, благодаря совсем небольшому усовершенствованию, поднять производительность примерно на 5%. Операции доступа к памяти, выполняемые в пределах одного NUMA-узла, раньше составляли примерно 50% от общего числа операций, а после этого доля локальных операций выросла до 97-99%! И, в дополнение к росту на 5%, уровень производительности разных серверов оказался более однородным.
Планировщик ОС
Когда мы мониторили хост, на котором работает игровой сервер, мы заметили интересную закономерность. Уровень использования ядер колеблется между 90 и 96%, но никогда не достигает 100% — даже тогда, когда на хосте запускается вдвое больше игр, чем он должен нормально поддерживать. Мы подозревали, что причиной этого был планировщик ОС. Для того чтобы решить вопрос — мы прибегли к Adaptive Optimization — к профилировщику из набора приложений для Intel PMU, а так же — к Linux-утилите perf, чтобы исследовать события планировщика. То, что мы применяем Linux, означает, что у нас, кроме прочего, есть возможность заглянуть в исходный код планировщика.
В ходе исследований выяснилось, что современные Linux-дистрибутивы используют планировщик Completely Fair Scheduler (CFS). Это — весьма интеллектуальный, хорошо оптимизированный планировщик. Одна из оптимизаций заключается в том, что он пытается держать процессы на одном ядре, предотвращая их миграцию и запуск на других ядрах. Делается это для того, чтобы позволить процессам повторно использовать всё ещё актуальные строки кеша. Ещё одна причина, вероятно, заключается в предотвращении неоправданной активации простаивающих ядер для выполнения незначительных задач. Применение механизма учёта стоимости миграции процесса на другое ядро приводит к тому, что процесс, прежде чем его позволено будет переместить на доступное ядро, ждёт некоторое время на сильно загруженном ядре. Стандартное значение показателя стоимости миграции процесса в нашем дистрибутиве составляло 0.5 мс.
В случае с VALORANT 0,5 мс — это немалая часть нашего целевого показателя в 2,34 мс. За это время можно обработать почти четверть серверного кадра! А шансы того, что в кеше сохранятся актуальные строки, которыми сможет воспользоваться процесс, запущенный на том же ядре, на котором он запускался раньше, равны 0%. Пока один игровой сервер бездействует между кадрами, другие серверы по полной используют кеш. Снизив стоимость миграции процессов до 0, мы гарантировали такое поведение планировщика, когда он немедленно перемещает на любое доступное ядро процесс, который нужно запустить. Это позволило нам гораздо полнее использовать ресурсы CPU и дало прирост производительности ещё в 4%. Мы, кроме того, заметили, что время простоя отдельных ядер во время работы хоста упало практически до нуля.
Энергосберегающие режимы процессорных ядер
Ещё одной областью, в которой мы чётко видели перспективы улучшения производительности, было управление энергосберегающими режимами процессорных ядер (C-states, C-состояния), в частности — тем, в какие режимы позволено переходить процессору. Когда многоядерный CPU выполняет код, он позволяет отдельным ядрам переходить в различные режимы энергосбережения. Под небольшой нагрузкой ядра часто переходят в режимы с пониженным энергопотреблением, что позволяет экономить энергию. Но после увеличения нагрузки, когда ядрам нужно перейти в режимы с более высоким потреблением энергии, этот переход может потребовать некоторого времени. Если нагрузка отличается высокой цикличностью, как в случае с игровым исерверами, которые обрабатывают кадры и останавливаются, CPU начинает достаточно часто менять режимы работы ядер. Каждая смена состояния отличается определённой задержкой, которая плохо влияет на производительность. Ограничив режимы, в которых может работать процесс, «высокими» состояниями C0, C1 и C1E, мы смогли добиться увеличения на 1-3% доли игровых сеансов, которые стабильно можно запускать на хосте. Это особенно сильно отразилось на стабильности серверов, загруженных на 60-90%, когда множество ядер, из-за пониженной нагрузки, могли переходить в режимы пониженного энергопотребления.
Технология Hyper-Threading
Hyper-Threading (гиперпоточность) — это технология, позволяющая одновременно запускать на одном физическом ядре два потока. При применении этой технологии некоторые ресурсы ядра используются процессами совместно (вроде кешей), а некоторые (вроде разных вычислительных блоков) — дублируются.
Как именно всё это устроено — зависит от конкретного CPU. На ранних этапах разработки время кадра у нас составляло 25 мс, и мы обнаружили, что отключив Hyper-Threading, можем выйти на 20 мс. Налицо рост производительности на 25%! Правда, наши друзья из Intel отнеслись к этому скептически. Дело в том, что, по их мнению, учитывая особенности нашей нагрузки, мы вполне могли выжать больше из имеющегося у нас железа, использовав виртуальные ядра, предлагаемые технологией Hyper-Threading. Когда мы, позже, снова включили Hyper-Threading, производительность выросла на 25%.
Как это получилось? Мы, в ходе оптимизации проекта, довели время серверного кадра до показателя, который был ниже целевых 7,8125 мс. Мы перешли на процессоры Intel Xeon Scalable, отличающиеся улучшенной работой кеша и повышенной производительностью в режиме Hyper-Threading. Мы подстроили под наши нужды планировщик ОС, сделав так, чтобы он эффективнее использовал все доступные ядра. Мы запретили переводить ядра в режимы энергосбережения, идущие после C1E, сделав это для того, чтобы обеспечить более быстрый отклик системы при росте нагрузки. Мы внести в проект и многие другие улучшения.
О важности измерений
Главные выводы, которые можно из всего этого сделать, заключаются в том, что каждое приложение обладает собственным профилем производительности, и что при оптимизации разных приложений применимы различные соображения относительно целевых показателей. Даже в случае с одной и той же программой может случиться так, что сегодня у неё одни требования к производительности, а через год — уже совершенно другие. Единственный способ, который позволяет с уверенностью заниматься оптимизацией приложений — это создание воспроизводимых тестов и измерения.
Предположим, вы собрали список «полезных советов по повышению производительности», которые нашли в статье про разработку игр. Если применить эти советы бездумно, не принимая во внимание нужды вашего проекта, то вполне можно не улучшить его производительность, а ухудшить.
Другие приёмы оптимизации производительности
Источник сведений о времени
В играх, как правило, достаточно часто используются сведения о времени. Для наблюдения за временем обычно применяют соответствующие системные вызовы. Операционные системы, работающие в виртуальных средах с гипервизором (вроде AWS) могут использовать виртуализованные источники времени, которые отличаются более низкой производительностью. На наших узлах AWS мы изначально использовали источник времени Xen, предоставляемый гипервизором Xen. Но позже мы перешли на источник времени tsc, который предоставляет инструкция CPU rdtsc. Это дало нашим игровым серверам прирост производительности в 1-3%.
Страшная история: такое бывает только в продакшне
Когда мы уже приближались к выпуску игры, мы заметили, что один из наших игровых серверов, выполняющий нагрузочные тесты, отстаёт от других. Его аппаратная часть была такой же, как и у других серверов, единственная его особенность заключалась в том, что на нём использовался весь стек инструментов, используемых нами для развёртывания проектов. Нагрузочные тесты, запускаемые на этом сервере, давали в два-три раза больше «медленных кадров», чем на других серверах, настройкой и развёртыванием которых я занимался вручную. Мы изучили проблему с разных точек зрения. В чём причина — в гипервизоре AWS, в других настройках, в дефектном «железе»? Ответа мы не нашли.
В итоге мы решили снова взглянуть на планировщик Linux, воспользовавшись perf sched, чтобы просто проверить — удастся ли найти какие-нибудь различия в том, как процессы выполняются на разных системах. Обнаружилось, что примерно каждые 5 секунд, будто по таймеру, запускается 72 процесса с именами scheduler_1 … scheduler_72. По одному процессу на каждое из виртуальных ядер. Эти процессы немедленно вытесняют с ядер работающие процессы игровых серверов. На сервере, пребывающем под высокой нагрузкой, это приводит к целому каскаду «тормозов». Оказалось, что система Mesos, которую мы использовали для развёртывания проектов, использует Telegraf для сбора метрик. Telegraf каждые 5 секунд делает DNS-запросы. Эти запросы перехватывает служба dcos_net, которая написана на Erlang.
В Erlang есть настройка, касающаяся того, сколько экземпляров планировщика ему можно запускать. По умолчанию эта настройка позволяет запускать по одному процессу на каждое ядро. Отсюда и 72 процесса. После того, как мы записали в этот параметр более разумное значение — 4 — проблема тут же исчезла. Отсюда можно сделать следующий вывод: жизненно необходимо проводить измерения производительности в среде, которая соответствует той, что будет использоваться в продакшне!
Итоги
В деле оптимизации крупных проектов очень легко, как говорится, не увидеть за деревьями леса. Даже в моём сегодняшнем небольшом рассказе всяческие технические мелочи могут очень быстро захватить всё внимание читателя. Очень легко потеряться в тонких деталях, во всяческих твиках и странностях.
Поэтому, занимаясь оптимизацией производительности, нужно постоянно пересматривать и подкреплять те цели, которые были установлены в начале работы. Нужно, чтобы вся команда помнила бы о них, следовала бы им, чтобы в нужный момент вы могли бы заручиться поддержкой тех, кто трудится рядом с вами.
Оптимизация кода — это немалая часть того, что называют «оптимизацией производительности». При этом важно разбивать то, что называется «производительностью приложения», на небольшие фрагменты. И не забывайте об оптимизации среды, в которой выполняется код (аппаратное обеспечение и операционная система). Это позволит вашим программам показать всё, на что они способны.
А самое главное — измеряйте, измеряйте и ещё раз измеряйте. Проведённые нами измерения различных аспектов VALORANT, в итоге, позволили выпустить игру, спрогнозировав потребности в серверных ресурсах с точностью до 1%. В результате игра успешно увидела свет, а наши игроки смогли бесплатно пользоваться 128-тиковыми серверами.
О, а приходите к нам работать? ? ?
Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.
Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.
Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.
Мы проводим соревнование по машинному обучению
Призовой фонд $13,600