Разработка
Как Spotify бесшовно перешел на сборки Bazel
Мы сосредоточили все свои силы на Bazel и полностью перевели приложение Spotify для iOS на сборку с помощью Bazel для наших 200 с лишним инженеров, не пропустив ни одного еженедельного релиза для миллионов наших пользователей iOS.
Мы в компании Spotify экспериментируем с системой сборки Bazel с 2017 года. За эти годы проект стал более зрелым, в него была добавлена поддержка большего количества языков и экосистем благодаря open source сообществу и мантейнерам из Google. В 2020 году стало ясно, что будущее нашей клиентской разработки требует единой системы сборки, которая бы хорошо масштабировалась с нашей многоязычной, многоплатформенной кодовой базой из миллионов строк.
Поэтому мы сосредоточили все свои силы на Bazel и полностью перевели приложение Spotify для iOS на сборку с помощью Bazel для наших 200 с лишним инженеров, не пропустив ни одного еженедельного релиза для миллионов наших пользователей iOS.
Что такое система сборки
Система сборки — это набор программных инструментов, используемых для облегчения процесса сборки. С момента выпуска нашего iOS-приложения в 2008 г. мы полагались на систему сборки Xcode для сборки и упаковки приложения как для разработки, так и для релиза. Это система сборки по умолчанию, поставляемая вместе с Xcode — интегрированной средой разработки (IDE) для платформ Apple, разработана компанией Apple, и используется миллионами разработчиков. По мере того как мобильные приложения становились все больше и больше (по количеству строк кода, числу участников и т.д.), компании начали писать более современные системы сборки, не привязанные к какому-либо существующему инструменту или IDE (например, Blaze от Google или Buck от Meta*). Обе эти системы были с открытым в какой-то момент за последние полдесятка лет. По мере того как наши кодовые базы росли более чем на 30% в год, мы наблюдали значительное снижение уровня счастья и производительности разработчиков.
Bazel обещал, что благодаря эффективному удаленному кэшу и сервисам удаленного выполнения сборки будут выполняться гораздо быстрее, и мы решили внедрить его в производство в начале 2020 года. Наши цели при внедрении Bazel были очевидны: сократить время непрерывной интеграции (CI) и локальной сборки и повысить производительность разработчиков за счет сокращения цикла обратной связи.
Миграция
Чтобы сделать миграцию максимально безопасной, не останавливая разработку для более чем 120 команд, участвующих в создании еженедельных релизов, нам нужно было найти способ разрабатывать с несколькими системами сборки бок о бок до тех пор, пока Bazel не будет полностью проверен. К счастью, прошлые инвестиции в наш инструментарий окупились: ранее мы перенесли декларацию проекта из проверяемого файла pxbproj в пользовательский Ruby DSL и формат YAML. Это позволило разработчикам декларативно добавлять в проект новые модули. В качестве примера можно привести следующий фрагмент, в котором объявляется новая цель FooKit с некоторыми исходными файлами Swift, каталогом ресурсов и одной зависимостью.
FooKit: type: spotify_static_library sources: - Sources/**/*.swift resource_dirs: - Resources deps: - BarKit
Благодаря этому уровню абстракции мы в течение нескольких дней написали скрипты, которые сгенерировали более 2 000 файлов BUILD.bazel и позволили нашему проекту успешно работать с Bazel. Только 30-50 файлов BUILD.bazel нужно было написать вручную, что означало, что большинство инженеров поначалу даже не подозревали о том, что мы работаем с двумя системами сборки бок о бок. В результате работы описанного выше модуля получался следующий gitignored файл BUILD.bazel:
music_swift_library( name = "FooKit", srcs = glob(["Sources/**/*.swift"]), data = music_glob(["Resources/**"]), deps = ["//Systems/BarKit"], )
Последствия
В мае 2022 года разработчикам приходилось около 80 минут ждать обратной связи по своим изменениям перед слиянием.
В последующие месяцы мы по очереди переносили наши CI-конфигурации в Bazel, начиная с конфигураций, которые занимали больше всего времени. В частности, одна из них, включающая более 800 тестовых целей, содержащих около 3 млн. строк кода, занимала более 45 минут при сборке с помощью Xcode. После перехода на Bazel это время сократилось до менее чем 10 минут. Улучшение было достигнуто в основном за счет использования эффективного удаленного кэша Bazel и распараллеливания тяжелых задач сборки с помощью удаленного выполнения сборки (remote build execution, RBE). Мы используем BuildBuddy для поддержки наших сборок и сбора ценной информации о том, как наши пользователи используют Bazel. Расширенная телеметрия помогла нам легко обнаружить проблемы с производительностью и устранить узкие места для повышения коэффициента попадания в кэш.
По мере продвижения миграции, связанные с ней усилия по повышению изоляции модулей и улучшению архитектуры приложений позволили нам создавать и проводить меньше тестов. Это было связано с тем, что теперь мы могли детерминированно выбирать, какую часть графа зависимостей затрагивает каждое изменение, используя Bazel-запросы (и, в частности, bazel-diff). Таким образом, всего за несколько месяцев цикл обратной связи CI улучшился в четыре раза, сократившись с 80 до 20 минут.
Стоимость миграции
На первых этапах миграции мы отдавали приоритет переносу в Bazel pre-merge сборок, в которых инженеры ждали дольше всего, с целью повышения качества работы 200 с лишним инженеров, ежемесячно вносящих вклад в iOS-репозиторий.
Поддержание нескольких систем сборки во время миграции может быть дорогостоящим. В нашем случае мы наблюдали различия между двумя параллельно работающими системами сборки, что иногда приводило к интересным сбоям компиляции в одной из них. Например, разработчики сообщали о том, что сборка локально проходит, а в CI — нет. Несмотря на то, что мы значительно сократили время, которое разработчики тратили на ожидание тестирования своих изменений в CI, наши платформенные инженеры теперь тратили больше времени на помощь функциональным инженерам в отладке проблем с системой сборки. Мы инвестировали в несколько приближений, чтобы максимально унифицировать эти две системы и сократить время, которое теряли разработчики функциональных модулей из-за необходимости отлаживать проблемы, не связанные с их работой. Мы поняли, что единственный способ полностью избавиться от этих затрат на обслуживание — как можно скорее перейти на единую систему сборки.
Опыт работы с IDE
iOS-инженеры используют Xcode для создания, отладки и тестирования приложений. Нам необходимо было найти решение для внедрения Bazel в IDE Xcode и поддержки всех существующих рабочих процессов. Мы решили тесно сотрудничать с сообществом разработчиков открытого кода, чтобы создать новую интеграцию с IDE для пользователей Xcode. После нескольких месяцев тестирования и участия в проекте других компаний, идущих по схожему с нами пути, мы приняли rules_xcodeproj. В ноябре 2022 года мы начали A/B-тестирование нашей новой интеграции с Xcode. Мы внимательно следили за временем локальной сборки и всеми проблемами, о которых сообщали.
На рисунке 2 показано постепенное развертывание системы. Мы начали с того, что сделали Bazel для локальной разработки опцией. Это позволило нам собрать отзывы инженеров, готовых протестировать экспериментальную интеграцию с IDE. По мере исправления ошибок и улучшения качества работы в течение последующих недель мы увеличивали масштабы распространения Bazel. К марту 2023 года почти все iOS-инженеры использовали нашу новую интеграцию Bazel без особых проблем, а 75-й процентиль времени сборки составлял около 30 секунд.
Как и ожидалось, после того как инженеры начали использовать Bazel для локальной сборки и отладки в одной и той же системе сборки, мы стали получать меньше жалоб на различия между локальными и CI-сборками. В мае 2023 года мы решили отказаться от старой системы сборки для локальной разработки. Настало время заняться последней и наиболее важной частью нашей инфраструктуры, все еще зависящей от старой системы сборки: перевести наши релизные сборки и миллионы пользователей на версию приложения, созданную с помощью Bazel.
Релизы с Bazel
Выпуск мобильного приложения не обходится без сложностей. Мы опираемся на множество собранных данных и тестов, чтобы обеспечить выпуск наиболее качественной версии в каждый момент времени. Существует множество интеграций с системой сборки, таких как взаимодействие с нашей платформой для проведения экспериментов, регистрация метаданных для новых сборок, хранение артефактов сборки для анализа отчетов о сбоях. Нам нужно было адаптировать все существующие инструменты для работы с Bazel, где это было необходимо. За 15 с лишним лет существования нашего мобильного приложения мы накопили бесчисленное количество пользовательских интеграций и оптимизаций процесса выпуска. Ответственность и знания обо всех этих шагах лежали на командах, разбросанных по всей компании, поэтому нам требовалось централизованное место для управления рисками. Мы решили написать документ о том, как мы будем подходить к этому внедрению.
План, риски, стратегию отката и предварительные сроки мы оформили в единый документ и предоставили его всем заинтересованным сторонам. Мы отслеживали все известные различия и разбивали на несколько релизов некоторые рискованные изменения, от которых мы зависели.
Мы создавали полностью оптимизированные сборки релизов как в Bazel, так и в Xcode с одинаковой почасовой периодичностью. Это было важно для того, чтобы быть готовыми предоставить сборки с горячими исправлениями из нашей предыдущей системы сборки в случае, если что-то пойдет не так.
Мы написали инструменты для постоянной проверки отсутствия неизвестных различий между двумя сборками. Наши инструменты проверяли все файлы, входящие в пакет, и, в зависимости от типа файла, выполняли более глубокие проверки, например, следующие:
- Мы сравнивали изображения и другие ресурсы, чтобы убедиться в отсутствии или неправильном включении ресурсов.
- Мы сравнивали конфигурационные файлы, такие как Info.plist и JSON, на их равенство.
- Мы сравнивали бинарные файлы, обращая внимание на включенные в них символы. Это позволяло нам быть уверенными в том, что все нужные классы скомпилированы правильно.
Развертывание
Мы разработали стратегию развертывания, которая позволяла тестировать релиз Bazel на устройствах сотрудников в течение примерно двух недель, прежде чем приступить к распространению на внешних пользователей. Это дало нам возможность протестировать весь релизный инструментарий без ущерба для еженедельного графика выпуска. После того как релиз был распространен среди сотрудников, мы приступили к тестированию альфа- и бета-версии на внешних устройствах. Поскольку мы не могли проводить A/B-тестирование нескольких сборок, мы выпустили только одну сборку для наших пользователей. Мы решили еще несколько раз выпустить сборки с использованием обеих систем, пока не убедились, что внедрение прошло успешно. К счастью, показатели аварийности и производительности оказались на высоте, и мы не получили никаких сообщений о существенных различиях или ошибках, возникших в результате смены системы сборки.
Заключение
Для осуществления этого перехода потребовалось внести большой вклад как в систему сборки Bazel с открытым исходным кодом, так и в ее правила и пользовательские интеграции. Мы с гордостью сообщаем, что наше приложение для iOS — это наш первый крупный клиент, полностью построенный на Bazel.
Мы хотели бы поблагодарить всех, кто принимал участие в этой работе на протяжении последних нескольких лет. Особые слова благодарности мы адресуем команде Bazel и всем другим участникам проекта, которые каждый день посвящают свое время этому замечательному проекту и сообществу.