Паралелізація і швидкість пайплайна
Зміст
Уся цінність 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) — ключ до розуміння меж. Кожен шард платить фіксовану ціну незалежно від кількості тестів у ньому: репозиторію, встановлення залежностей, завантаження браузерів, підняття . Ці секунди-хвилини — підлога, нижче якої шардінг не опускається. Тому виграш нелінійний: перші шарди дають багато, а після певної межі кожен наступний майже не зменшує wall-clock, зате додає ще одну порцію overhead. Це той самий ефект, що й у : фіксована частина роботи обмежує прискорення.
Друга — жорсткіша — умова: тести мають бути незалежними. Жодного покладання на порядок, жодних спільних мутабельних даних чи облікових записів. Якщо два шарди водночас логіняться під одного тестового юзера, паралельність не прискорить, а породить чергу й гонки. Ця передумова (ізоляція на тест і на ) — канон розділу про стратегію автоматизації; шардінг лише оголює її відсутність.
Механічно шардінг — це патерн / fan-in: сюїту розрізають, шарди біжать паралельно, кожен віддає частковий звіт (формат обміну з CI — JUnit XML, див. Автотести в CI: стабільність і дебаг), а окрема фінальна джоба зливає їх у єдиний вердикт. Без кроку злиття ви отримаєте M розрізнених звітів і жодного цілісного «пройшло / впало».
Балансування шардів
Наївний поділ — за кількістю файлів чи тестів порівну. Проблема в тому, що тести дуже різні за тривалістю: один e2e-флоу з логіном і оплатою може їхати хвилину, а сотня швидких перевірок — кілька секунд. Розбивши «по рівну кількість», ви легко отримаєте шард, що фінішує за 2 хвилини, і сусідній, що висить 12. А wall-clock всього прогону дорівнює найповільнішому шарду — його називають straggler (відстаючий). Решта агентів простоюють, а ви платите за час, який визначив один невдало навантажений шард.
Правильний орієнтир — балансувати за історичним часом, а не за кількістю: розкласти тести так, щоб очікувана сумарна тривалість кожного шарда була приблизно однакова (жадібне «пакування» за минулими замірами). Де інфраструктура це підтримує — ще краще працює динамічний розподіл: черга видає наступний тест тому агенту, який щойно звільнився, тож straggler не виникає за побудовою — ціною окремого шару координації.
Метрика, за якою стежать, — перекіс шардів (відношення часу найповільнішого до найшвидшого): близько до одиниці означає добрий баланс. Пастка балансування за історією: новий важкий тест ще не має замірів, отримує усереднену оцінку й може перекосити розкладку, доки не набере статистики.
Кешування: не робити ту саму роботу двічі
Кеш (cache) у CI — це роботи між прогонами пайплайна. Його треба відрізняти від артефакту: артефакт передає результат між джобами одного прогону, кеш зберігає результат між різними прогонами. Це головний канон кешування у розділі — інші глави посилаються сюди.
Три речі, які кешують найчастіше:
- Залежності.
node_modulesабо кеш пакетного менеджера. Ключ кешу мусить містити хеш (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). Конкурентність (concurrency) тут — це скільки одиниць роботи система дозволяє тримати в польоті водночас; не плутай її з паралельністю, тобто фактичним одночасним виконанням на різних ранерах. План або організація визначають, скільки джобів біжить одночасно. Можна оголосити 20 шардів, але зі стелею у 5 паралельних джобів вони підуть чотирма хвилями — і wall-clock визначить стеля, а не число шардів. Шардінг понад ваш ліміт конкурентності не дає нічого. Сюди ж — час у черзі: на завантаженому спільному пулі ранерів джоба може хвилинами чекати старту, і це теж частина фідбеку, хоч її й не видно у власне тривалості тестів.
Self-hosted ранери — це агенти на власному залізі. Замість похвилинної оплати — фіксована вартість; контроль над CPU/RAM (важливо для важких браузерних тестів, див. Docker); за масштабу — часто швидше й дешевше. Ціна цього — ви їх самі підтримуєте, патчите й захищаєте. Окремий ризик: self-hosted ранер, що збирає недовірені PR із форків, — це пряма діра в безпеці (доступ до секретів, виконання чужого коду). Це канон глави про секрети та конфігурацію в CI.
Практичний висновок: спершу міряй, де час, а тоді оптимізуй критичний шлях, а не сумарні хвилини наосліп. Порядок дій майже завжди той самий — кеш (майже безкоштовний wall-clock) → вибірковість → паралельність до стелі конкурентності. Кидати шарди в сюїту, яка не ізольована, — значить лише помножити .
Загальний контекст CI/CD у силабусі ISTQB CTFL 4.0 подано в темі про тестування у DevOps і місце автоматизованих тестів у пайплайні; сама економіка паралелізації окремою темою силабусу не є — основи CI/CD див. у гл.9.
Типові помилки
- Додали 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 — «The whole point of Continuous Integration is to provide rapid feedback»; число атрибутовано не автору, а XP: «the XP guideline of a ten minute build is perfectly within reason», і вузьке місце назване прямо — «the usual bottleneck is testing».
- GitHub Docs — Understanding GitHub Actions — джоби за замовчуванням залежностей не мають і йдуть паралельно; матриця запускає ту саму джобу багато разів із різними комбінаціями змінних (ОС, версії) — тобто вона про покриття конфігурацій, а не про швидкість.
Шардінг: розрізати сюїту між агентами
- Playwright — Sharding — воркери масштабують у межах однієї машини, шардінг — між машинами, і шард у CI дорівнює окремій джобі; кожен шард дає власний звіт, а єдиний отримують злиттям
blob-звітів, які містять і , і діфи . - GitHub Docs — Understanding GitHub Actions — звідки береться накладна ціна кожного шарда: кожен прогін іде на свіжій віртуалці, і один ранер виконує одну джобу за раз.
- Playwright — Sharding — межа наївного поділу задана самим механізмом: шардяться лише тести, які можна виконувати паралельно, і за замовчуванням одиницею поділу є файл, а не час виконання.
Кешування: не робити ту саму роботу двічі
- Playwright — Continuous Integration — кешувати бінарники браузерів
is not recommended: відновлення кешу займає стільки ж, скільки завантаження, а залежності ОС не кешуються; якщо кешувати — за хешем версії. - GitHub Docs — Understanding GitHub Actions — межа між кешем і артефактом: свіжа віртуалка на кожен прогін означає, що результат для наступної джоби передають саме артефактом.
Вибірковість: найшвидша джоба — та, яку ти не запустив
- Martin Fowler — Continuous Integration — перший крок оптимізації названий прямо: Deployment Pipeline як «multiple builds done in sequence», де commit build «has to be done quickly», а довші перевірки їдуть далі по пайплайну.
- GitHub Docs — Understanding GitHub Actions — інструментальна опора розкладки: крім подій, запускається за розкладом, вручну або через REST API.
Ціна: хвилини CI, ліміти конкурентності, self-hosted
- GitHub Docs — Understanding GitHub Actions — один ранер виконує одну джобу за раз; своя ОС чи специфічне залізо — це self-hosted у власному датацентрі чи хмарі.
- Playwright — Continuous Integration — self-hosted названий саме тією умовою, за якої паралельність у CI виправдана; для ширшої паралелізації дока радить шардінг між джобами.
- Secure use reference (GitHub Actions) — зворотний бік self-hosted: такі ранери
should almost neverвикористовуватися для публічних репозиторіїв, бо будь-хто може відкрити PR і скомпрометувати середовище.
Чому швидкість пайплайна взагалі важлива — хіба тести не пройдуть однаково за 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-in. На fan-out сюїту розрізають на M частин, і кожен шард стартує на своєму агенті паралельно; в ідеалі wall-clock падає приблизно як загальний_час / M. Кожен шард виконує свою порцію тестів незалежно й віддає частковий звіт — зазвичай у форматі обміну з CI, як-от JUnit XML. На fan-in окрема фінальна джоба збирає всі часткові звіти й зливає їх у один результат «пройшло / впало». Критичний нюанс: без кроку злиття ви отримаєте M розрізнених звітів і жодної цілісної картини — CI не знатиме, зелений прогін чи ні. Тому шардінг — це не лише «розрізати й запустити», а обовʼязково ще й «зібрати назад».
Розбили сюїту на 8 шардів, а швидше стало лише вдвічі, не у 8 разів. Чому?
Лінійне прискорення — ідеалізація, а на практиці його зʼїдають три речі. Перша — overhead на кожен шард: , встановлення залежностей, завантаження браузерів, підняття — це фіксована ціна, яку платить кожен агент незалежно від кількості тестів у ньому, і вона задає підлогу, нижче якої 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-специфічний виграш) і білд застосунку. Ключ кешу залежностей мусить залежати від хеша (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-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— не костиль, а страховка. Частковий збіг за префіксом дає теплий старт (не з нуля), але точний ключ із хешем усе одно змусить перезбудувати кеш при зміні — тобто швидко і без застаряння водночас.
Метрика і важелі
- Знаю різницю між wall-clock (час за годинником від
pushдо вердикту) і сумарними CPU-хвилинами, і чому оптимізують саме перше. - Памʼятаю орієнтир «десятихвилинного білда» і логіку за ним: понад ~10 хвилин фідбеку — і увага розробника вже деінде.
- Розумію два важелі (робити менше: кеш + вибірковість; робити паралельно: , matrix, ) і чому кеш перший — він майже безкоштовний, тоді як паралелізм піднімає оплачувані хвилини.
Паралельні джоби і matrix
- Можу пояснити, що matrix — це декартів добуток параметрів (браузер × ОС × версія), тобто «3 × 3» = 9 незалежних джобів.
- Розумію, що matrix — про різних середовищ, а не про швидкість, і чому матрицю проріджують (виключають безглузді комбінації).
- Не плутаю наміри: matrix = «чи працює той самий тест у різних середовищах», шардінг = «як прогнати ту саму конфігурацію швидше».
Шардінг і балансування
- Можу пояснити шардінг як поділ однієї на M частин на M агентах з ідеалом
час / Mмінус overhead. - Знаю, що overhead (, install, завантаження браузерів, підняття ) — фіксована підлога на кожен шард, звідки нелінійність виграшу (ефект ).
- Памʼятаю жорсткішу : тести мають бути незалежними — без порядку, спільних даних чи облікових записів.
- Розумію шардінг як / fan-in: розрізати → часткові звіти (JUnit XML) → окрема джоба зливає в єдиний вердикт; без злиття — M розрізнених звітів.
- Можу пояснити straggler (wall-clock = найповільніший шард) і як його прибирають: балансувати за історичним часом, а не за кількістю; динамічний розподіл прибирає straggler за побудовою.
Кешування
- Знаю різницю кеш vs артефакт: артефакт передає результат між джобами одного прогону, кеш зберігає між різними прогонами.
- Памʼятаю три головні цілі кешу: залежності (
node_modules), браузерні бінарники (за версією інструмента), застосунку. - Розумію золоте правило: ключ кешу мусить бути похідним від вмісту (хеш , версія браузера), інакше кеш ніколи не інвалідовується — і зеленіє на застарілому коді.
Вибірковість
- Знаю розкладку за тригером: smoke критичного шляху на кожен PR (блокує merge, кілька хвилин), повна nightly за cron.
- Можу пояснити вибір за змінами (affected) і його : неповний мапінг «код → тести» пропустить регресію, тож це прискорювач на PR, а не заміна нічної регресії.
- Знаю, що таке fail-fast і його ціну: економить хвилини, але втрачає повну картину й шумить на -сюїтах.
Ціна і ранери
- Можу пояснити ліміт конкурентності: 20 шардів зі стелею 5 підуть хвилями, і wall-clock визначить стеля, а не число шардів; час у черзі — теж частина фідбеку.
- Знаю тредоф self-hosted vs hosted (фіксована ціна й контроль над CPU/RAM проти вартості підтримки) і безпековий ризик: self-hosted на форкових PR — діра до секретів.
- Памʼятаю сталий порядок дій: поміряти → кеш → вибірковість → паралельність до стелі конкурентності; шарди в неізольовану сюїту лише множать .
Квіз
Перед стартом
- Питань: 16
- Поріг «зараховано»: ≥70% правильних відповідей.
- Результат впливає на прогрес; завалені питання підуть у чергу повторення.
- Квіз впливає на компліт теми: тема стає «пройдено», лише коли прочитано теорію І квіз складено на ≥70%.
Питання
Wall-clock vs сумарні CPU-хвилини — у чому різниця й що оптимізують?
