Привет! Я Сергей Боиштян, Software engineer в команде Speed. Мы делаем инструменты для тестирования, андроид-разработки, CI и CD. Чтобы больше узнать о том, чем мы занимаемся, посмотрите наш github или канал для обсуждения CI и сборки под андроид.

В Авито постоянно что-нибудь обновляется, улучшается и дорабатывается, поэтому мы хотим, чтобы код быстро писался и проверялся, чтобы его быстро мёржили и проверяли после интеграции. В итоге мы получаем новую версию раз в неделю. В этой статье я расскажу, что мы делаем, чтобы весь 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-команды, делать её прозрачней для пользователей и лучше понимать, хорошо ли живется нашим разработчикам.

Болезни сборок: почему ломаются ваши SLA

Сборки очень хрупкие и постоянно болеют. Я сталкивался с четырьмя видами болезней.

Обновлянка

Обновление Gradle, Kotlin или AGP может повлиять на время сборки, её стабильность и gradle cache hit rate.

Пример из практики Авито: обновили Gradle до версии 7.6 и получили flaky-сборки

Обновлянкой болеют практически все хотя бы один раз. Вот пример из issues: на Gradle 7.5 проект из 1000 пустых андроид-модулей тратит 10 ГБ памяти и синхронизируется за 3 минуты.

Автор поста показал графики сборки: она выглядит здоровой

После обновления на версию 7.6 время выросло до 13 минут, а после 15 минут сборка упала. При этом успела сожрать 16 ГБ памяти.

На графиках явные признаки обновлянки: выросло потребление памяти, а сборка так и не выполнена

Явный признак дефицита железа — это когда пишешь в чат: пацаны и пацанессы, у нас почему-то умирают поды из-за нехватки места на диске, которое мы даже не запрашивали. 

В этой ситуации мы с админами Kubernetes долго разбирались, где какого диска не хватает, если мы его не запрашиваем

Ещё часто не хватает памяти. Пример с нашего CI: robolectric-тесты падали с ошибкой, которая говорит о недостатке памяти. 

Тестовый диабет

У нас в Авито 16 000 юнит-тестов, поэтому при запуске на PR они иногда работают медленно или нестабильно. Например, однажды я заметил на наших метриках, что юнит-тесты в обычном простом модуле идут 35 минут. При этом, если запустить их локально, хватало 1,5 минуты.

Что-то тут не так. Хорошо, что есть дашборд с метриками

Также тесты могут зависать.

В одном из обсуждений PR разработчик сообщил, что у него не работает CI. Оказалось, что тесты просто зависли

Наконец, бывают тяжёлые robolectric-тесты. Если вы не знали, robolectric всегда течёт, и его демоны всегда потребляют больше памяти, чем вы ожидаете.

Например, мы говорим: robolectric, вот тебе 0,5 ГБ памяти. А он: хорошо, возьму 2,5 ГБ

Дело в том, что мы можем ограничить только хип, а у robolectric есть другие области памяти, к которым у нас нет доступа.

Я экспериментировал с maxParallel и forkEvery и оптимизировал тесты. Выиграл суммарно 700 секунд

Кодовая диарея

В Авито 75 андроид-разработчиков, они создают огромное количество кода. Получается, что speed-команда ускоряет сборку, а они её замедляют просто в силу большого объема кода. Условно, у нас было 750 модулей, а через год их уже 1000.

Об этой болезни я узнал почти случайно. Мы пробовали добавлять кучу разных билд-вариантов, но оказалось, что проблема в юнит-тестах, которые лежат в main source set. И все они запускаются уникально для каждого билд-варианта.

Если вы добавили семь новых вариантов, то у вас станет в восемь раз больше задач в конце сборки

Градусники: дашборды SLA

Если вы уже заключили SLA, нужно каким-то образом понять, что вы их соблюдаете. Или почему не получается это делать. С этим вам помогут дашборды, причём чем они проще, тем легче понять, выполняете ли вы SLA.

Отличный пример — ртутный градусник. У него есть одно понятное значение: если температура выше 37 градусов, значит, вы болеете. Можно получить быструю обратную связь, потому что достаточно знать одно критическое значение. 

Такой метрикой может пользоваться кто угодно — от дошкольника до старика

У нас в команде есть два элементарных дашборда для метрик времени и стабильности сборки. 

90% наших сборок проходят за 1 час 7 минут. Это значит, что время может быть 20 минут, а может все 67
У нас было 267 стабильных и 10 нестабильных сборок. Число flaky-сборок — меньше 4%

Но у простых дашбордов есть минус: они не позволяют понять, что случилось. На градуснике больше 38 градусов — значит, я заболел. Но чем именно и что делать дальше?

Когда речь идет о здоровье человека, мы обычно обращаемся к врачу, проходим обследования и сдаем анализы. Ну или просто выпиваем что-то от температуры, если точно знаем, что дело в обычном ОРВИ.

Для сборок тоже нужны дополнительные анализы. Ещё раз упомяну книгу «Site reliability engineering. Надежность и безотказность как в Google»: в ней есть такая штука, как золотые сигналы. 

Это четыре метрики, за которыми обязательно нужно следить. У метрик есть общие определения, но я буду пояснять их в контексте Android CI в Авито

Latency — время, за которое можно прогнать всю сборку на PR.

90% успешных сборок укладываются в 1 час 10 минут. Среди упавших 90% укладывается в 47 минут. По всем сборкам среднее Latency — 1 час 5 минут

Traffic — количество сборок за заданное время, — например, неделю. Мы отслеживаем, сколько их, в какой диапазон они укладываются.

Все сборки за неделю. Зелёные точки — упавшие, жёлтые — успешные

В рамках недели этот дашборд нужен нечасто. Зато по нему можно ретроспективно посмотреть данные — например, за полгода. Допустим, мы наняли 20 новых разработчиков и время билдов увеличилось. Я проверю дашборд, увижу, на сколько процентов изменилось время и смогу точно посчитать, сколько железа нужно докупить.

Также я могу посмотреть дашборд по дням. Если увижу, что резко выросло количество упавших сборок, значит, что-то сломалось. 

Зелёный столбик — очередь сборок за день, жёлтые — успешные, синие — упавшие

Errors — количество ошибок сборки. Дашборд с ошибками я уже показывал выше, здесь мы учитываем число flaky-сборок.

Saturation — требуемый объем ресурсов для работы — например, железа, CPU, диска, памяти. 

Видно, что CPU мы используем очень мало, а вот память расходуем сильнее: после билда на диске остается около 250 ГБ

Эти четыре метрики — базовые, они помогут увидеть серьёзные болезни сборок. Вы увидите, в какой части возникла проблема: не хватает железа, упала стабильность или выросло время сборок. 

Какие ещё метрики можно отслеживать

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

  • Что сильнее всего увеличивает время сборки?

  • Из чего складывается время сборки?

  • Что потребляет больше всего ресурсов?

Например, у нас в сборке примерно 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 потоков. Самый долгий путь от начала до конца потока является критическим.

Задачи, которые попали в критический путь, сильнее всего влияют на время сборки

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

Из финального модуля Авито больше всего импакта у UI-тестов, потому что они идут в конце
Ещё мы измеряем критический путь по типам задач. Например, было бы неплохо ускорить Dex Merge

Медленные задачи. Помимо критического пути есть отдельные задачи и целые модули, которые просто идут медленно. Такие задачи мы тоже отслеживаем в дашборде. 

На третьем месте из самых медленных — линт
Обычно мы смотрим только самые медленные модули, они сверху

Обычно мы смотрим только самые медленные модули, они сверхуТакже мы собираем все данные в один дашборд, по которому удобно смотреть работу в целом и отдельные выбросы.

Какой-то юнит-тест шел целый час. Интересно, почему?

Благодаря этому графику дежурный может увидеть максимальный импакт за период, — например, за две недели. Также по дашборду легко понять, что стоит ускорять, а что нет. 

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, который можно подключить к себе. Но лучше сначала написать нам, потому что возможно, что там не хватает документации или вам нужны дополнительные фичи. Мы это с радостью доработаем, либо вы можете законтрибьютить сами. 

Для сбора метрик через наш плагин вам нужен graphite-like бэкенд

Анализ метрик. Мы используем Grafana, а вам советую использовать то, что совместимо с вашим бэкендом для сбора. 

Реакция на деградацию. У нас Moira от Контура. В ней нужно написать код для алертов, который потом автоматически уведомляет по разным каналам связи, если что-то случается. Также умеет делать скриншоты. 

Анализ внутренностей сборки. У нас есть Gradle Scan, в целом он отлично справляется. Но с ним есть проблемы. Например, он хранится ограниченное время и без Gradle Enterprise будет ограниченный функционал. Это подходит не всем. Бывает, что наши сканы не отправляются, потому что слишком много весят. И его нельзя дорабатывать под себя и отслеживать критический путь. 

Мы написали плагин Avito Build Trace. Он проще, чем Gradle Scan, создаёт trace-файл вашей сборки. По нему можно пройтись SQL-like запросами и найти узкие места.

Avito Build Trace генерирует trace file с метаданными и строит график на его основе
Trace file хранится в CI сборки неограниченное время. Его можно прикреплять к задаче на оптимизацию, и это очень удобно

Формат файла совместим с Chrome://tracing и Perfetto Dev от Google, который они используют везде для трейсинга Android, Linux, Chrome. 

У Perfetto Dev есть SDK, чтобы создавать этот трейс-совместимый формат. Готовый файл можно загрузить на бесплатный сайт и посмотреть красивую выгрузку. 

Можно выбрать конкретную таску, посмотреть её статус, тип и многие другие детали

На сайте Perfetto Dev есть такая фишка: query language, похожий на SQL. Можно написать собственные селекты, чтобы проанализировать всё что происходит внутри сборки. 

Я вывел имя, отсортировал сборки по длительности и вывел время задачи в секундах

Наконец, для анализа сборки можно использовать логирование. Подробно про логи рассказывал Алексей Данилов, советую посмотреть его доклад «Логи не нужны».

В логировании с помощью Gradle есть куча проблем: нельзя добавить новые источники, настроить разные уровни для разных частей. Лог выглядит как огромная простыня, в которой могут быть тысячи строк. Найти в нём что-то почти нереально. 

Чтобы не мучиться со стандартным логом, мы написали свой Avito Gradle logger. Добавили в него источники: Kibana, Sentry, FileSystem. К тому же, логгер можно конфигурировать прямо внутри кода. 

Система здравоохранения для сборки

Когда у вас уже есть SLA, метрики и инструментарий, всё это нужно систематизировать и создать единую систему. Это самое сложное, потому что можно создать любой дашборд, но пользоваться им нужно системно и с пониманием. Без этого вы не получите большой пользы даже от очень крутых инструментов. 

В нашей системе здравоохранения есть несколько важных вещей: 

  1.  Зона ответственности. Мы отвечаем только за скорость и стабильность CI/CD и не связываемся с проблемами, которые к нам не относятся. Например, не пытаться оптимизировать чужой код, потому что этим занимается другая команда. 

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

У нас дежурный загружен на 50%, все остальное время он общается с пользователями, смотрит дашборды и готовит отчёт для ретроспективы, если был сбой.

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

В конце отмечу, как понять, что система работает. Вы знаете типичное состояние вашей системы, понимаете, когда и как она работает нетипично. Благодаря телеметрии вы можете ответить на вопрос «всё хорошо?» или «всё плохо?», и если плохо — то что именно и почему.

Предыдущая статья: Apache Spark и PySpark для аналитика. Учимся читать и понимать план запроса в SparkUI