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

Когда прогон перестаёт укладываться вовремя
Набор тестов может казаться нормальным, пока внезапно не перестанет быть таким. Один месяц он завершается за 4–5 минут. Через несколько релизов тот же pull request сидит в CI 30–40 минут, и каждое небольшое изменение начинает казаться дорогим.
Эта задержка меняет рабочие привычки. Инженеры объединяют больше кода в одном коммите, чтобы не ждать снова. Ревью замедляются, потому что обратная связь приходит поздно. Смердженные ветки накапливаются. Небольшие исправления, которые должны занять десять минут, растягиваются на полдня, потому что каждая проверка блокирует следующий шаг.
Проблема — не только в суммарном времени выполнения. Долгие ожидания делают обратную связь менее полезной. Если CI отвечает слишком медленно, разработчики переключаются на другие задачи, а потом возвращаются к красному билду без контекста в голове. Простейшие баги дольше понимаются и исправляются.
Прежде чем менять что‑то, отделите медленные тесты от нестабильных. На плохой день они могут выглядеть похоже, но тратят время по-разному. Медленные тесты стабильно проходят и жгут минуты на каждом запуске. Нестабильные тесты падают время от времени, вызывают перезапуски, повторные прогоны и дополнительные расследования. Если смешать оба типа, вы начнёте решать не ту проблему.
Перезапуски это скрывают. Команда может думать, что прогон занимает 18 минут, потому что финальный зелёный прогон показал 18. На деле люди ждали 35 минут, потому что два джоба упали, кто‑то нажал rerun, и следующий прогон прошёл. Дашборд выглядит лучше, чем реальный опыт разработчика.
Именно поэтому шардирование тестов в CI должно начинаться с точной диагностики, а не с увеличения параллелизма. Если один пакет всегда медленный — шардируйте для скорости. Если несколько тестов падают случайно — сначала исправьте или изолируйте их. Иначе вы разнесёте нестабильные тесты по большим числу воркеров, скроете источник задержки и сделаете пайплайн громоздким без ощутимого выигрыша в скорости.
Простое правило работает: измеряйте реальное время ожидания людей, считайте количество перезапусков и назовите нестабильные тесты, прежде чем решать, как делить набор.
Выберите единицу, по которой делить
Определите единицу шардирования до добавления раннеров. Этот выбор решает, будут ли джобы завершаться примерно одновременно или один медленный шард всё равно будет задерживать всех.
Шардирование по файлам — самый простой вариант. Берёте полный список файлов тестов, делите его на равные группы и запускаете одну группу на джоб. Это хорошо работает, когда большинство файлов занимают примерно одинаковое время и каждый файл можно запускать самостоятельно без сложной подготовки. Обычно это самый быстрый первый шаг, потому что большинство тестовых инструментов уже поддерживает запуск выбранных файлов.
Шардирование по пакетам подходит для кода с ясными границами модулей. Если тесты в billing, auth и search в основном остаются внутри этих областей, делите по пакетам или папкам и сохраняйте эту структуру в CI. Это легче объяснить, чем шардирование по файлам, и ошибки чаще можно локализовать по названию шарда.
Разбиение по историческому времени выполнения обычно даёт наилучший баланс. Вместо подсчёта файлов или пакетов вы используете прошлые данные времени и стремитесь дать каждой джобе примерно одинаковое суммарное время выполнения. Если один файл занимает 8 минут, а десять других — по 10 секунд, данные по времени не дадут тяжёлому файлу разрушить один шард. Это подходящая стратегия, когда в наборе есть пару очень медленных тестов среди множества быстрых.
Торговля очевидна. Шардирование по файлам легко настроить, но становится неравномерным по мере разброса времён. Шардирование по пакетам удобно, когда репозиторий уже хорошо сгруппирован, но разваливается, если размеры пакетов сильно различаются. Разбиение по историческому времени требует больше настроек и поддержания, но обычно даёт самое короткое реальное время ожидания.
Поддержка важнее, чем многие команды ожидают. Файловые и пакетные разбиения требуют чистки по мере изменения набора тестов. Разбиение по времени требует хранения таймингов и запасного варианта для новых тестов без истории.
Хорошая отправная точка: используйте шардирование по файлам, если набор тестов ещё относительно ровный; по пакетам — если репозиторий уже отражает границы продукта; переходите к историческому разбиению, когда одна‑две медленные области делают шарды перекошенными. И что бы вы ни выбрали, показывайте результаты по каждому шарду, чтобы нестабильные тесты не исчезли внутри сокращённого пайплайна.
Измерьте набор тестов прежде чем менять его
Начните с базовой метрики. Если вы броситесь в шардирование без неё, можно визуально ускорить дашборд, а разработчики будут по‑прежнему ждать так же долго.
Возьмите недавние прогоны за обычную неделю, а не за один удачный день. Запишите четыре числа для каждого прогона: общее время набора, время в очереди до старта джобов, самый длинный джоб и полное время ожидания от коммита до результата. Последнее число — то, что реально чувствует команда.
Потом посмотрите, куда уходит время. У многих команд небольшой процент файлов или пакетов «съедает» большую часть времени. Один шард заканчивает за 6 минут, другой тянется 24, и весь пайплайн ждёт медленную полосу.
Полезный пример базовой метрики может выглядеть так:
- Медиана полного времени ожидания: 38 минут
- Медиана времени в очереди: 7 минут
- Самый медленный джоб тестов: 21 минута
- Топ‑15 файлов тестов: 46% от общего времени
- Нестабильные падения и перезапуски в неделю: 18
Последняя строка важна. До изменений отсчитайте нестабильные падения, таймауты и перезапуски. Если число уменьшается после шардирования, проверьте почему. Улучшение баланса — это хорошо. Автоматические перезапуски, которые скрывают нестабильные тесты, — нет.
Поставьте одну ясную цель. «Сократить медиану ожидания с 38 до 20 минут, сохранив понятные сигналы о падениях» — достаточно. «Сделать CI быстрее» слишком расплывчато. Нужна цель, которая покажет, помогло ли изменение или время просто сместилось в другую часть пайплайна.
Небольшой пример показывает это. Если набор занимает 30 минут, но 9 из них — очередь, а 14 — один перегруженный шард, простое добавление шардов не решит проблему. Возможно, нужно лучше сбалансировать тесты, уменьшить конкуренцию за раннеры или и то, и другое.
Сначала измерьте, затем делите. CI не прощает догадок.
Настройка шардирования по файлам шаг за шагом
Шардирование по файлам — самый простой старт. Берёте полный список файлов тестов, делите на несколько групп и запускаете эти группы параллельно. Если у вас 120 файлов, начните с 4 шардов по ~30 файлов.
Пусть первая версия будет скучной. Делите файлы по счёту, а не по интуиции. Это даст чистую базу и покажет, ровен ли набор или сильно смещён.
Роллаут может быть простым. Соберите список файлов в стабильном порядке, разделите его на равные группы, запустите одну группу в каждом джобе одной и той же командой и сохраняйте время выполнения каждого шарда после каждого прогона.
Сделайте окружение одинаковым для всех шардов. Используйте одинаковый размер машины, один и тот же шаг установки зависимостей, одинаковые правила кэша и одинаковую начальную работу. Если шард 3 тратит две дополнительные минуты на сборку тестовых данных или скачивание образа, ваши числа теряют смысл.
Наблюдайте за разбросом между самым быстрым и самым медленным шардом в течение недели–двух. Если три шарда завершают за 6 минут, а один — за 14, то на бумаге всё ровно, но на практике шард не работает. Весь пайплайн ждёт самый медленный джоб.
Тогда переносите «выбивающиеся» файлы. Несколько больших интеграционных тестов часто создают длинный хвост. Перенесите один‑два тяжёлых файла из медленного шарда в более быстрый и снова измерьте. Небольшие перемещения работают лучше, чем постоянные перестановки.
Ведите запись времени по каждому шарду после прогона. Сначала достаточно простых логов или метрик CI, чтобы видеть дрейф во времени.
Одно предупреждение: не скрывайте видимость нестабильных тестов во время настройки. Если один файл часто падает, пусть этот провал остаётся привязанным к шару, который его запускал. Повторы могут помочь разблокировать мерджи, но они размывают сигналы на этапе, когда вы всё ещё изучаете, где боль.
Большинство команд получают ощутимый выигрыш от шардирования по файлам прежде, чем переходить к более умным методам. Это просто, понятно и даёт реальные тайминги для следующего этапа очистки.
Используйте пакетное шардирование, когда код уже хорошо сгруппирован
Шардирование по пакетам работает, когда кодовая база уже имеет чёткие границы модулей. Если пакет auth в основном тестирует аутентификацию, а billing — биллинг, вы можете делить по пакетам с минимальной настройкой и сократить ожидание без риска.
Этот подход легче понять, чем на уровне файлов. Инженеры видят, как шард соответствует части продукта, и когда один шард замедляется, причина обычно очевидна.
Проблема — структура. Шардирование по пакетам полезно, только если пакеты отражают реальную ответственность и реальную стоимость выполнения. Кодовая база с одним огромным пакетом «common» и десятком крошечных пакетов плохо сбалансируется, даже если количество шардов выглядит аккуратно.
Держите общие тестовые хелперы вне огромного пакета, который тянет на себе каждый прогон. Поместите фикстуры, билдэры, моки и код подготовки в отдельную утилитную область, чтобы один пакет не превратился в свалку несвязанных тестов.
Обычная ошибка — сваливать все интеграционные тесты в один пакет, потому что они медленнее и кажутся отличными от юнит‑тестов. Это обычно создаёт один ползущий шард, пока остальные заканчивают рано. Это также прячет, куда уходит время. Если интеграционный тест принадлежит payments, orders или search, держите его рядом с модулем, если нет серьёзной причины вынести.
Здоровое пакетное шардирование обычно следует нескольким простым правилам:
- группируйте тесты по модулю продукта, а не только по типу теста
- вынесите общие хелперы в нейтральное место
- следите за пакетом, который растёт быстрее остальных
- разделяйте большие пакеты до того, как они станут новым узким местом
Проверяйте баланс пакетов каждые несколько недель. Команды постепенно добавляют тесты, так что шард, который выглядел ровным месяц назад, может стать перекошенным.
Если один пакет начинает занимать половину времени набора — воспринимайте это как сигнал. Может быть, модуль нуждается в более тонком разбиении. Может быть, туда свалилось слишком много несвязанных тестов из‑за удобства. Исправление этого рано сохраняет честность шардирования и не прячет медленные и нестабильные тесты в одном гигантском бакете.
Используйте данные по времени выполнения, когда тесты очень разные
Когда один файл выполняется 6 секунд, а другой — 4 минуты, подсчёт файлов перестаёт работать. Вы можете дать каждому шарду одинаковое количество файлов и всё равно получить один медленный джоб, который держит весь пайплайн открытым. В таком случае шардирование должно опираться на недавние тайминги, а не на сырые счётчики файлов.
Историческое разбиение по времени хорошо работает, когда в наборе есть достаточно повторяющихся прогонов, чтобы проявилась картина. Большинству команд хватает среднего по последним 5–20 успешным прогонам. Это сглаживает один‑два плохих результата из‑за занятых раннеров, холодного кэша или медленной сети. Если набор часто меняется в течение недели — берите меньшее окно; если стабилен — большее окно даст спокойнее распределение.
Простое правило помогает: суммируйте недавние времена для каждого файла, сортируйте по самым медленным и кладите каждый файл в шард с наименьшей текущей суммой. Обычно это даёт гораздо более равные времена шардов, чем деление по количеству файлов или пакетов.
У новых и переименованных тестов должен быть запасной план. Если у файла нет истории, поместите его по простому резервному правилу, например по счёту файлов или пакету, затем измерьте в следующем прогоне. Не переставляйте всё агрессивно: один неизвестный тест не должен перекраивать весь расклад.
Делайте ребаланс предсказуемым
Ребалансируйте по расписанию, а не на каждом прогоне. Достаточно ежедневного или еженедельного обновления для большинства команд. Если переставлять тесты при каждом запуске, мелкие флуктуации будут постоянно перемещать тесты. Это усложняет сравнение падений и может скрыть паттерны в нестабильных тестах.
Держите отчётность по падениям отдельно от ребалансировки. Нестабильный тест остаётся нестабильным, даже если он переехал в другой шард. Отслеживайте, какие тесты падают чаще всего, какие сильно колеблются по длительности и какие шарды всё ещё финишируют последними. Быстрые суиты важны, но ясная видимость — не менее важна.
Этот метод обычно окупается, когда суита вырастает до точки, где «10 файлов на шард» кажется справедливым, но ощущается медленным.
Простой пример из растущей команды
Небольшая продуктовая команда в начале года имела набор, который завершался примерно за 8 минут. Через 12 месяцев тот же пайплайн занял 45 минут на каждом merge request. Ничего драматичного не произошло: приложение росло, покрытие браузером добавлялось, и пара медленных интеграционных тестов оказались в тех же CI‑джобах.
Первым решением стало шардирование по пакетам, потому что репозиторий уже имел папки API, web и workers. Это немного помогло, но один пакет тянул большую часть дорогих тестов. Шард API — 7 минут, workers — 5, а web всё ещё тянул около 24 минут. Все остальные ждали медленный шард.
Потом они попробовали равное количество файлов внутри web‑тестов. На бумаге это выглядело честно. На практике — нет. Один шард получил 110 маленьких файлов юнит‑тестов и завершился за 6 минут. Другой получил 108 файлов, но несколько из них запускали браузеры, сеяли данные и ждали асинхронные UI‑флоу. Тот шард занял 19 минут. То же количество файлов — очень разные расходы.
После недели измерений времени по файлам они изменили подход. Вместо деления по пакетам или счёту файлов они перешли на историческое разбиение по времени. Каждый шард получил предсказуемое итоговое время, даже если число файлов было разным. Один джоб теперь выполнял 65 файлов, другой — 140, и это было нормально, потому что оба финишировали примерно за 10–12 минут. Полный этап тестов сократился с 24 до ~11 минут.
Они также приняли разумный подход к нестабильным браузерным тестам. Они не скрывали их массовыми перезапусками в каждом джобе. Это сделало бы пайплайн визуально здоровее, но не помогло бы с реальными проблемами. Они повторно запускали только явные инфраструктурные ошибки один раз, держали браузерные тесты в отдельной группе и отслеживали файлы, которые падали чаще раза в неделю. Люди видели флейки и действительно их фиксили.
Такая картина типична для тонких CI. Разделение, которое работает при 8 минутах, часто ломается при 45. Когда суита становится неравномерной, данные по времени обычно лучше аккуратных границ папок.
Ошибки, которые делают цифры лучше, чем опыт
Разделённый набор может выглядеть быстрее на графиках, тогда как разработчики по‑прежнему ждут так же. Это происходит, когда отчёт изменяется сильнее, чем реальное узкое место.
Самая частая ловушка — автоматические перезапуски при каждом падении. Перезапуск может сохранить зелёный статус, но скрывает нестабильные тесты и приучает команду игнорировать шум. Если тест упал и прошёл на повторе, отметьте это. Отделяйте видимость флейков от процента проходов, иначе суита постепенно ухудшится.
Ещё ошибка — смешивать очень разные тесты в одном пуле. Быстрые юнит‑тесты и медленные интеграционные флоу ведут себя по‑разному. Юнит‑тесты обычно хорошо масштабируются по шардам. Интеграционные тесты часто борются за общие данные, внешние сервисы, порты или долгую подготовку. Если смешать их без меток, шардирование будет выглядеть сбалансированным, пока один шард не получит три медленных флоу и не заблокирует все мерджи.
Команды также забывают про работу вокруг тестов. Команда тестов может выполняться 4 минуты, но джоб всё равно тратит ещё 6 минут на подготовку окружения, потерянные кэши, установку зависимостей, запуск контейнеров, сидирование базы или создание тестовых данных. Если измерять только саму прогонку тестов, вы будете делить не то. В некоторых пайплайнах сокращение времени подготовки даёт больше пользы, чем добавление шардов.
Средние значения могут обмануть. Если семь шардов финишируют за 3 минуты, а один — за 11, пайплайн всё равно займёт 11. Время ждёт самых медленных, а не среднее. Поэтому дашборд, который хвалится средним временем, может скрыть плохой расклад.
После запуска держите в фокусе короткий список чисел:
- общее время пайплайна
- время самого длинного шарда
- время подготовки перед стартом тестов
- количество нестабильных падений за прогон
Одна растущая команда столкнулась именно с этим. Они удвоили число шардов и отпраздновали снижение среднего времени, но мерджи оставались медленными, потому что один шард утащил за собой большинство браузерных тестов. После того как они размечали типы тестов, измерили подготовку отдельно и убрали тихие перезапуски, настоящая проблема стала очевидна. Суита не сократилась. Сократилась только диаграмма.
Короткий чеклист для внедрения
Первая версия шардирования обычно быстро сокращает ожидание, но развёртывание проваливается, если людям нельзя доверять вывод. Хороший шард — легко читаемый, легко перезапускаемый и легко поддерживаемый в актуальном состоянии.
Прежде чем считать внедрение завершённым, проверьте простые детали, которые облегчают ежедневную работу.
Каждый шард должен отчётливо показывать ошибки, чтобы инженеры могли это просканировать за секунды. Включайте номер шарда, список файлов или диапазон пакетов, которые он запускал, и точные имена упавших тестов в вывод джоба.
Перезапуски тоже должны быть простыми. Один инженер должен уметь перезапустить тот же шард локально или в CI одной ясной командой, не угадывая, какие файлы туда попали.
Если вы используете историческое разбиение, обновляйте тайминги достаточно часто, чтобы это соответствовало изменению кодовой базы. Занятые команды часто обновляют ежедневно. Малым командам хватает обновления каждые несколько дней, но раз в месяц — слишком редко.
Проверяйте количество нестабильных тестов еженедельно. Держите счётчик по шардам и по тестам, иначе команда начнёт считать случайные падения проблемой шардирования.
Понятный вывод ошибок звучит мелочью, но экономит время каждый день. Если шард 6 падает, и никто не понимает, что он запускал, инженеры лезут в логи, ищут конфигурацию джоба и теряют большую часть преимуществ разделения.
Нестабильные тесты заслуживают отдельного внимания, а не того, чтобы их зарыли в общий шум CI. Если шард падает случайно дважды в неделю, исправьте тест или пометьте его и изолируйте. Не позволяйте шардированию скрыть проблему.
Если что‑то из этого базиса отсутствует — приостановите развёртывание и исправьте сначала. Разделённый набор тестов должен облегчать работу с первого дня, а не усложнять её.
Что делать после первого запуска
Не переворачивайте весь набор сразу. Начните с одной ветки, одного сервиса или одной группы тестов, которые дают наибольшую боль. Это уменьшит радиус воздействия и даст команде чистое сравнение «до/после», а не разбросанные изменения по всем пайплайнам.
Первые две недели рассматривайте новое шардирование как эксперимент. Следите за числами, но также наблюдайте за поведением команды. Если инженеры по‑прежнему ждут так же долго из‑за улучшившихся перезапусков, дашборд может выглядеть лучше, а повседневный опыт — нет.
Отслеживайте короткий набор метрик: полное время от пуша до результата теста, число перезапусков в день или на MR, ложные срабатывания вроде таймаутов или отсутствующего вывода тестов, и как часто один шард финиширует значительно позже остальных.
Небольшой прогресс уже успех. Если медленная суита упала с 28 до 16 минут и количество перезапусков осталось на том же уровне — это реальный выигрыш. Если упала до 14, но люди теперь тратят 10 минут на то, чтобы понять, какой шард упал — выгоды почти нет.
Общайтесь с инженерами, которые работают с пайплайном чаще всего. Задайте простой вопрос: стало ли проще обрабатывать падения, сложнее или просто по‑другому? Их ответ обычно говорит больше, чем графики сборок.
Будьте готовы откатить шардирование, если оно не помогает. Некоторые команды сохраняют запутанную конфигурацию только потому, что в неё вложили усилия и никто не хочет признать ошибку. Это обычно ухудшает ситуацию. Если шардирование добавляет шум, прячет ошибки или почти не улучшает время отклика — откатите и попробуйте более простое решение.
Если пайплайн всё ещё тянет после первой итерации, настоящая проблема может быть глубже. Подготовка тестов, запуск контейнеров, сбросы баз данных и плохое использование кэша часто тратят больше времени, чем сами тесты. Если нужен внешний обзор, Oleg Sotnikov из oleg.is работает со стартапами и небольшими командами по CI, инфраструктуре и рабочим процессам с поддержкой AI — и такие узкие места стоит исправлять как можно раньше.
Часто задаваемые вопросы
How do I know if I have a slow suite or flaky tests?
Смотрите на повторяемость. Медленные тесты проходят стабильно и тратят время на каждый прогон. Нестабильные тесты падают и проходят через раз, вызывают повторные запуски и создают всплески работы. Измерьте полное время ожидания, количество перезапусков и выпишите тесты, которые падают случайно, прежде чем менять шардирование.
When should I start splitting tests in CI?
Начинайте, когда задержки начинают мешать повседневной работе, а не просто когда набор тестов стал больше. Если PR ждёт 20–30 минут, один джоб завершает намного позже остальных или инженеры пачкают изменения, чтобы не ждать CI, шардирование обычно окупается. Сначала снимите базовую метрику, чтобы понять, помогло ли изменение.
Should I shard by file first?
Чаще всего — да. Шардирование по файлам просто настроить и просто объяснить. Это работает хорошо, когда файлы тестов примерно равны по времени выполнения и каждый файл можно запускать автономно. Начните с равного числа файлов на шард и следите, насколько медленнее становятся самые долгие шарды.
When does package sharding make more sense?
Выбирайте шардирование по пакетам, когда репозиторий уже логично разделён по областям продукта — billing, auth, search и т.д. Тогда инженеры быстрее увидят владельцев и причину замедления. Не используйте его, если один пакет содержит большинство тяжёлых тестов — такой шард всё равно будет тормозить конвейер.
When should I use historical runtime data?
Когда счёт файлов вводит в заблуждение. Если один файл выполняется минуты, а другие — секунды, данные по времени выполнения сбалансируют шарды лучше, чем простое деление по количеству файлов. Берите тайминги из последних нескольких успешных прогонов, расставляйте самые медленные файлы первыми и перестраивайте шарды по расписанию, а не на каждом прогоне.
How many shards should I start with?
Держите первый запуск небольшим. Четыре шарда часто дают достаточно данных и не превращают пайплайн в головоломку. Если добавить слишком много джобов сразу, время подготовки, очередь и вывод ошибок могут ухудшиться, даже если тестовое время упадёт.
What metrics should I watch after the rollout?
Следите за тем, что ощущают люди: полное время от коммита до результата, время в очереди, время самого медленного шарда, количество перезапусков и нестабильных падений. Среднее время тестов может выглядеть лучше, даже если один шард по-прежнему блокирует слияния. Если можно выбрать только одну метрику — берите медиану полного ожидания.
Should I turn on automatic retries for failing tests?
Используйте перезапуски экономно. Один перезапуск для явной инфраструктурной ошибки может разблокировать работу, но массовые перезапуски скрывают нестабильные тесты и создают иллюзию здоровья набора тестов. Сохраняйте видимость первого падения и отслеживайте тесты, которые падают чаще раза в неделю.
What if sharding does not make the pipeline much faster?
Проверьте подготовительные шаги вокруг тестов: установка зависимостей, запуск контейнеров, заполнение БД, потерянные кэши и создание тестовых данных часто съедают больше времени, чем сами тесты. Исправьте самый долгий шаг подготовки или уменьшите нагрузку на раннеры прежде, чем увеличивать число шардов.
How often should I rebalance shard assignments?
Большинству команд хватает ежедневного или еженедельного ребалансинга. Это удерживает время шарда в пределах без постоянного перемещения тестов. Новые или переименованные тесты ставьте по резервному правилу (по файлу или пакету) до накопления истории таймингов.