Как мы ускорили конвейер CI более чем в 4 раза


Проблема

В CompanyCam мы полагаемся на автоматизированное тестирование, чтобы убедиться, что наш код работает так, как ожидается. Наш бэкенд написан на Ruby on Rails, и мы используем автоматизированное тестирование (через rspec), чтобы убедиться, что новый код не нарушает существующую функциональность. У нас много тысяч таких автоматизированных тестов — более 3500 на момент написания статьи — и мы постоянно пишем новые тесты для новых функций, чтобы быть уверенными, что они не сломаются в будущем.

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

Для новых функций и работ по обслуживанию мы используем типичный рабочий процесс pull-request, выполняя тесты на каждом этапе. Обычный день разработчика может выглядеть следующим образом:

  1. Написать код.
  2. Протестировать код на месте.
  3. Выложить новый код в наш центральный репозиторий (Github).
  4. Запустить тесты, чтобы убедиться, что наш новый код работает правильно.
  5. Если необходимы изменения, перейдите к шагу 1, в противном случае продолжайте.
  6. Внесите изменения, которые за это время написали другие разработчики.
  7. Снова проведите тесты, чтобы убедиться, что все по-прежнему работает.
  8. Если необходимы изменения, перейдите к шагу 1, в противном случае продолжайте.
  9. Объедините код в основную ветку.

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

Именно здесь у нас возникла проблема.

Запуск всего набора тестов от начала до конца занимал 13 и более минут. В течение этого времени разработчику было нечем заняться — если тесты проваливались, приходилось исправлять или переделывать работу. Если тесты проходили, они могли продолжать работу, но было небезопасно продолжать работу, не убедившись, что все работает правильно. Когда оставалось 13 минут, легко было увлечься другим разговором, сварить еще одну чашку кофе, поиграть в Wordle или отвлечься на что-нибудь другое. Внезапно эти 13 минут превратились в 20 минут, или полчаса. Умножьте эту задержку на все тесты, которые нужно было запускать каждый день (не менее десятков), и вы увидите, что тесты сильно замедляли работу нашей команды.

Постановка цели

Мы знали, что хотим, чтобы тесты выполнялись быстрее, но насколько быстрее? Мы поставили цель сделать так, чтобы тесты выполнялись менее 5 минут. Мы интуитивно чувствовали, что это возможно, и 5 минут казались достаточно коротким промежутком времени, чтобы не превратиться в сплошное отвлечение. У меня была внутренняя цель — сократить время тестирования до 3 минут, но в то время у меня не было веских оснований полагать, что мы сможем этого добиться.

Шаг 1: Параллельные тесты

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

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

Как только все тесты были запущены параллельно, мы отправили ветку в CI-провайдер, и наши тесты стали проходить примерно за 8 минут. Это примерно 40% экономии времени! Хорошее начало, но мы знали, что можем добиться большего.

Шаг 2: Оценка нашего CI-провайдера

Процесс автоматического запуска тестов с каждым новым фрагментом кода называется непрерывной интеграцией (CI). Мы используем облачный сервис CI для выполнения этих тестов. 

Изучая отчеты о времени выполнения тестов, мы обнаружили, что наши тесты выполняются довольно быстро — около 3 минут, но этап настройки среды занимает целых 5 минут, прежде чем тесты успевают запуститься!

Мы потратили некоторое время на чтение документации от нашего поставщика CI и сделали столько оптимизаций, сколько смогли. Тем не менее, мы так и не смогли сократить время установки. Казалось, что в процессе установки мы максимально использовали мощности их серверов.

Мы решили, что пришло время посмотреть на других CI-провайдеров, чтобы найти тот, который лучше соответствует нашим потребностям.

Шаг 3: Тестирование других провайдеров CI

Мы создали пробные учетные записи у нескольких провайдеров и потратили время на то, чтобы заставить нашу тестовую среду работать на каждом из них. Во время этого процесса мы поддерживали интеграцию с существующими CI-провайдерами; было крайне важно не нарушать обычный рабочий процесс наших инженеров, пока мы пытались ускорить работу.

Сразу же мы обнаружили, что другие CI-провайдеры имеют более мощные платформы, и нам удалось сократить время выполнения тестов до 6 минут — что было очень близко к нашей цели в 5 минут! 

Один провайдер выделялся среди других: CircleCI. После тестирования мы решили направить наши усилия по оптимизации на CircleCI и посмотреть, какие результаты мы сможем получить. 

Это решение нас не разочаровало.

Шаг 4: Оптимизация CircleCI

CircleCI предложил больше возможностей для выбора размера машины и параллелизма, чем другие протестированные нами провайдеры, и, используя несколько «больших» экземпляров, мы смогли увидеть, что наши тесты выполняются молниеносно — всего за две минуты! Однако время настройки — время, необходимое для начала тестов, в течение которого инициализируется тестовая среда — все еще находилось в диапазоне 90-120 секунд. После некоторого исследования мы убедились, что можем сделать этот этап намного быстрее.

Кэширование активов

Для того, чтобы ускорить время установки, первое, что нам нужно было сделать, это кэшировать наши гемы; установка гемов занимала 30-45 секунд, а наши гемы редко менялись. Используя функцию кэширования активов CircleCI, мы можем хранить копию установленных гемов, которую мы восстанавливаем при каждом запуске вместо повторной установки. Используя эту кэшированную копию, мы смогли сократить шаг установки активов до двухсекундной проверки, экономя полминуты на каждый запуск. (Когда драгоценные камни меняются, первый запуск занимает немного больше времени. Последующие запуски используют новые кэшированные драгоценные камни и снова выполняются быстро).

Большой выигрыш: Использование функции параллелизма CircleCI

В CircleCI есть очень хорошая функция для параллельного запуска тестов. Функция параллелизма похожа на гем parallel_test, но на стероидах — она разделяет отдельные файлы тестов для вас, на столько виртуальных тестовых серверов, сколько вы хотите (до 64). 

Допустим, у вас есть 16 тестов, которые необходимо запустить. Если запустить тесты со значением параллелизма, установленным на 2, будет создано два тестовых сервера. Первый будет выполнять 1-8, второй — 9-16. Если вы установите параллелизм на 4, то у вас будет четыре сервера, выполняющих 1-4, 5-8, 9-12 и 13-16 соответственно. 

Но на этом волшебство не заканчивается! После каждого запуска CircleCI сохраняет количество времени, затраченного на выполнение каждого теста, и при следующем запуске тестов делит их на предполагаемую продолжительность!

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

Тест 1: 30 секунд
Тест 2: 27 секунд
Тест 3: 17 секунд
Тест 4: 9 секунд

Если вы разделите тесты по порядку (1&2, 3&4), то общее время выполнения будет равно времени самого длинного сервера. Так, в нашем примере это будет 30+27, итого 57 секунд. Другой сервер закончит работу за 26 секунд. CircleCI заметит, сколько времени занимает каждый тест, а затем в следующем запуске разделит их так, чтобы общее время для каждой группы было максимально близким. Так что следующий запуск будет 1&4 и 2&3. Поскольку самый длинный запуск в этом случае составляет всего 44 секунды, вы сократили общее время выполнения почти на 25% (13 секунд)!

Поскольку у нас много тысяч тестов, мы можем разделить их очень равномерно.

Используя параллелизм CircleCI, мы смогли запустить наши тесты на 32 экземплярах одновременно, сократив время выполнения тестов менее чем до 30 секунд! Тем не менее, из-за настройки общее время выполнения было в районе 2:30.

Кроме того: В качестве еще одного бонуса, запуск тестов таким способом означал, что нам не нужно было использовать гем parallel_tests. У нас не было никаких проблем с гемом parallel_test (его все еще имеет смысл использовать в локальной разработке, например, когда мы не используем магию CircleCI), но отсутствие необходимости использовать его в CI сделало наш стек тестов проще и легче в обслуживании.

Последний шаг: Хостинг и кэширование образов Docker

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

Первым шагом было перемещение наших образов Docker в Amazon Elastic Container Registry (ECR). Инфраструктура ECR находится близко (в сетевом смысле) к CircleCI, и загрузка была очень быстрой для нас. 

Вторым шагом было убедиться, что мы используем образы CircleCI Convenience Images везде, где это возможно. Эти контейнеры Docker в значительной степени кэшируются; в большинстве случаев они мгновенно доступны для вас.

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

В целом, эти оптимизации позволили снизить общее время выполнения до двух минут (в среднем 2:15)! Наша первоначальная цель была менее 5 минут — мы уложились в половину этого времени!

Баланс цена/производительность

Хотя мы смогли выполнить наши тесты молниеносно быстро с параллелизмом 32, мы платили за 32-кратную стоимость установки при работе в одной коробке. Со временем это может стать дорогостоящим.

Мы провели серию тестов, изменяя параметр параллелизма в конфигурации CircleCI и проверяя итоговое общее время выполнения. 

В конце концов, мы остановились на параллелизме 12. Это примерно 1/3 от общей стоимости 32, и наш набор тестов завершается за 2:45-3:00, что, по нашему мнению, является приемлемым диапазоном времени для наших сотрудников.

Что дальше

При тестировании работа никогда не бывает по-настоящему закончена. У нас все еще есть несколько неработающих тестов (тесты, которые иногда проходят, а иногда нет), которые мы пытаемся очистить. Мы также находимся в середине процесса выявления медленно выполняющихся отдельных тестов и их оптимизации, чтобы немного ускорить наш набор. 

И, конечно же, по мере того, как мы продолжаем писать тесты, нам необходимо поддерживать наш набор CI, чтобы он не замедлялся со временем. Мы регулярно проверяем среднее время выполнения тестов и по мере необходимости корректируем коэффициент параллелизма (и оптимизируем отдельные медленные тесты), чтобы все шло быстро. 

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

Оцените статью
Procodings.ru
Добавить комментарий