Паралелізація і швидкість пайплайна
Зміст
Уся цінність CI тримається на одному: швидкому зворотному звʼязку. Пайплайн (pipeline), який їде 40 хвилин, перестає бути страхувальною сіткою й стає податком. Розробник не чекає результату — він перемикається на іншу задачу, накопичує зміни, зливає рідше й більшими партіями. Це рівно та «інтеграційна» біль, яку CI мав прибрати (див. CI/CD: інтеграція, доставка, розгортання). Для AQA це особистий біль: гарний e2e-сьют, результату якого ніхто не дочікується, — це сьют, якому ніхто не довіряє.
Ця глава має бейдж «поглиблення»: при першому проході її можна пропустити й повернутися, коли ваш власний пайплайн почне гальмувати (умовно — за 15–20 хвилин на PR). Тут — єдиний повний виклад економіки швидкості пайплайна на сайті; інші глави посилаються сюди. Суміжні теми лишаються деінде: сам концепт паралельного прогону і його передумови (ізоляція тестів і даних) — у розділі про стратегію автоматизації, а конкретні прапорці раннера (наприклад, workers і --shard у Playwright) — у розділі про інструменти автоматизації. Ми ж дивимося на пайплайн з боку економіки: що паралелити, що кешувати, що взагалі не запускати — і скільки це коштує.
Що саме ми оптимізуємо
Головна метрика — не сумарний час роботи процесорів, а wall-clock: скільки хвилин минає від push до вердикту (зелено/червоно). Саме цей час чекає людина, і саме він визначає, чи працює зворотний звʼязок. Класичний орієнтир — «десятихвилинний білд» (ten-minute build): гайдлайн Extreme Programming, який закріпив Martin Fowler у принципі «Keep the Build Fast» (Humble і Farley у «Continuous Delivery» так само радять тримати commit-стадію в межах приблизно десяти хвилин). Логіка проста: якщо фідбек триває понад приблизно десять хвилин, увага розробника встигає піти на інше, і CI втрачає ефект.
Важелів рівно два, і все в цій главі — один з них:
- Робити менше роботи — кешування й вибірковість.
- Робити роботу паралельно — джоби, matrix, шардинг.
Між ними є прихована напруга. Паралелізація ріже wall-clock, але зазвичай підвищує сумарні хвилини (за які ви платите), бо кожна паралельна одиниця несе власний накладний коефіцієнт. Тому починають завжди з кешу — він майже безкоштовний, — і лише потім розпаралелюють.
Паралельні джоби і matrix
Пайплайн має ієрархію pipeline → stage → job → step (див. Будова пайплайна). Стадії йдуть послідовно за залежностями, а от джоби (job) в одній стадії за замовчуванням стартують паралельно — стільки, скільки дозволяють доступні агенти.
Matrix-білд (matrix build) — це один опис джоби, розгорнутий на набір комбінацій параметрів: браузер × ОС × версія Node. Це декартів добуток: конфігурація «3 браузери × 3 ОС» — це 9 незалежних джобів. Matrix потрібен саме для покриття різних конфігурацій (кросбраузерність, різні рантайми), а не для прискорення. Його ціна — множення хвилин, тому матрицю проріджують: свідомо виключають безглузді комбінації (наприклад, WebKit під Windows), лишаючи тільки ті, що реально несуть ризик.
Не плутайте два різні наміри: matrix відповідає на питання «чи працює той самий тест у різних середовищах», а шардинг (нижче) — на питання «як прогнати ту саму конфігурацію швидше».
Шардинг: розрізати сьют між агентами
Шардинг (sharding) — це поділ одного й того самого набору тестів на M частин (шардів), які виконуються паралельно на M агентах. Ідеально wall-clock падає приблизно як загальний_час / M. На практиці — мінус накладні витрати.
Ці накладні витрати (overhead) — ключ до розуміння меж. Кожен шард платить фіксовану ціну незалежно від кількості тестів у ньому: checkout репозиторію, встановлення залежностей, завантаження браузерів, підняття контейнера. Ці секунди-хвилини — підлога, нижче якої шардинг не опускається. Тому виграш нелінійний: перші шарди дають багато, а після певної межі кожен наступний майже не зменшує wall-clock, зате додає ще одну порцію overhead. Це той самий ефект, що й у законі Амдала: фіксована частина роботи обмежує прискорення.
Друга — жорсткіша — умова: тести мають бути незалежними. Жодного покладання на порядок, жодних спільних мутабельних даних чи облікових записів. Якщо два шарди водночас логіняться під одного тестового юзера, паралельність не прискорить, а породить чергу й гонки. Ця передумова (ізоляція на тест і на воркер) — канон розділу про стратегію автоматизації; шардинг лише оголює її відсутність.
Механічно шардинг — це патерн fan-out / fan-in: сьют розрізають, шарди біжать паралельно, кожен віддає частковий звіт (універсальний формат обміну — JUnit XML, див. Автотести в CI: стабільність і дебаг), а окрема фінальна джоба зливає їх у єдиний вердикт. Без кроку злиття ви отримаєте M розрізнених звітів і жодного цілісного «пройшло / впало».
Балансування шардів
Наївний поділ — за кількістю файлів чи тестів порівну. Проблема в тому, що тести дуже різні за тривалістю: один e2e-флоу з логіном і оплатою може їхати хвилину, а сотня швидких перевірок — кілька секунд. Розбивши «по рівну кількість», ви легко отримаєте шард, що фінішує за 2 хвилини, і сусідній, що висить 12. А wall-clock всього прогону дорівнює найповільнішому шарду — його називають straggler (відстаючий). Решта агентів простоюють, а ви платите за час, який визначив один невдало навантажений шард.
Правильний орієнтир — балансувати за історичним часом, а не за кількістю: розкласти тести так, щоб очікувана сумарна тривалість кожного шарда була приблизно однакова (жадібне «пакування» за минулими замірами). Де інфраструктура це підтримує — ще краще працює динамічний розподіл: черга видає наступний тест тому агенту, який щойно звільнився, тож straggler не виникає за побудовою — ціною окремого шару координації.
Метрика, за якою стежать, — перекіс шардів (відношення часу найповільнішого до найшвидшого): близько до одиниці означає добрий баланс. Пастка балансування за історією: новий важкий тест ще не має замірів, отримує усереднену оцінку й може перекосити розкладку, доки не набере статистики.
Кешування: не робити ту саму роботу двічі
Кеш (cache) у CI — це перевикористання роботи між прогонами пайплайна. Його треба відрізняти від артефакту: артефакт передає результат між джобами одного прогону, кеш зберігає результат між різними прогонами. Це головний канон кешування у розділі — інші глави посилаються сюди.
Три речі, які кешують найчастіше:
- Залежності.
node_modulesабо кеш пакетного менеджера. Ключ кешу мусить містити хеш lock-файлу (package-lock.json): попадання в кеш дозволяє пропуститиnpm install— часто найбільший одиничний пожирач часу. Це найтонше місце: якщо ключ не залежить від lock-файлу, кеш ніколи не інвалідовується — ви тягнете старі залежності й ловите загадкові фейли на, здавалося б, свіжому коді. - Браузери. Playwright та інші рушії щоразу качають сотні мегабайтів бінарників браузерів. Їх кешують за версією інструмента — класичний AQA-специфічний виграш. Альтернатива — готовий Docker-образ із уже вшитими браузерами (див. Docker і тестові середовища).
- Білд. Зібрати застосунок один раз, опублікувати як артефакт, а всі тестові джоби споживають готове замість перезбирання. Плюс шар-кеш самого Docker для образів.
Золоте правило дзеркалить HTTP-кеш у браузері: кеш вартий рівно стільки, скільки варта його інвалідація. Ключуйте за вмістом — хеш lock-файлу, версія браузера, — ніколи за статичним рядком, що не змінюється. Інакше ви ризикуєте регулярно бачити зелений пайплайн, який тестує застарілий код (той самий клас проблеми, що застарілий кеш у браузері).
Вибірковість: найшвидша джоба — та, яку ти не запустив
Найдешевша робота — та, якої не було. Вибірковість — важіль, який недооцінюють, бо він не такий ефектний, як «додати паралельності».
Базовий прийом — розкладати прогони за тригером. Smoke (швидкий набір критичного шляху) — на кожен PR, блокує merge, їде кілька хвилин. Повна регресія — уночі за розкладом (cron), поза критичним шляхом, десятки хвилин. Логіка проста: фідбек на PR мусить вимірюватися хвилинами, тож 90-хвилинну регресію не можна вішати на кожен push — але можна ганяти раз на ніч і ловити те, що smoke пропустив.
Глибший рівень — вибір за змінами (affected/changed-based): запускати тільки тести, що стосуються зміненого коду чи області. Потужно, але ризиковано: якщо мапінг «код → тести» неповний, ви пропустите тест, який саме й зловив би регресію. Тому такий вибір застосовують як прискорювач на PR, а повну нічну регресію лишають страхувальною сіткою. Ще один прийом — fail-fast: скасувати решту джобів після першого падіння, щоб не палити хвилини; ціна — втрачаєте повну картину (виправив одне, наступного прогону червоне вже інше), а на флакі-сьютах fail-fast буває шумним.
Межа зон відповідальності: що саме блокує merge, як розкладати перевірки по стадіях і як coverage працює як гейт — це канон Стратегії якості в пайплайні. Тут ми дивимося на ту саму розкладку під кутом швидкості: вибірковість тримає критичний шлях коротким.
Ціна: хвилини CI, ліміти конкурентності, self-hosted
Hosted-раннери тарифікуються похвилинно. Паралелізація міняє гроші на wall-clock: 10 шардів — це приблизно десята частина очікування, але приблизно вдесятеро більше хвилин (плюс overhead на кожен шард, тож трохи більше). Звідси спадна віддача: після певної межі ви доплачуєте помітні хвилини за секунди зекономленого очікування.
Друге обмеження — ліміт конкурентності (concurrency limit). План або організація визначають, скільки джобів біжить одночасно. Можна оголосити 20 шардів, але зі стелею у 5 паралельних джобів вони підуть чотирма хвилями — і wall-clock визначить стеля, а не число шардів. Шардинг понад ваш ліміт конкурентності не дає нічого. Сюди ж — час у черзі: на завантаженому спільному пулі раннерів джоба може хвилинами чекати старту, і це теж частина затримки фідбеку, хоч її й не видно у власне тривалості тестів.
Self-hosted раннери — це агенти на власному залізі. Замість похвилинної оплати — фіксована вартість; контроль над CPU/RAM (важливо для важких браузерних тестів, див. Docker); за масштабу — часто швидше й дешевше. Ціна цього — ви їх самі підтримуєте, патчите й захищаєте. Окремий ризик: self-hosted раннер, що збирає недовірені PR із форків, — це пряма діра в безпеці (доступ до секретів, виконання чужого коду). Це канон глави про секрети та конфігурацію в CI.
Практичний висновок: спершу міряй, де час, а тоді оптимізуй критичний шлях, а не сумарні хвилини наосліп. Порядок дій майже завжди той самий — кеш (майже безкоштовний wall-clock) → вибірковість → паралельність до стелі конкурентності. Кидати шарди в сьют, який не ізольований, — значить лише помножити флак.
Типові помилки
- Додали 20 шардів, а пайплайн майже не прискорився. Виглядає як брак паралельності, а насправді впираєтесь у ліміт конкурентності (умовно 5 раннерів) або в overhead-підлогу: кожен шард по 3 хвилини качає браузери, і цей setup зʼїдає весь виграш.
- Один шард стабільно висить 12 хвилин, решта — по 3. Виглядає як повільний агент, а насправді нерівне балансування: розбили за кількістю файлів, а не за часом; straggler тримає весь wall-clock.
- Кеш є, а залежності старі — і тести зелені на застарілому коді. Виглядає як баг тесту, а насправді ключ кешу не містить хеша lock-файлу: кеш ніколи не інвалідовується (той самий клас проблеми, що застарілий HTTP-кеш).
- Прибрали нічну регресію, лишили лише smoke на PR — стало швидко. Виглядає як оптимізація, а насправді дірка в покритті: smoke не ловить регресій поза критичним шляхом, і вони течуть у прод між релізами.
- Тести падають лише коли шардів багато. Виглядає як проблема інфраструктури, а насправді тести не ізольовані: два шарди ділять одного тестового юзера чи запис, і паралельність оголила приховану залежність (діагностика — гл.15).
Підсумок
- Метрика, що має значення, — wall-clock від
pushдо вердикту, а не сумарні CPU-хвилини. Оптимізуй критичний шлях. - Важелів два: робити менше (кеш + вибірковість) і робити паралельно (джоби, matrix, шардинг). Кеш — перший, бо майже безкоштовний.
- Шардинг ріже час майже лінійно лише до межі, яку ставлять overhead на шард і ліміт конкурентності; за нею додавати шарди марно.
- Паралельність вимагає ізольованих тестів і даних; без цього вона не прискорює, а оголює флак.
- Кеш вартий рівно стільки, скільки варта його інвалідація: ключуй за вмістом (хеш lock-файлу, версія браузера), ніколи за статичним рядком.
Що питають на співбесіді
- «Ваш пайплайн їде 40 хвилин. Що робитимете?» Інтервʼюер хоче структурний підхід, а не «додам шардів»: спершу поміряти, де час (профіль джобів), потім кеш → вибірковість → паралельність до ліміту. Наосліп кидати паралелізм — червоний прапорець.
- «Чим шардинг відрізняється від matrix?» Перевіряють чистоту понять: matrix — різні конфігурації тих самих тестів (покриття), шардинг — ті самі тести, розрізані між агентами (швидкість).
- «Розбили сьют на 8 шардів, а швидше стало вдвічі, не у 8 разів. Чому?» Очікувані відповіді: overhead на кожен шард, нерівне балансування (straggler), стеля конкурентності.
- «Що ганяти на PR, а що вночі?» Smoke критичного шляху на PR (швидкий блокувальний фідбек), повна регресія nightly. Дивляться, чи розумієте, що короткий фідбек-луп важливіший за повноту на кожному пуші (канон розкладки — гл.19).
- «Коли self-hosted раннери, а коли hosted?» Тредоф ціни, контролю над залізом і вартості підтримки — плюс безпековий нюанс з PR із форків.
За всім цим інтервʼюер дивиться на одне: чи мислите ви економічно (wall-clock проти хвилин), чи не лікуєте симптом «більше паралелізму» без діагнозу і чи памʼятаєте передумову ізоляції.
Джерела
- Martin Fowler. Continuous Integration — принцип «Keep the Build Fast»: martinfowler.com/articles/continuousIntegration.html
- Jez Humble, David Farley. Continuous Delivery (Addison-Wesley, 2010) — концепт deployment pipeline і швидка commit-стадія (орієнтир — кілька хвилин, не більше десяти).
- Playwright. Sharding — механіка розрізання сьюта між агентами: playwright.dev/docs/test-sharding (tool-специфіка прапорців — у розділі про інструменти автоматизації).
- GitHub Actions — документація зі стратегії matrix, кешування залежностей і лімітів конкурентності: docs.github.com/en/actions
- Загальний контекст CI/CD у силабусі ISTQB CTFL 4.0 подано в темі про тестування у DevOps і місце автоматизованих тестів у пайплайні; сама економіка паралелізації окремою темою силабусу не є — основи CI/CD див. у гл.9.
Чому швидкість пайплайна взагалі важлива — хіба тести не пройдуть однаково за 5 і за 40 хвилин?
Пройдуть, але цінність результату різна, бо вся вигода CI тримається на швидкому зворотному звʼязку. Поки пайплайн їде, розробник або чекає (простій), або перемикається на іншу задачу — і тоді до моменту вердикту він уже подумки в іншому контексті, а червоний білд повертає його назад із подвійними витратами на згадування. Довгий пайплайн змушує зливати рідше й більшими партіями, а це рівно та інтеграційна біль, яку CI мав прибрати. Для AQA наслідок особистий: e2e-сьют, результату якого ніхто не дочікується, поступово перестає бути страхувальною сіткою і стає податком, якому ніхто не довіряє. Тому швидкість — не косметика, а умова, за якої автоматизація взагалі працює.
Що таке wall-clock і чому оптимізують саме його, а не сумарні CPU-хвилини?
Wall-clock — це реальний час за годинником від push до вердикту, тобто рівно те, що чекає жива людина. Сумарні CPU-хвилини — це скільки напрацювали всі процесори разом, і вони важливі для рахунку (за них платять hosted-раннери), але не для швидкості фідбеку. Ці дві метрики можуть тягнути в різні боки: розпаралеливши сьют на десять агентів, ви ріжете wall-clock приблизно вдесятеро, але сумарні хвилини навпаки ростуть, бо кожен агент несе власні накладні витрати. Класичний орієнтир швидкості — «десятихвилинний білд» із практик Extreme Programming: якщо фідбек триває понад приблизно десять хвилин, увага розробника встигає піти на інше. Тому головне питання при оптимізації — «де довший критичний шлях», а не «як зменшити загальне навантаження».
Які є важелі прискорення пайплайна і чому починають з кешу?
Важелів по суті два: робити менше роботи (кешування й вибірковість) і робити роботу паралельно (джоби, matrix, шардинг). Між ними прихована напруга — паралелізація скорочує wall-clock, але підвищує сумарні хвилини, за які ви платите, тож це завжди розмін грошей на швидкість. Кеш натомість майже безкоштовний: він не додає паралельних одиниць, а прибирає повторну роботу між прогонами, тому дає виграш wall-clock без помітного зростання рахунку. Звідси канонічний порядок дій: спершу кеш, потім вибірковість (не запускати зайве), і лише наостанок паралельність — до стелі, яку ставить ваш ліміт конкурентності. Кидати шарди в сьют, у якому ще не вичавлено кеш, — значить переплачувати за те, що можна було прибрати даром.
Чим шардинг відрізняється від matrix-білда?
Це два різні наміри, які часто плутають. Matrix розгортає один опис джоби на набір комбінацій параметрів (браузер × ОС × версія рантайму) і відповідає на питання «чи працює той самий тест у різних середовищах» — тобто це про покриття конфігурацій, кросбраузерність. Шардинг ділить один і той самий набір тестів на частини, що біжать паралельно на різних агентах, і відповідає на питання «як прогнати ту саму конфігурацію швидше» — тобто це про швидкість. Практична різниця в ефекті на рахунок: matrix свідомо множить хвилини заради ширшого покриття, а шардинг розмінює хвилини на скорочення очікування тих самих тестів. На співбесіді цим питанням перевіряють чистоту понять: якщо кандидат каже «шардинг — це коли ганяємо в різних браузерах», він плутає покриття зі швидкістю.
Навіщо проріджувати matrix, якщо можна перевірити всі комбінації?
Бо matrix — це декартів добуток, і хвилини множаться швидко: «3 браузери × 3 ОС» — це вже 9 незалежних джобів, кожна зі своїм overhead. Ганяти всі комбінації означає платити за конфігурації, які або технічно неможливі, або не несуть реального ризику. Тому матрицю свідомо проріджують — виключають безглузді перетини (наприклад, WebKit під Windows) і лишають лише ті комбінації, де регресія справді ймовірна й важлива. Орієнтир простий: кожна клітинка матриці мусить відповідати на питання «яку саме несправність я тут ловлю»; якщо відповіді немає, клітинку прибирають. Це прямий приклад того, як покриття балансують проти ціни, а не нарощують наосліп.
Розкажіть механічно, як улаштований шардинг від розрізання до єдиного вердикту.
Це патерн fan-out / fan-in. На fan-out сьют розрізають на M частин, і кожен шард стартує на своєму агенті паралельно; в ідеалі wall-clock падає приблизно як загальний_час / M. Кожен шард виконує свою порцію тестів незалежно й віддає частковий звіт — зазвичай у нейтральному форматі на кшталт JUnit XML, який розуміють усі системи. На fan-in окрема фінальна джоба збирає всі часткові звіти й зливає їх у один результат «пройшло / впало». Критичний нюанс: без кроку злиття ви отримаєте M розрізнених звітів і жодної цілісної картини — CI не знатиме, зелений прогін чи ні. Тому шардинг — це не лише «розрізати й запустити», а обовʼязково ще й «зібрати назад».
Розбили сьют на 8 шардів, а швидше стало лише вдвічі, не у 8 разів. Чому?
Лінійне прискорення — ідеалізація, а на практиці його зʼїдають три речі. Перша — overhead на кожен шард: checkout, встановлення залежностей, завантаження браузерів, підняття контейнера — це фіксована ціна, яку платить кожен агент незалежно від кількості тестів у ньому, і вона задає підлогу, нижче якої wall-clock не опускається. Друга — нерівне балансування: якщо один шард навантажений важче за інших, весь прогін чекає на нього (див. straggler), і восьмиразового виграшу не буде. Третя — ліміт конкурентності: якщо одночасно дозволено, скажімо, 4 джоби, вісім шардів підуть двома хвилями, і стелю визначить не число шардів, а конкурентність. Сильна відповідь називає всі три причини, а не одну, і згадує закон Амдала: фіксована частина роботи принципово обмежує прискорення.
Що таке straggler і як його уникнути?
Straggler (відстаючий) — це шард, який фінішує помітно пізніше за решту й тим самим тримає весь wall-clock прогону, бо загальний час дорівнює найповільнішому шарду. Поки straggler висить 12 хвилин, решта агентів, що впоралися за 3, простоюють — а ви платите за час, який визначив один невдало навантажений шард. Виникає він майже завжди від наївного балансування «за кількістю» тестів чи файлів, коли не враховано, що тести дуже різні за тривалістю. Лікують straggler балансуванням за історичним часом: розкладають тести так, щоб очікувана сумарна тривалість кожного шарда була приблизно однакова. Де інфраструктура дозволяє — ще надійніше динамічний розподіл: черга віддає наступний тест агенту, який щойно звільнився, тож straggler не виникає за побудовою, ціною окремого шару координації.
Чому балансувати шарди за кількістю тестів — погана ідея?
Бо кількість тестів не корелює з часом їх виконання. Один e2e-флоу з логіном і оплатою може їхати хвилину, а сотня дрібних перевірок — кілька секунд разом. Розбивши сьют «по рівну кількість тестів на шард», ви легко отримаєте шард, що фінішує за 2 хвилини, і сусідній, що висить 12, — а wall-clock визначить саме довший. Тому правильний орієнтир — не рахувати тести, а зважувати їх за минулими замірами часу й пакувати шарди так, щоб їхня очікувана тривалість збіглася. Метрика контролю тут — перекіс шардів, тобто відношення часу найповільнішого до найшвидшого: близько до одиниці означає добрий баланс. Пастка методу: новий важкий тест ще не має історії, отримує усереднену оцінку й може тимчасово перекосити розкладку, доки не набере статистики.
Чим кеш у CI відрізняється від артефакту?
Обидва перевикористовують роботу, але на різних осях часу. Артефакт передає результат між джобами одного й того самого прогону: наприклад, стадія збірки будує застосунок раз і публікує його, а тестові джоби того ж прогону забирають готове. Кеш зберігає результат між різними прогонами пайплайна: node_modules, завантажені браузери, шар-кеш Docker — щоб наступний прогін не робив ту саму роботу знову. Простими словами: артефакт живе в межах прогону й гине з ним, кеш переживає прогони. Плутати їх на співбесіді — типова помилка: якщо кандидат каже «закешуємо білд, щоб передати в тестову джобу», він насправді описує артефакт, а не кеш.
Що кешують найчастіше і чому ключ кешу залежностей мусить містити хеш lock-файлу?
Три речі: залежності (node_modules чи кеш пакетного менеджера), браузерні бінарники (Playwright щоразу качає сотні мегабайтів — класичний AQA-специфічний виграш) і білд застосунку. Ключ кешу залежностей мусить залежати від хеша lock-файлу (package-lock.json), бо саме lock-файл однозначно описує, який набір версій має лежати в кеші. Якщо ключ містить хеш і lock-файл не змінився — кеш попадає, і ви пропускаєте npm install, часто найбільшого одиничного пожирача часу. Якщо ж ключ статичний і не залежить від lock-файлу — кеш ніколи не інвалідовується: ви оновили залежність, а пайплайн тягне стару з кешу й ловить загадкові фейли на, здавалося б, свіжому коді. Тому золоте правило: ключуй за вмістом, а не за рядком, що не змінюється.
Кеш працює, а тести зелені на застарілому коді. Що сталося?
Найімовірніше, ключ кешу не привʼязаний до вмісту того, що кешується, тож кеш не інвалідовується при зміні. Класичний випадок — кеш залежностей із ключем, що не містить хеша lock-файлу: ви підняли версію бібліотеки, а пайплайн раз за разом дістає з кешу старий node_modules і фактично тестує код зі старими залежностями. Зовні це виглядає як баг тесту або магія, а насправді це той самий клас проблеми, що застарілий HTTP-кеш у браузері: кеш вартий рівно стільки, скільки варта його інвалідація. Лікування — зробити ключ похідним від вмісту (хеш lock-файлу, версія браузера, хеш білд-конфігурації), щоб будь-яка змістовна зміна автоматично давала новий ключ і промах кешу. Небезпека тут підступна саме тим, що пайплайн лишається зеленим — тобто мовчки бреше.
Що ганяти на кожен PR, а що вночі, і чому саме так?
На кожен PR ганяють smoke — швидкий набір критичного шляху, що блокує merge і вкладається в кілька хвилин; його задача — дати розробнику вердикт, поки він ще в контексті. Повну регресію (десятки, а то й дев’яносто хвилин) виносять на нічний запуск за розкладом (cron), поза критичним шляхом. Логіка в тому, що фідбек на PR мусить вимірюватися хвилинами: вішати важку регресію на кожен push означає вбити зворотний звʼязок, а раз на ніч її цілком можна прогнати й зловити те, що smoke пропустив. Тобто це свідомий обмін повноти на швидкість там, де швидкість важливіша. Головне — не переплутати: smoke не заміняє регресію, він лише тримає критичний шлях коротким, а регресія лишається страхувальною сіткою.
Що таке вибір тестів за змінами (affected) і в чому його ризик?
Це прийом, коли на PR запускають не весь сьют, а лише тести, повʼязані зі зміненим кодом чи областю застосунку — потенційно величезна економія wall-clock. Ризик у тому, що він тримається на мапінгу «код → тести», а цей мапінг майже завжди неповний: якщо зміна зачепила щось поза відомими звʼязками, ви пропустите саме той тест, який зловив би регресію. Тому affected-вибір застосовують як прискорювач на PR, а не як заміну повного прогону: повну регресію лишають уночі страхувальною сіткою, щоб пропущене на PR усе одно спливло до релізу. Родич цього прийому — fail-fast (скасувати решту джобів після першого падіння, аби не палити хвилини), але й у нього є ціна: ви втрачаєте повну картину, а на флакі-сьютах він буває шумним.
Що таке ліміт конкурентності і чому 20 шардів можуть не дати нічого?
Ліміт конкурентності — це стеля, скільки джобів вашого плану чи організації дозволено виконувати одночасно. Якщо стеля 5, а ви оголосили 20 шардів, вони підуть чотирма послідовними хвилями по 5 — і wall-clock визначить не число шардів, а саме ця стеля. Тобто шардинг понад ліміт конкурентності не купує нічого, крім зайвого overhead на кожну додаткову одиницю. Це одна з трьох типових причин, чому «додав шардів, а не прискорилось». Сюди ж додається час у черзі: на завантаженому спільному пулі раннерів джоба може хвилинами просто чекати старту, і ця затримка теж зʼїдає фідбек, хоч її й не видно у власне тривалості тестів. Тому перш ніж нарощувати шарди, зʼясовують, яка реальна стеля паралельності.
Коли брати self-hosted раннери, а коли hosted, і який тут безпековий нюанс?
Hosted-раннери зручні й тарифікуються похвилинно — добрий вибір, поки навантаження помірне. Self-hosted (агенти на власному залізі) дають фіксовану вартість замість похвилинної, контроль над CPU/RAM (критично для важких браузерних тестів) і за масштабу часто виходять швидшими й дешевшими — ціною того, що ви самі їх підтримуєте, патчите й захищаєте. Тобто це тредоф ціни, контролю над залізом і вартості обслуговування, а не «одне завжди краще». Безпековий нюанс жорсткий: self-hosted раннер, який збирає недовірені PR із форків, — це пряма діра, бо чужий код виконується у вашому середовищі й може дотягнутися до секретів. Тому self-hosted або не пускають до форкових PR узагалі, або ізолюють ефемерними одноразовими агентами без доступу до чутливих даних.
Ваш пайплайн їде 40 хвилин. Ваші дії?
Спершу поміряти, а не діяти наосліп — профіль джобів показує, де реально час: встановлення залежностей, завантаження браузерів, найдовший шард, час у черзі. Далі йти по важелях у сталому порядку: кеш (майже безкоштовний wall-clock — залежності за хешем lock-файлу, браузери за версією, білд як артефакт), потім вибірковість (smoke на PR, регресія вночі, можливо affected-вибір), і лише тоді паралельність — шардинг до стелі конкурентності з балансуванням за історичним часом. Паралелізм кидають в останню чергу і лише в ізольований сьют, інакше він не прискорить, а помножить флак. Червоний прапорець на співбесіді — одразу відповісти «додам шардів»: це лікування симптому без діагнозу. Правильна рамка — оптимізувати критичний шлях, а не сумарні хвилини, і завжди спочатку виміряти.
Тести падають лише коли шардів багато, а поодинці все зелене. Де копати?
Це майже завжди не інфраструктура, а порушена ізоляція тестів. Коли шардів мало, тести природно не перетинаються в часі; коли їх багато, два шарди починають одночасно чіпати спільний ресурс — одного тестового юзера, той самий запис у БД, глобальний стан — і паралельність оголює приховану залежність, яка раніше маскувалася послідовністю. Симптом упізнаваний: що більше паралелізм, то більше плавучих падінь, а зменшиш конкурентність — і «магічно» зеленіє. Тому копати треба не в раннери, а в дані й стан: чи унікальні облікові записи й фікстури на воркер, чи немає покладання на порядок, чи прибирає кожен тест за собою. Шардинг тут не винуватець, а лише діагност: він показав, що передумова ізоляції не виконана.
Три кейси з життя AQA, де швидкість пайплайна вирішується не «додаванням заліза», а діагнозом: таблиця рішень для повільного пайплайна, шардинг Playwright у CI за схемою fan-out / fan-in зі злиттям звітів і розбір ключа кешу, що мовчки бреше. Скрізь — що дивитися і чому.
Кейс 1. Пайплайн їде 40 хвилин: від симптому до важеля
Тімлід просить «прискорити CI». Спокуса — одразу накинути шардів, але це лікування без діагнозу. Спершу профілюємо джоби (більшість CI показує тривалість кожного кроку) і зіставляємо симптом із важелем. Груба таблиця, з якої починається розмова:
| Симптом у профілі | Ймовірний діагноз | Важіль |
|---|---|---|
npm install щоразу 4–6 хв | кеш залежностей не попадає або відсутній | ключ кешу за хешем package-lock.json |
| Кожен агент 3 хв качає браузери | немає кешу браузерів / базового образу | кеш за версією інструмента або Docker-образ із вшитими браузерами |
| Один шард 12 хв, решта по 3 | нерівне балансування, straggler | балансувати за історичним часом, не за кількістю |
| 20 шардів, а wall-clock як у 5 | впертість у ліміт конкурентності | не плодити шарди понад стелю; вимірювати чергу |
| Регресія 90 хв висить на кожен PR | немає розкладки за тригером | smoke на PR, повна регресія nightly |
| Джоба хвилинами не стартує | час у черзі на спільному пулі | окремий пул / self-hosted під навантаження |
Що дивитися і чому:
- Порядок важелів не випадковий. Спершу кеш (майже безкоштовний wall-clock), потім вибірковість (не запускати зайве), і лише тоді паралельність. Накинути шардів першими — значить помножити ще й overhead, і часто виграш зʼїдається setup-ом.
- Найбільший одиничний пожирач часто не тести, а setup. Встановлення залежностей і завантаження браузерів легко зʼїдають половину прогону; поки вони не закешовані, шардинг ділить не так тести, як цей самий setup, помножений на кожен шард.
- wall-clock визначає найдовша гілка, а не сума. Тому оптимізують критичний шлях: один straggler або одна незакешована стадія тримають увесь прогін, скільки б решта не була швидкою.
Кейс 2. Шардинг Playwright у CI: fan-out і обовʼязковий fan-in
Сьют їде 24 хвилини одним прогоном. Ріжемо на 4 шарди — очікуємо приблизно 24 / 4 мінус overhead. Ключове, що часто забувають: без кроку злиття вийде 4 розрізнені звіти й жодного цілісного «пройшло / впало». Тому схема — саме fan-out (чотири паралельні шарди) плюс fan-in (окрема джоба зливає часткові звіти).
# .github/workflows/e2e.yml (спрощено)
jobs:
test:
strategy:
fail-fast: false # інакше падіння одного шарда сховає решту
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm' # кеш залежностей за хешем package-lock.json
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npx playwright test --shard=${{ matrix.shard }}/4
- uses: actions/upload-artifact@v4
with:
name: blob-${{ matrix.shard }}
path: blob-report # частковий звіт цього шарда
merge:
needs: test # fan-in: чекає на всі 4 шарди
if: always() # зливаємо навіть якщо якийсь шард червоний
steps:
- uses: actions/download-artifact@v4
with:
pattern: blob-*
path: all-blobs
- run: npx playwright merge-reports --reporter=html ./all-blobs
Що дивитися і чому:
fail-fast: falseу матриці шардів — свідомий вибір. Зfail-fast: trueперше падіння скасує решту шардів, і ви не побачите повної картини — не той компроміс, коли мета прогону зібрати всі падіння, а не лише перше.- Крок злиття не опційний.
merge-reportsзбирає часткові звіти (Playwright віддає їх як blob) у єдиний вердикт;if: always()гарантує злиття навіть із червоним шардом, інакше отримаєте розсипані артефакти без цілісного результату. - 4 шарди
× 3хв на завантаження браузерів — це 12 хв overhead, розмазаних по агентах. Саме тому браузери й залежності кешують до того, як нарощувати шарди: інакше setup, помножений на кількість шардів, зʼїдає виграш від розрізання. - Наївний
--shard=i/4ділить за кількістю тестів, не за часом. Якщо серед них є важкий e2e-флоу, він може зробити свій шард straggler-ом; для рівного балансу орієнтуються на історичний час прогону кожного тесту.
Кейс 3. Ключ кешу, що ніколи не інвалідовується
Найпідступніший баг CI — зелений пайплайн на застарілому коді. Джерело — кеш залежностей із ключем, не привʼязаним до вмісту. Порівняймо дві конфігурації.
# ПОГАНО: ключ статичний, від lock-файлу не залежить
- uses: actions/cache@v4
with:
path: node_modules
key: deps-cache # ← ніколи не змінюється
# ДОБРЕ: ключ містить хеш package-lock.json
- uses: actions/cache@v4
with:
path: node_modules
key: deps-${{ hashFiles('package-lock.json') }}
restore-keys: deps- # запасний частковий збіг
З першим варіантом сценарій такий: розробник підняв версію бібліотеки, package-lock.json змінився, а ключ кешу — ні. Пайплайн раз за разом дістає старий node_modules, npm ci фактично працює вхолосту, і сьют зеленіє на коді, який ще ніхто не тестував зі свіжими залежностями. Швидко перевірити підозру можна діагностичним кроком:
- run: |
npm ls some-lib --depth=0 # яка версія реально стоїть
cat package-lock.json | grep -A2 '"some-lib"' # яку очікуємо
Що дивитися і чому:
- Кеш вартий рівно стільки, скільки варта його інвалідація. Ключ мусить бути похідним від вмісту — хеш
package-lock.jsonдля залежностей, версія інструмента для браузерів, хеш конфігурації для білда. Статичний рядок перетворює кеш на джерело мовчазної брехні. - Симптом підступний саме тим, що пайплайн зелений. Це не червоний фейл, який видно одразу, а той самий клас проблеми, що застарілий HTTP-кеш у браузері: система впевнено віддає старе за нове, і ловиться це лише прицільною перевіркою версій.
restore-keys— не костиль, а страховка. Частковий збіг за префіксом дає теплий старт (не з нуля), але точний ключ із хешем усе одно змусить перезбудувати кеш при зміні lock-файлу — тобто швидко і без застаряння водночас.
Метрика і важелі
- Знаю різницю між wall-clock (час за годинником від
pushдо вердикту) і сумарними CPU-хвилинами, і чому оптимізують саме перше. - Памʼятаю орієнтир «десятихвилинного білда» і логіку за ним: понад ~10 хвилин фідбеку — і увага розробника вже деінде.
- Розумію два важелі (робити менше: кеш + вибірковість; робити паралельно: джоби, matrix, шардинг) і чому кеш перший — він майже безкоштовний, тоді як паралелізм піднімає оплачувані хвилини.
Паралельні джоби і matrix
- Можу пояснити, що matrix — це декартів добуток параметрів (браузер × ОС × версія), тобто «3 × 3» = 9 незалежних джобів.
- Розумію, що matrix — про покриття різних середовищ, а не про швидкість, і чому матрицю проріджують (виключають безглузді комбінації).
- Не плутаю наміри: matrix = «чи працює той самий тест у різних середовищах», шардинг = «як прогнати ту саму конфігурацію швидше».
Шардинг і балансування
- Можу пояснити шардинг як поділ одного сьюта на M частин на M агентах з ідеалом
час / Mмінус overhead. - Знаю, що overhead (checkout, install, завантаження браузерів, підняття контейнера) — фіксована підлога на кожен шард, звідки нелінійність виграшу (ефект закону Амдала).
- Памʼятаю жорсткішу передумову: тести мають бути незалежними — без порядку, спільних даних чи облікових записів.
- Розумію шардинг як fan-out / fan-in: розрізати → часткові звіти (JUnit XML) → окрема джоба зливає в єдиний вердикт; без злиття — M розрізнених звітів.
- Можу пояснити straggler (wall-clock = найповільніший шард) і як його прибирають: балансувати за історичним часом, а не за кількістю; динамічний розподіл прибирає straggler за побудовою.
Кешування
- Знаю різницю кеш vs артефакт: артефакт передає результат між джобами одного прогону, кеш зберігає між різними прогонами.
- Памʼятаю три головні цілі кешу: залежності (
node_modules), браузерні бінарники (за версією інструмента), білд застосунку. - Розумію золоте правило: ключ кешу мусить бути похідним від вмісту (хеш lock-файлу, версія браузера), інакше кеш ніколи не інвалідовується — і пайплайн зеленіє на застарілому коді.
Вибірковість
- Знаю розкладку за тригером: smoke критичного шляху на кожен PR (блокує merge, кілька хвилин), повна регресія nightly за cron.
- Можу пояснити вибір за змінами (affected) і його ризик: неповний мапінг «код → тести» пропустить регресію, тож це прискорювач на PR, а не заміна нічної регресії.
- Знаю, що таке fail-fast і його ціну: економить хвилини, але втрачає повну картину й шумить на флакі-сьютах.
Ціна і раннери
- Можу пояснити ліміт конкурентності: 20 шардів зі стелею 5 підуть хвилями, і wall-clock визначить стеля, а не число шардів; час у черзі — теж частина затримки фідбеку.
- Знаю тредоф self-hosted vs hosted (фіксована ціна й контроль над CPU/RAM проти вартості підтримки) і безпековий ризик: self-hosted на форкових PR — діра до секретів.
- Памʼятаю сталий порядок дій: поміряти → кеш → вибірковість → паралельність до стелі конкурентності; шарди в неізольований сьют лише множать флак.
Яку метрику насамперед оптимізують, коли прискорюють пайплайн?
Питання
Wall-clock vs сумарні CPU-хвилини — у чому різниця й що оптимізують?