Разработка игр с многотысячной пользовательской базой и постоянно накатывающимися обновлениями — комплексный процесс. Он включает в себя не только работу над новым контентом и фичами, но и оптимизацию процессов, чтобы все происходило гладко, и разработчики, аналитики, QA-инженеры постоянно получали новые билды.
DevOps — это и есть набор методов, благодаря которым можно сократить время разработки и ускорить выпуск обновлений. А CI/CD, или непрерывная интеграция и доставка — это не сколько технология, сколько целая культура, позволяющая чаще и надежнее производить небольшие изменения в игре с частыми коммитами, доставлять модули проекта в разные отделы и автоматизировать их тестирование. Все это представляет собой целый пласт работ — эдакий невидимый игрокам фронт.
Изначально, до старта работ над War Robots Remastered, у нас уже был выстроенный пайплайн CI/CD для всех проектов, и оригинальная War Robots не была исключением. Сам проект тогда в среднем собирался 40-100 минут. Но чем дальше продвигалась работа над ремастером, чем больше накапливалось проблем со скоростью сборок. Спустя полгода проект стал собираться от 3-х часов и больше с периодическими зависаниями, которые могли доходить до 7-10 часов. Это становилось совсем неприемлемым: QA в динамике не могли проверять билды, разработчикам тоже приходилось тратить время на ожидание, чтобы посмотреть результат или начать профилировать. Пришлось серьезно подумать над тем, как все это чинить и возвращать время сборок к исходному значению.
Как было раньше и какие проблемы это повлекло
Раньше наш пайплайн сборки приложения выглядел следующим образом. В качестве сервера CI мы использовали TeamCity от JetBrains: на War Robots тогда было выделено 22 агента, которые могли выполнять скрипты сборки. Они располагались на четырех физических компьютерах-нодах. Конфигурации нод приблизительно похожи, средняя тачка имела следующие характеристики:
OS: Windows;
ЦП: AMD Ryzen Threadripper 1950X 16-Core Processor 3,40 ГГц;
RAM: 128 ГБ;
Диски: SSD не менее 1ТБ на одного агента и не более двух агентов на один диск;
Для сервера Unity Cache V1 мы использовали:
VM: Linux CentOS 7;
RAM: 24 ГБ;
Диск: 150 ГБ.
Дополнительно был установлен Mac mini для сборок на iOS. Более подробно о старом пайплайне можно прочитать здесь.
Агенты у нас запущены как служба, работающая фоново независимо от пользователя, а не как отдельное приложение. Сделано это для возможности более гибкого менеджмента непосредственно самих агентов. Кроме того, таким образом можно выделить для каждого агента отдельного юзера со своими правами и реестром. Это оказалось удобно, но породило проблему, которую оказалось не так-то просто диагностировать.
Заключалась она в том, что во время сборки Unity неожиданно начинала крешиться без особых опознавательных логов. Но небольшой ресерч в Интернете помог определить, что проблема связана с desktop heap, или кучей рабочего стола на Windows.
В чем дело? У Windows есть неочевидная настройка: чем больше запущено процессов одновременно, тем быстрее происходит переполнение кучи. И поскольку у нас параллельно запускается несколько Unity для сборки билдов, это очень сильно влияло на скорость переполнения.
Экспериментально увеличив значение HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\SubSystems\Windows, креши удалось локализовать. Но с этим стоит быть аккуратнее: Microsoft не рекомендует выделять на кучу памяти больше, чем 20480 КБ:
Если вы выделяете слишком много памяти на кучи рабочего стола, может возникнуть отрицательная производительность. Поэтому мы не рекомендуем устанавливать значение более 20480 КБ.
Больше пресетов графики — дольше собирается билд. Что делать?
Как уже говорилось выше, по итогу время сборки билда выросло критически: с 40-100 минут до 3-4 часов с периодическими зависаниями импорта до 10 часов. Связано это с тем, что мы разделили текущий пресет качества графики и добавили новые. Если изначально у нас был только один пресет — Legacy, то теперь их стало четыре: Legacy, ULD (Ultra-Low Definition), LD (Low Definition) и HD (High Definition).
Разница между пресетами ULD, LD, HD:
В первую очередь мы решили начать искать проблему внутри кода и стали разбираться с нашими обработчиками событий преимпорта ассетов.
У нас используется самописный инструмент, который позволяет применять различные настройки графики, ориентированные на маску путей ассетов. Благодаря этому во время импорта можно применять настройки к текстурам как на единичные ассеты, так и сразу на целую папку, и на выбранные директории проекта.
Кроме того, изначально у нас были дополнительные самописные импортеры, которые работали с разными типами ассетов. Преимущественно они писались для работы с шейдерами и материалами.
В какой-то момент мы поняли, что время сборки сильно растет, и для определения причины начали смотреть логи редактора и сборки. В тех логах фигурировали следующие строки:
Hashing assets (38480 files)... 66.051 seconds
file read: 37.856 seconds (42992.805 MB)
wait for write: 22.787 seconds (I/O thread blocked by consumer, aka CPU bound)
wait for read: 9.976 seconds (CPUT thread waiting for I/O thread, aka disk bound)
hash: 53.298 seconds
Они не говорят ни о чем плохом — лишь показывают, что система начала хешировать ассеты для последующих действий над ними. В нашем случае проблема была в том, что такое сообщение появлялось в логе после импорта каждого пятого-десятого ассета. Полный рехеш одного ассета выполняется за минуту, так что время импорта взлетело до небес. Обычно такое происходит, когда что-то в коде вызывает AssetDatabase.Refresh() — именно из-за него перехватчики других событий зацикливаются, и Unity приходится пересчитывать весь ассет полностью.
Мы нашли все такие места — которых, конечно, оказалось немало, ведь эта функциональность нужна многим плагинам, — и обложили их трейсами. Затем мы нашли, где происходили рефреши — это оказалась подписка на AssetPostprocessor.OnPostprocessAllAssets. Так мы выяснили, что комбинация нашего скрипта и GPGSUpgrader (Google Play third-party файл) дает тот самый эффект увеличения времени сборки.
Мы уже обрадовались, но довольно быстро выяснили, что проблему до конца так и не решили: в некоторых случаях импорт проходил быстрее, в других — особо не менялся. Мы стали думать и изучать проблемы кэш-серверов, ведь по логу было видно, что зависшие билды часто очень долго работают с ассетами вместо того, чтобы просто быстро скачать их и положить к себе в библиотеку.
Следующим нашим шагом была оптимизация текущего кэш-сервера Unity.
Мало кэш-серверов = большие нагрузки
Когда мы создаем проект в Unity, нельзя просто добавить в него ассет — Unity переформатирует его в собственные форматы отдельно для iOS и Android. Для хранения таких файлов и существует кэш-сервер, откуда можно запросить уже собранные и переформатированные ассеты, и их не нужно заново пересчитывать. Если ассеты на сервере не хранятся — в таком случае происходит их пересчет, и он занимает какое-то время.
Для справки приведем немного цифр нашей конфигурации проекта:
Размер репозитория — более 150 ГБ;
Общее количество веток в репозитории — более 1000;
Количество файлов в проекте — более 170 000, из них большая часть — ассеты.
На момент старта работы над War Robots Remastered у нас было использовано три кэш-сервера на весь проект: один использовался разработчиками, остальные — для нашего окружения CI и Dev/Release.
Проблема заключалась в том, что проект имеет очень большое количество мелких файлов, так что сервер не выдерживал нагрузки и просто не успевал их вовремя раздавать. Редко возникали ошибки сети, гораздо чаще — ошибки типа «Disk I/O is overloaded» из-за огромного количества обращений к диску. Из-за этого — те самые зависания на 5-7 часов, вызванные тем, что при невозможности кэш-сервера отдать файлы клиентам Unity начинала полный импорт проекта.
В результате мы пришли к тому, чтобы увеличить число кэш-серверов для CI для перераспределения нагрузки на дисках. Таким образом, 22 агента мы развели на сеть серверов — по три на каждую платформу: Android-Dev, iOS-Dev, Win-Dev, Android-Release, iOS-Release, Win-Release, — что значительно улучшило ситуацию. Также мы провели 10-гигабитную сеть до агентов. В результате проблемы сети перестали появляться, а количество ошибок I/O значительно снизилось.
Но все же время импорта оставляло желать лучшего. В среднем сам импорт с кэш-сервера достигал полутора часов, и мы решили провести эксперимент с новой версией кэш-сервера — Unity Accelerator.
На момент начала наших экспериментов у нас использовалась версия Unity 2018.4, которая не позволяла использовать Asset Database V2 — а он был необходим для того, чтобы использовать Unity Accelerator, поскольку формат хранения ассетов в нем был несовместим с версией Unity Cache Server V1. Параллельно нам пришлось обновлять версию Unity до 2019.4.22f1 — тогда-то и получилось совершить переход на новый кэш-сервер, который пошел проекту только на пользу. Теперь импорт с платформы iOS с очисткой папки Library выглядел следующим образом:
Таким образом, Unity Accelerator оказался вполне рабочим вариантом: время импорта у него приемлемое, но прогрев не происходит за 1 прогон, и необходимо несколько итераций для наполнения кэшей.
Так, решив проблему с импортом, мы стали разбираться дальше.
А что с бандлами?
Следующим шагом оптимизации времени стала непосредственно сборка бандлов в проекте.
Мы используем разделение качеств для разных типов девайсов (подробнее о том, как это устроено у нас в проекте, можно почитать здесь). Отсюда следует, что практически весь контент у нас разбит на паки качеств, в которых уже лежат необходимые бандлы с ассетами.
Мы выявили сразу две проблемы:
Проблема переиспользования бандлов из прошлых сборок;
Проблема однопоточных сборок бандлов.
Первая проблема исходит из того, что мы собираем достаточно большое количество билдов в день: около 60, в дни релиза — еще больше. Как правило, контент в них не сильно меняется: в основном программисты проверяют свою работу либо тестируют новые фичи. Все билды в процессе сборки бандлов выкладывают во внешнее хранилище.
Таким образом, решением стала возможность перед сборкой указать билд в хранилище, из которого можно забрать уже готовые бандлы, а также в полуавтоматическом режиме забирать бандлы из последней успешно собранной конфигурации с такой же ветки. А организация локального кэширования бандлов сразу на нодах агентов позволила нам отказаться от сборки бандлов в конкретном билде, сразу забирая уже готовые.
Но, к сожалению, в данном подходе присутствует человеческий фактор, когда программист должен сам решить, менялся у него контент или нет. Это накладывает существенные ограничения в виде ошибок сборки, когда может отсутствовать или измениться контент.
Вторая проблема заключается в том, что для каждого пресета качества Unity собирала бандлы последовательно, друг за другом, так что на сборку всех четырех пресетов качеств уходило более часа.
Чтобы этого избежать и снизить время сборки, мы стали действовать по следующей схеме:
После импорта проекта мы генерим дополнительные Unity-проекты для каждого необходимого качества, где с помощью symlink линкуем папку Library для каждого сгенерированного проекта. Но будьте аккуратнее: внутри библиотеки есть динамические папки, которые создаются во время сборки. Например, может пересчитываться кэш шейдеров или папка с packages, что может привести к проблемам в параллельных сборках. Соответственно, линковать нужно только статический контент и саму базу.
В отдельных процессах запускаем одновременно несколько экземпляров Unity и в каждом собираем отдельное качество.
Результат от каждой сборки мы перемещаем в родительский проект.
Таким образом, мы смогли распараллелить время сборки бандлов в рамках одной ноды.
Подводя итоги: результаты нашей борьбы с гигантским временем сборки
Нам удалось вернуть показатели времени сборок на прежний уровень до выхода War Robots Remastered даже несмотря на то, что количество контента в проекте увеличилось в 2,5 раза. Для этого мы использовали Unity Accelerator. Также удалось убрать зависания импорта, минимизировав вызовы AssetDatabase.Refresh().
Нагляднее в цифрах:
На графике ниже можно подробнее посмотреть распределение времени сборки по датам:
Мы перестали пересобирать бандлы — вместо этого теперь забираем их сразу готовые из ранее собранных билдов.
Распараллелили сборку бандлов разных качеств, запустив одновременно несколько экземпляров Unity и залинковав ассеты.
Автор материала — Александр Панов