Привет! Я Сергей Боиштян, Software engineer в команде Speed. Мы делаем инструменты для тестирования, андроид-разработки, CI и CD. Чтобы больше узнать о том, чем мы занимаемся, посмотрите наш github или канал для обсуждения CI и сборки под андроид. В Авито постоянно что-нибудь обновляется, улучшается и дорабатывается, поэтому мы хотим, чтобы код быстро писался и проверялся, чтобы его быстро мёржили и проверяли после интеграции. В итоге мы получаем новую версию раз в неделю. В этой статье я расскажу, что мы делаем, чтобы весь CI работал четко. В Авито есть большой monorepo примерно на 1000 модулей. В нём легко переиспользовать код, делать глобальный рефакторинг, нет проблем с репозиторием. Для быстрой проверки всего этого мы используем 16 000 robolectric unit-тестов, около 3000 instrumentation-тестов и 600 E2E-тестов, а также кастомные detect для lint и различные правила. Все, кроме e2e, запускаем на pull request (PR). Ещё каждую ночь мы запускаем специальную сборку, в которой крутятся все тесты на develop-ветке. По итогам формируется отчет для каждого юнита, по которому можно понять, какие тесты хорошие, а какие плохие.  В какой-то момент мы поняли, что вся эта система стала слишком сложной для понимания. Оказалось, что можно несколько месяцев улучшать и ускорять сборку, а в итоге сделать её медленнее. В нашем CI/CD много тестов, кастомных элементов, деталей. Если в нашу speed-команду придет новый сотрудник, ему будет тяжело разобраться в работе системы. Мы начали искать ответы, и как часто бывает, всё изменила всего одна книга — «Site reliability engineering. Надёжность и безотказность как в Google».  Благодаря всем решениям, которые мы внедрили на основе этой книги, я могу быстро понять, что нужно улучшить. Например, в начале квартала я посмотрел наши дашборды и всего за час составил себе такой роадмап на 3 месяца: хочу добавить импакт-анализ, чтобы запускать только нужные UI- и Unit-тесты; разобраться, почему в конце сборок есть 6-минутная задержка. Скорее всего, дело в построении build-trace и отправке build-metric; убрать в Авито сборку лишних build variant: ruStore, Xiaomi, staging можем спокойно не собирать; увеличить количество девайсов для Avito UI-тестов, чтобы ускорить эту задачу; по Gradle Cache нужно разбираться, что чаще всего в miss и сколько оно занимает. Но прежде чем перейти к дашбордам, метрикам и мониторингу, давайте пройдёмся по базовым понятиям. Чтобы определить здоровье сборки, нужно понимать свой CI и что в нём происходит. Это значит уметь интерпретировать телеметрию: метрики, дашборды, логи и прочее. Вы должны пользоваться всеми этими данными, знать, куда смотреть, чтобы увидеть, всё ли в порядке. При этом вы точно не понимаете сборку, если не представляете, какие метрики что показывают. Возможно у вас слишком много дашбордов или в них просто никто не смотрит.  Если вы точно поняли сборку, то можете решить, какое её состояние считать здоровым, определить этот уровень и зафиксировать его через SLA. Для этого надо договориться о приемлемом для пользователей уровне гарантий работы. В нашем случае это андроид-разработчик. Мы спрашиваем его: если твой PR идёт 40 минут, ты будешь доволен? Если он отвечает, что будет, то мы фиксируем наш уровень обслуживания: 40 минут на PR.  На всё, что делает ваша команда и чем пользуются другие, нужно установить подобный уровень. Для андроид-разработчиков у нас есть ещё параметр «стабильность сборки». Он нужен, чтобы пользователи не перезапускали сборки из-за ошибок, не связанных с их изменениями. Например, flaky-тесты, проблемы с внешним окружением. SLA — это верхний уровень, соглашение в виде текста. Под ним есть еще SLO и SLI, но я буду все называть SLA для простоты. Здоровье сборки и SLA всегда субъективны и зависят от ваших ресурсов и пользователей. Если у вас один CI-сервер с 2 ГБ памяти, то ваш SLA — миллион миллиардов часов. А если, к примеру, как у нас — 20 нод по 300 ГБ памяти и по 60 ядер, то вы сможете выполнять сборки быстрее и чаще. Отталкивайтесь от реальных возможностей.  SLA помогают ставить приоритеты и фокусироваться на важном. Если они соблюдаются (сборка здорова), то не надо ничего трогать. А если сборка болеет и SLA нарушаются, то фиксим, ускоряем и в конце квартала получаем премию. Еще такие метрики помогают мне измерять работу speed-команды, делать её прозрачней для пользователей и лучше понимать, хорошо ли живется нашим разработчикам. Сборки очень хрупкие и постоянно болеют. Я сталкивался с четырьмя видами болезней. Обновление Gradle, Kotlin или AGP может повлиять на время сборки, её стабильность и gradle cache hit rate. Обновлянкой болеют практически все хотя бы один раз. Вот пример из issues: на Gradle 7.5 проект из 1000 пустых андроид-модулей тратит 10 ГБ памяти и синхронизируется за 3 минуты. После обновления на версию 7.6 время выросло до 13 минут, а после 15 минут сборка упала. При этом успела сожрать 16 ГБ памяти. Явный признак дефицита железа — это когда пишешь в чат: пацаны и пацанессы, у нас почему-то умирают поды из-за нехватки места на диске, которое мы даже не запрашивали.  Ещё часто не хватает памяти. Пример с нашего CI: robolectric-тесты падали с ошибкой, которая говорит о недостатке памяти.  У нас в Авито 16 000 юнит-тестов, поэтому при запуске на PR они иногда работают медленно или нестабильно. Например, однажды я заметил на наших метриках, что юнит-тесты в обычном простом модуле идут 35 минут. При этом, если запустить их локально, хватало 1,5 минуты. Также тесты могут зависать. Наконец, бывают тяжёлые robolectric-тесты. Если вы не знали, robolectric всегда течёт, и его демоны всегда потребляют больше памяти, чем вы ожидаете. Дело в том, что мы можем ограничить только хип, а у robolectric есть другие области памяти, к которым у нас нет доступа. В Авито 75 андроид-разработчиков, они создают огромное количество кода. Получается, что speed-команда ускоряет сборку, а они её замедляют просто в силу большого объема кода. Условно, у нас было 750 модулей, а через год их уже 1000. Об этой болезни я узнал почти случайно. Мы пробовали добавлять кучу разных билд-вариантов, но оказалось, что проблема в юнит-тестах, которые лежат в main source set. И все они запускаются уникально для каждого билд-варианта. Если вы уже заключили SLA, нужно каким-то образом понять, что вы их соблюдаете. Или почему не получается это делать. С этим вам помогут дашборды, причём чем они проще, тем легче понять, выполняете ли вы SLA. Отличный пример — ртутный градусник. У него есть одно понятное значение: если температура выше 37 градусов, значит, вы болеете. Можно получить быструю обратную связь, потому что достаточно знать одно критическое значение.  У нас в команде есть два элементарных дашборда для метрик времени и стабильности сборки.  Но у простых дашбордов есть минус: они не позволяют понять, что случилось. На градуснике больше 38 градусов — значит, я заболел. Но чем именно и что делать дальше? Когда речь идет о здоровье человека, мы обычно обращаемся к врачу, проходим обследования и сдаем анализы. Ну или просто выпиваем что-то от температуры, если точно знаем, что дело в обычном ОРВИ. Для сборок тоже нужны дополнительные анализы. Ещё раз упомяну книгу «Site reliability engineering. Надежность и безотказность как в Google»: в ней есть такая штука, как золотые сигналы.  Latency — время, за которое можно прогнать всю сборку на PR. Traffic — количество сборок за заданное время, — например, неделю. Мы отслеживаем, сколько их, в какой диапазон они укладываются. В рамках недели этот дашборд нужен нечасто. Зато по нему можно ретроспективно посмотреть данные — например, за полгода. Допустим, мы наняли 20 новых разработчиков и время билдов увеличилось. Я проверю дашборд, увижу, на сколько процентов изменилось время и смогу точно посчитать, сколько железа нужно докупить. Также я могу посмотреть дашборд по дням. Если увижу, что резко выросло количество упавших сборок, значит, что-то сломалось.  Errors — количество ошибок сборки. Дашборд с ошибками я уже показывал выше, здесь мы учитываем число flaky-сборок. Saturation — требуемый объем ресурсов для работы — например, железа, CPU, диска, памяти.  Эти четыре метрики — базовые, они помогут увидеть серьёзные болезни сборок. Вы увидите, в какой части возникла проблема: не хватает железа, упала стабильность или выросло время сборок.  Чтобы лучше разобраться в причинах, нужны другие метрики. Сначала их надо придумать. Для этого задайте себе такие вопросы: Что сильнее всего увеличивает время сборки? Из чего складывается время сборки? Что потребляет больше всего ресурсов? Например, у нас в сборке примерно 100 000 тасок: линты, Dex Merge, Kotlin Compile, Kapt. Если разбить всё время сборки на части, то появятся отдельные метрики для каждой части. Допустим, мы добавили Anvil, который не умеет выполнять инкрементальную компиляцию, и общее время сборки выросло. Без дашборда с детальными метриками мы не поняли бы причину роста. Какие ещё метрики мы отслеживаем:  Gradle Cache. У нас есть Hit Rate, где видно, что 80% из миллиона задач в день берётся из Cache, и только другие 20% исполняются.  Важно, чтобы не было ошибок Gradle Cache, потому что после нескольких ошибок походы в cache выключаются. Так Gradle «оптимизирует» работу с cache, чтобы не тратить время на бесполезные попытки скачать что-то из недоступного источника. Internal. Мы разделяем метрики по разным сценариям. У нас есть Avito Android Build — сборка на PR. Я могу взять любой другой сценарий, например релиз, вбить его название и смотреть метрики конкретной конфигурации. Configuration. Здесь мы можем смотреть, сколько времени занимает конфигурация у конкретного человека или по всем сценариям.  Build Time. У нас есть дублирующий дашборд, который показывает неполное время PR, а время по конкретному типу сборки. Критический путь. Это важная метрика, которую мы сделали сами. Я ни у кого больше не встречал похожего. У нас сборка работает на 15 ядрах и параллелит таски на 15 потоков. Самый долгий путь от начала до конца потока является критическим. Если ускорять задачи из критического пути, то импакта будет больше, чем если брать просто медленные задачи. Поэтому мы измеряем импакт задач, которые чаще всего попадают в критический путь. Медленные задачи. Помимо критического пути есть отдельные задачи и целые модули, которые просто идут медленно. Такие задачи мы тоже отслеживаем в дашборде.  Обычно мы смотрим только самые медленные модули, они сверхуТакже мы собираем все данные в один дашборд, по которому удобно смотреть работу в целом и отдельные выбросы. Благодаря этому графику дежурный может увидеть максимальный импакт за период, — например, за две недели. Также по дашборду легко понять, что стоит ускорять, а что нет.  Android emulators nodes. У нас много UI-тестов, поэтому есть несколько дашбордов для эмуляторов.  Demo-приложения. Все тесты, которые есть у нас в Авито, мы распараллелили и разнесли по разным модулям, поэтому их можно кэшировать отдельно друг от друга. Поэтому мы можем использовать demo — отдельные app, в которых подключены только те модули, которые разработчик правит сейчас. Остальные модули и тесты не выполняются. Такой подход ускоряет разработку и тестирование, а нам приходится отслеживать состояние demo-приложений. Дашборды помогают диагностировать болезни и понять, где копать. Но без конкретного инструмента, который может пролезть внутрь конкретной сборки, вылечить её сложно. В общем виде нужны инструменты для: сбора метрик, их анализа, реагирования на их деградацию, анализа самой сборки. Сбор метрик. Теоретически, можно использовать Gradle Enterprise, Talaiot, плагин Avito Build Metrics.  На практике Gradle Enterprise очень дорогой и в России его невозможно купить. В него нельзя контрибьютить, потому что это Black Box, а также нельзя подсчитать агрегаты на стороне клиента.  Когда мы начали делать свои инструменты, Talaiot был очень сырым. В нём не было поддержки Configuration Cache и каких-либо агрегатов на стороне клиента. На момент выхода статьи Talaiot уже выпустил версию с поддержкой Configuration Cache. А вот наш плагин Avito Build Metrics мы активно разрабатываем и используем. В нём пока тоже нет Configuration Cache, но в ближайшее время появится. Это Open Source, который можно подключить к себе. Но лучше сначала написать нам, потому что возможно, что там не хватает документации или вам нужны дополнительные фичи. Мы это с радостью доработаем, либо вы можете законтрибьютить сами.  Анализ метрик. Мы используем Grafana, а вам советую использовать то, что совместимо с вашим бэкендом для сбора.  Реакция на деградацию. У нас Moira от Контура. В ней нужно написать код для алертов, который потом автоматически уведомляет по разным каналам связи, если что-то случается. Также умеет делать скриншоты.  Анализ внутренностей сборки. У нас есть Gradle Scan, в целом он отлично справляется. Но с ним есть проблемы. Например, он хранится ограниченное время и без Gradle Enterprise будет ограниченный функционал. Это подходит не всем. Бывает, что наши сканы не отправляются, потому что слишком много весят. И его нельзя дорабатывать под себя и отслеживать критический путь.  Мы написали плагин Avito Build Trace. Он проще, чем Gradle Scan, создаёт trace-файл вашей сборки. По нему можно пройтись SQL-like запросами и найти узкие места.  Формат файла совместим с Chrome://tracing и Perfetto Dev от Google, который они используют везде для трейсинга Android, Linux, Chrome.  У Perfetto Dev есть SDK, чтобы создавать этот трейс-совместимый формат. Готовый файл можно загрузить на бесплатный сайт и посмотреть красивую выгрузку.  На сайте Perfetto Dev есть такая фишка: query language, похожий на SQL. Можно написать собственные селекты, чтобы проанализировать всё что происходит внутри сборки.  Наконец, для анализа сборки можно использовать логирование. Подробно про логи рассказывал Алексей Данилов, советую посмотреть его доклад «Логи не нужны». В логировании с помощью Gradle есть куча проблем: нельзя добавить новые источники, настроить разные уровни для разных частей. Лог выглядит как огромная простыня, в которой могут быть тысячи строк. Найти в нём что-то почти нереально.  Чтобы не мучиться со стандартным логом, мы написали свой Avito Gradle logger. Добавили в него источники: Kibana, Sentry, FileSystem. К тому же, логгер можно конфигурировать прямо внутри кода.  Когда у вас уже есть SLA, метрики и инструментарий, всё это нужно систематизировать и создать единую систему. Это самое сложное, потому что можно создать любой дашборд, но пользоваться им нужно системно и с пониманием. Без этого вы не получите большой пользы даже от очень крутых инструментов.  В нашей системе здравоохранения есть несколько важных вещей:   Зона ответственности. Мы отвечаем только за скорость и стабильность CI/CD и не связываемся с проблемами, которые к нам не относятся. Например, не пытаться оптимизировать чужой код, потому что этим занимается другая команда.  Скорая помощь. В команде должен быть дежурный, который чинит то, что сломалось. Это должна быть его рабочая задача — отслеживать алерты и оперативно исправлять сбои. У нас дежурный загружен на 50%, все остальное время он общается с пользователями, смотрит дашборды и готовит отчёт для ретроспективы, если был сбой. Разбор инцидентов. На ретроспективе мы обсуждаем, что нужно сделать, чтобы сбой больше не повторился. Думаем, каких метрик и алертов нам не хватило, где нужно доработать документацию.  В конце отмечу, как понять, что система работает. Вы знаете типичное состояние вашей системы, понимаете, когда и как она работает нетипично. Благодаря телеметрии вы можете ответить на вопрос «всё хорошо?» или «всё плохо?», и если плохо — то что именно и почему.  Предыдущая статья: Apache Spark и PySpark для аналитика. Учимся читать и понимать план запроса в SparkUI