vyvchy
    Теми розділу

    09 · Git і CI/CD

    Паралелізація і швидкість пайплайна

    Зміст

    Уся цінність 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 втрачає ефект.

    Важелів рівно два, і все в цій главі — один з них:

    1. Робити менше роботи — кешування й вибірковість.
    2. Робити роботу паралельно — джоби, 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 розрізнених звітів і жодного цілісного «пройшло / впало».

    Push або PR

    Білд один раз
    артефакт

    Шард 1/4

    Шард 2/4

    Шард 3/4

    Шард 4/4

    Злити часткові звіти
    JUnit XML

    Єдиний вердикт

    Push або PR

    Білд один раз
    артефакт

    Шард 1/4

    Шард 2/4

    Шард 3/4

    Шард 4/4

    Злити часткові звіти
    JUnit XML

    Єдиний вердикт

    Балансування шардів

    Наївний поділ — за кількістю файлів чи тестів порівну. Проблема в тому, що тести дуже різні за тривалістю: один 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 пропустив.

    PR / push

    cron уночі

    Тригер

    Критичний шлях:
    lint + smoke
    кілька хвилин, блокує merge

    Повна регресія
    десятки хвилин, поза критичним шляхом

    Швидкий фідбек розробнику

    Ловить те, що smoke пропустив

    PR / push

    cron уночі

    Тригер

    Критичний шлях:
    lint + smoke
    кілька хвилин, блокує merge

    Повна регресія
    десятки хвилин, поза критичним шляхом

    Швидкий фідбек розробнику

    Ловить те, що 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).

    Підсумок

    1. Метрика, що має значення, — wall-clock від push до вердикту, а не сумарні CPU-хвилини. Оптимізуй критичний шлях.
    2. Важелів два: робити менше (кеш + вибірковість) і робити паралельно (джоби, matrix, шардинг). Кеш — перший, бо майже безкоштовний.
    3. Шардинг ріже час майже лінійно лише до межі, яку ставлять overhead на шард і ліміт конкурентності; за нею додавати шарди марно.
    4. Паралельність вимагає ізольованих тестів і даних; без цього вона не прискорює, а оголює флак.
    5. Кеш вартий рівно стільки, скільки варта його інвалідація: ключуй за вмістом (хеш 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.