При работе над более крупным проектом вы можете столкнуться с проблемой постоянно растущего набора тестов, которые со временем начинают работать медленнее на вашем сервере непрерывной интеграции (CI). У меня возникла эта проблема во время работы над проектом в Ruby on Rails, где тесты RSpec на CircleCI занимали около 15 минут.
Поскольку это меня беспокоило, я решил что-то с этим сделать, что привело к созданию библиотеки гемов Knapsack Ruby с открытым исходным кодом (название происходит от задачи о рюкзаке), которая занимается распределением тестов между параллельными серверами CI. В этой статье вы узнаете о двух подходах к разделению тестов на параллельных серверах непрерывной интеграции - статическом и динамическом.
Если в вашем проекте есть набор тестов, выполнение которого на сервере CI занимает около десятка минут, а может быть, даже несколько часов, вы знаете, насколько это неудобно для программистов. Когда вы работаете над какой-то новой функцией и отправляете новый коммит git в репозиторий, вам придется долго ждать, пока ваш CI-сервер не выполнит CI-сборку.
Ожидание на несколько минут или час задерживает обратную связь, которую вы можете получить от CI-сервера о тестах, которые, возможно, не были завершены (красные тесты). Ведь все мы хотим как можно скорее получить информацию о том, является ли наша CI-сборка зеленой или красной, чтобы работа программистов не блокировалась.
Проблема с запуском параллельных тестов на CI-сервере
Чтобы ускорить выполнение сборки CI, вы можете использовать параллелизм на сервере CI, то есть запуск нескольких параллельных машин CI (контейнеров CI, например, в Docker), где каждый параллельный сервер будет выполнять часть набора тестов. Однако существует проблема с тем, какие тесты следует запускать на каких серверах (узлах CI), чтобы их распределение было достаточно равномерным и вам не приходилось ждать узла CI, который является узким местом.
Ниже вы можете увидеть пример неоптимального распределения тестов на 4 серверах CI, где второй сервер, отмеченный красным, является узким местом, поэтому время ожидания завершения всей сборки CI составляет до 20 минут.
Оптимальное распределение тестов на параллельных CI-серверах
В идеальном сценарии тесты должны быть распределены таким образом, чтобы все параллельные серверы CI завершали операции в одно и то же время. В следующей части я покажу, как этого можно достичь.
Ниже вы можете увидеть пример оптимального распределения тестов, где каждая параллельная машина CI выполняет тесты в течение 10 минут, благодаря чему вся сборка CI длится всего 10 минут, а не 20, как в предыдущем примере.
Статическое разделение тестов детерминированным способом
Один из способов определить, как разделить тесты между параллельными машинами на сервере CI, чтобы каждый сервер выполнял тесты в одно и то же время, - это использовать измеренное время выполнения файлов в наборе тестов. Это был первый подход, который я реализовал в геме Knapsack Ruby.
После измерения времени выполнения теста мы можем назначить отдельные тестовые файлы между параллельными серверами CI, чтобы убедиться, что сборка CI не имеет узких мест.
С помощью библиотеки ранца вы можете запускать тесты для многих средств выполнения тестов на Ruby, таких как RSpec, Minitest, Cucumber, Spinach и Turnip. Используя среду выполнения тестов, гем Knapsack может создать список тестов, которые будут выполняться на конкретном узле CI.
Я улучшил этот способ разделения тестов, измерив время тестовых файлов для каждого коммита и веток git. В видео ниже я показываю, как в Knapsack Pro детерминированно работает статическое разделение тестов в обычном режиме. В следующем разделе вы узнаете о некоторых крайних случаях этого подхода и о том, как их решить.
Проблема со статическим разбиением тестов
Собирая информацию от пользователей, я обнаружил, что статическое распределение тестов - не всегда хорошее решение. Иногда некоторые тесты имеют случайное время выполнения, которое зависит, например, от того, насколько загружен CI-сервер или от того факта, что тест не проходит из-за ошибки программного обеспечения, прекращения работы быстрее, чем обычно, и т. Д.
Например, тесты с использованием браузера могут иметь колебания во времени выполнения (тесты в Capybara в Ruby или тесты E2E в JavaScript).
Проблема также растет в зависимости от того, какой CI-сервер вы используете. Обладает ли каждая из параллельных машин CI одинаковой производительностью или они совместно используют ресурсы, такие как ЦП или ОЗУ? Контейнер CI работает в общей среде? Если узел CI перегружен, наши тесты, конечно, могут быть медленнее.
Кроме того, возникнут проблемы с тем, запускаются ли все параллельные машины в одно и то же время или нет. Если вы приобрели пул параллельных серверов CI, его может использовать кто-то другой, например другая сборка CI из текущего проекта или другого проекта из вашей организации.
Если не все узлы CI запускаются одновременно или время загрузки определенных шагов в середине выполнения узла CI может занять произвольное время, то мы хотели бы иметь возможность убедиться, что все машины CI завершают свою работу в определенное время. аналогичный момент. Машины с медленным CI или те, которые начали работать поздно, должны проводить меньше тестов, а те машины, которые начали работать раньше, могут легко сделать больше.
Все параллельные узлы CI должны прекратить работу в одно и то же время, чтобы избежать узкого места, то есть перегрузки машины тестами.
Сплит динамических тестов
Решение вышеуказанной проблемы заключается в динамическом разделении тестов между параллельными машинами в рамках одной сборки CI. Это проблема, над которой я работал в последние годы, создав библиотеку Knapsack Pro и режим очереди для Ruby и JavaScript с поддержкой нескольких популярных средств запуска тестов, таких как Jest или Cypress.
Идея проста. У нас есть набор тестов, которые ставятся в очередь на сервере Knapsack Pro. Отдельные параллельные машины CI используют очередь с Knapsack Pro API, пока она не закончится. Благодаря этому тесты оптимально распределяются между CI-серверами, помогая избежать узкого места в виде перегруженного (слишком медленного) CI-сервера. Ниже вы можете увидеть пример:
Разделение набора динамических тестов решает нашу проблему со случайным временем выполнения теста, с медленными серверами CI или с перегруженными серверами, которые работают медленнее. Независимо от того, когда они начинают или заканчивают работу - важно, чтобы они не проходили слишком много тестов, пока не закончат свою текущую работу.
Посмотрите, как работает разделение динамического набора тестов в режиме очереди для Knapsack Pro.
Реализация Knapsack Pro на Ruby и JavaScript
Knapsack Pro имеет встроенную поддержку многих популярных CI-серверов. Это также независимый инструмент CI, поэтому вы можете использовать любой сервер CI. Все, что вам нужно сделать, это настроить команду Knapsack Pro для каждого параллельного сервера CI, работающего в рамках одной сборки CI. Ниже вы можете увидеть общий пример того, как config YAML может искать CI-сервер с Knapsack Pro:
Выводы
Knapsack Pro поддерживает Ruby и несколько средств запуска тестов на JavaScript, таких как Jest и Cypress, но есть планы добавить поддержку дополнительных средств запуска тестов и языков программирования. Я хотел бы услышать, что вы используете для тестирования приложений и какие серверы CI. Вы можете связаться со мной в LinkedIn, а более подробную информацию об описанном решении вы можете найти на KnapsackPro.com. Надеюсь, эта статья была вам полезна. :)
Первоначально опубликовано на https://docs.knapsackpro.com 2 февраля 2020 г.