open atlas
↑ К треку
CI/CD-пайплайны CICD · 06 · 03

Шардинг тестов и карантин flaky-тестов

У масштабирования тест-сьюта два режима отказа: серийный сьют, гейтящий каждый мердж, и flaky-тесты, которые компаундятся и держат CI красным почти всегда. Шардь по времени, а не по числу файлов; детектируй flake на ретрае и уводи с гейта — не делай глобальный ретрай политикой.

CICD Senior ◷ 18 min
Уровень
ОсновыJuniorMiddleSenior

Сьют из 600 тестов крутился 40 минут серийно на каждом мердже — раздражает, но терпимо. Потом он начал падать. Не из-за чьего-то кода: несколько интеграционных тестов сползли до прохождения примерно в 95% запусков, и на 600 тестах это скомпаундилось в красные сборки примерно в трети случаев. Команда сделала очевидное и добавила retries: 2 в конфиг раннера. CI стал медленнее — перепрогон падений не бесплатен — а красное упало, и все забыли. Через три недели в прод уехал баг в платежах. Тот «flaky»-тест, что мигал красным-зелёным, всё это время ловил настоящую гонку; политика ретрая тихо превращала его истинный негатив в зелёную галочку со второй попытки. Этот урок — про две вещи, которые ломают тест-сьют на масштабе: он слишком медленный, потому что крутится серийно, и ему нельзя доверять, потому что flakiness компаундится. Шардинг чинит первое. Детекция и карантин — а не ретраи — чинят второе.

Шардинг: превратить 40 серийных минут в total/N

За десять минут ты поймёшь, почему шардинг по числу файлов почти не помогает, как flakiness делает здоровый сьют красным в 95% случаев и что с этим делать вместо добавления ретраев.

У сьюта, который крутится серийно, wall-clock равен его суммарному CPU-времени. Шардинг (разбиение сьюта на параллельные фрагменты) разбивает тесты на N параллельных раннеров — CI-матрицу — так что каждый раннер выполняет лишь подмножество, и wall-clock падает примерно до total / N + overhead. Раннер даёт тебе флаг среза; CI даёт матрицу, которая запускает N копий джобы, каждую со своим индексом среза.

# GitHub Actions: матрица разворачивает одну джобу в N параллельных шардов
jobs:
  test:
    strategy:
      fail-fast: false           # падение одного шарда не должно отменять остальные
      matrix:
        shard: [1, 2, 3, 4, 5, 6, 7, 8]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npx playwright test --shard=${{ matrix.shard }}/8

Каждый шард запускает --shard=i/8, выполняя примерно одну восьмую тестов. Затем финальная джоба мерджит пошардовые отчёты в один результат — у тебя не восемь отдельных вердиктов pass/fail, а один. Механика такая: разворачиваешь по индексу среза, гоняешь подмножество, собираешь частичные отчёты, мерджишь.

Ловушка в том, как раннер выбирает подмножество каждого шарда. Наивное разбиение — по числу файлов или по алфавиту: поделить 600 файлов на 8 групп по 75. Выглядит сбалансированно, но это не так, потому что тесты не равны по длительности: медленные интеграционные тесты кучкуются, один шард наследует большинство из них, и этот шард становится стрэглером (отстающим). Wall-clock ограничен самым медленным шардом, а не средним, так что если шард 3 идёт 18 минут, пока остальные семь финишируют за 6, твой сьют занимает 18 минут, и семь раннеров простаивали. Ты купил параллелизм и получил одну быструю полосу плюс семь пустых.

# Фикс: баланс по ВРЕМЕНИ, а не по числу файлов.
# Записывай длительности тестов из прошлых прогонов, затем разбивай так,
# чтобы каждый шард нёс примерно равное суммарное время (медленные тесты распределяются).
npx playwright test --shard=3/8   # раннер читает данные о времени для балансировки шардов
# Jest: --shard плюс time-aware sequencer; многие CI-инструменты (Knapsack,
# встроенный split) делают то же — передавай длительности из прошлого прогона.

Убывающая отдача: оверхед задаёт пол

Шардинг не бесплатен в расчёте на раннер. Каждый шард платит фиксированную цену старта — checkout, установка зависимостей, прогрев кеша — до того, как выполнит хоть один тест. Назовём этот оверхед c. Wall-clock — это total / N + c. Поднимая N, ты ужимаешь total / N к нулю, но c не двигается, так что за некой точкой ты добавляешь раннеры, каждый из которых тратит на старт больше времени, чем на тесты. Восемь раннеров на 40-минутном сьюте с двухминутным оверхедом дают 5 + 2 = 7 минут; тридцать два раннера дают 1.25 + 2 = 3.25 минуты за вчетверо больше машин — а счёт растёт с N. Правило большого пальца: бери N там, где total / N всё ещё кратно нескольким c. Дальше ты платишь за оверхед, а не за скорость.

Flaky-тесты: математика, которая держит сьют красным

Flaky-тест проходит часть запусков и падает в части на том же коммите — скажем, зелёный в 99% прогонов. Один такой — досада. Теперь масштабируй. Исходы тестов примерно независимы, так что вероятность того, что весь сьют зелёный, — это произведение вероятностей прохождения каждого теста: P(green) = (1 - flake)^N. Подставь 1% flake на 300 тестов: 0.99^300 ≈ 0.05. Сьют зелёный примерно в 5% случаев — он красный в 95% прогонов по причинам, не имеющим отношения к ревьюимому изменению.

Это и есть убийца на масштабе, и урон сначала социальный, потом технический. Когда сьют красный почти всегда на зелёной кодовой базе, разработчики учатся, что красное ничего не значит. Они перестают читать падения, слепо ретраят до зелёного или мерджат на чутьё. И в тот момент, когда красное перестаёт быть сигналом, настоящее падение проскальзывает в шуме — никто не смотрит дважды на ещё одну красную сборку. Сьют стал игровым автоматом.

Детектируй, карантинь, чини — а не слепо ретрай

В сеньорском плейбуке три шага по порядку:

  1. Детектируй автоматически. Перепрогони падение один раз на том же коммите. Тест, который падает, а затем проходит без изменения кода, по определению flaky — пометь его и веди flake-rate по тесту со временем. Ретрай здесь — классификатор, а не лекарство: его единственная работа — отличить flaky от реального.
  2. Карантинь. Убери известно-flaky тесты с блокирующего пути — гоняй их в отдельной негейтящей полосе, которая всё ещё записывает результаты, так что они перестают валить мердж, пока ты за ними наблюдаешь. Гейт сразу снова начинает что-то значить.
  3. Чини корневую причину. У flaky-теста есть баг детерминизма — допущение о тайминге, общий фикстур, неотожиданный промис, зависимость от порядка. Почини это, докажи стабильность, верни тест на гейт.

Вместе эти три шага значат: гейт остаётся доверенным всё время — детект даёт классификатор, карантин даёт немедленное облегчение, а фикс замыкает петлю и тест зарабатывает возврат. Без шага 2 ты просто наблюдаешь, как накапливается flakiness; без шага 3 карантинная полоса растёт и в итоге на неё никто не смотрит.

Антипаттерн — делать глобальный авто-ретрай (retries: 3 на весь сьют) постоянной политикой. Выглядит как фикс, потому что красное уходит, но делает три плохие вещи: прячет flakiness вместо того, чтобы вынести её на починку, замедляет каждую сборку перепрогоном, и — то, что кусается, — маскирует тесты, которые flaky именно потому, что ловят настоящий перемежающийся баг. Ретрай, превращающий красное настоящей гонки в зелёное со второй попытки, выбрасывает ровно тот сигнал, что был нужен. Ретрай хорош как механизм детекции; он яд как постоянная политика гейта.

Почему это работает

Почему крошечный flake-rate на тест делает большой сьют красным почти всегда, и почему глобальный ретрай — неверный фикс? Потому что независимые вероятности flake компаундятся мультипликативно по сьюту: P(all pass) = (1 - p)^N, так что даже p = 1% на N = 300 тестов даёт около 5% шанса, что весь сьют зелёный — он красный примерно в 95% прогонов вообще без изменения кода. Глобальный авто-ретрай замазывает симптом, но замедляет каждую сборку перепрогоном, и хуже — маскирует тесты, которые flaky именно потому, что ловят НАСТОЯЩИЙ перемежающийся баг; ретрай, переворачивающий такой тест из красного в зелёное, выбрасывает единственный нужный сигнал. Фикс — детектировать-и-карантинить flaky-тесты с гейта и чинить их детерминизм, чтобы гейт оставался доверенным и настоящее падение не пряталось в рутинном красном.

Выбери лучший вариант

Сьют из 600 тестов красный примерно в 30% случаев из-за горстки flaky интеграционных тестов и медленный, потому что крутится серийно. Как сделать его и быстрым, и доверенным?

Викторина

Ты шардишь сьют на 8 раннеров, разбив тест-файлы по алфавиту на 8 равных групп. Wall-clock едва улучшается против серийного. Почему и что чинит это?

Викторина

В твоём большом сьюте есть тест, flaky примерно в 1% случаев. Команда добавляет глобальную политику retries: 3, и красное уходит. Что не так с этим как с постоянным фиксом?

Вспомните перед уходом
  1. 01
    Объясни шардинг тестов: механику, почему наивное разбиение проваливается и где наступает убывающая отдача.
  2. 02
    Почему крошечный flake-rate делает большой сьют красным почти всегда, и каков плейбук детект-карантин-фикс против слепого ретрая?
Итог

Тест-сьют ломается на масштабе двумя способами, и у каждого свой фикс. Он становится слишком медленным, потому что крутится серийно: шардинг разбивает тесты на N параллельных раннеров (CI-матрицу), каждый гоняет подмножество через —shard=i/N, после чего финальная джоба мерджит пошардовые отчёты в один вердикт, так что wall-clock падает с серийного total до примерно total/N + overhead. Ловушка — наивное разбиение по числу файлов или по алфавиту: тесты разнятся по длительности, медленные кучкуются в один шард-стрэглер, и поскольку wall-clock ограничен самым медленным шардом, балансируй по записанному времени каждого теста. Отдача убывает, потому что каждый шард платит фиксированную цену старта c, так что за точкой, где total/N — небольшое кратное c, ты платишь за оверхед, а не за скорость. Второй отказ — доверие: flakiness компаундится, потому что P(green) = (1 - flake)^N, так что 1% flake на 300 тестов оставляет сьют зелёным лишь в ~5% прогонов — красным почти всегда, что приучает разработчиков игнорировать красное и пускает настоящее падение проскользнуть. Сеньорский плейбук — детект (ретрай один раз на том же коммите, чтобы классифицировать flaky-vs-реальное), карантин (увести flaky-тесты в негейтящую полосу, которая всё ещё записывает результаты, чтобы гейт оставался доверенным) и починка бага детерминизма, затем возврат теста на гейт. Глобальный авто-ретрай как постоянная политика — антипаттерн: он прячет flakiness, замедляет каждую сборку и маскирует flaky-тест, который на деле ловит настоящий перемежающийся баг; ретрай — сигнал детекции, не политика гейта. Теперь, когда видишь сьют, красный большую часть времени на зелёной кодовой базе, — не добавляешь ретраи, а смотришь на flake-rate по тестам, карантинишь главных виновников и чинишь их детерминизм.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 5 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.