Modern QA2026Оптимизация пайплайна
Join

Course16 CI/CD Pipelines

Foundations · Chapter 16

Оптимизация пайплайна

Updated Jul 2026

Почему скорость важна

Медленные пайплайны подрывают доверие разработчиков. Когда пайплайн занимает 45 минут, разработчики перестают его ждать и сливают код в любом случае. Когда пайплайн занимает 5 минут, разработчики воспринимают его как надёжную страховочную сеть и действительно смотрят результаты перед слиянием.

Цель: менее 5 минут для цикла обратной связи по PR (линтинг + unit-тесты) и менее 20 минут для полного пайплайна (включая браузерные тесты).

Кэширование

Кэширование — самая эффективная единичная оптимизация. Без кэширования каждый запуск пайплайна скачивает и устанавливает зависимости с нуля — часто тратя 2-5 минут, не добавляющих никакой ценности.

Что кэшировать

Что Ключ кэша Типичная экономия
Node modules хэш package-lock.json 1-3 минуты
Python virtualenvs хэш requirements.txt или poetry.lock 1-2 минуты
Браузеры Playwright хэш package-lock.json 2-4 минуты
Слои Docker хэш Dockerfile 2-10 минут
Зависимости Gradle/Maven хэш build.gradle или pom.xml 1-5 минут
Go-модули хэш go.sum 30с-2 минуты

Кэширование в GitHub Actions

# Cache node_modules (automatic with setup-node)
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'npm'

# Cache Playwright browsers (manual)
- uses: actions/cache@v4
  id: playwright-cache
  with:
    path: ~/.cache/ms-playwright
    key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}

- run: npx playwright install --with-deps
  if: steps.playwright-cache.outputs.cache-hit != 'true'

Выход cache-hit позволяет полностью пропустить установку, когда кэш валиден. Это превращает 3-минутную установку браузеров Playwright в 5-секундное восстановление кэша.

Стратегия инвалидации кэша

Ключи кэша должны меняться при изменении зависимостей и оставаться стабильными в остальных случаях:

# Good: Changes only when lockfile changes
key: deps-${{ hashFiles('package-lock.json') }}

# Bad: Changes on every commit (cache is never used)
key: deps-${{ github.sha }}

# Better: Fallback to partial cache match
key: deps-${{ hashFiles('package-lock.json') }}
restore-keys: |
  deps-

Запасной вариант restore-keys находит самый свежий кэш, начинающийся с deps-, который может быть немного устаревшим, но значительно быстрее, чем установка с нуля.

Параллелизация

Шардирование тестов

Разделите ваш набор тестов между несколькими раннерами. Каждый раннер выполняет часть тестов, и общее время выполнения примерно равно общее_время / количество_шардов.

# Playwright sharding
strategy:
  matrix:
    shard: [1, 2, 3, 4]
steps:
  - run: npx playwright test --shard=${{ matrix.shard }}/4

# Jest sharding
strategy:
  matrix:
    shard: [1, 2, 3]
steps:
  - run: npx jest --shard=${{ matrix.shard }}/3

# pytest-xdist (automatic parallelization within a single runner)
steps:
  - run: pytest -n auto  # Uses all available CPU cores

Выбор правильного количества шардов:

  • Начните с 3-4 шардов и измерьте результат
  • Каждый шард должен занимать примерно одинаковое время (сбалансированное распределение)
  • Слишком много шардов — и накладные расходы на настройку/завершение начинают доминировать
  • Слишком мало — и каждый шард по-прежнему медленный

Матричная стратегия для сквозных задач

Комбинируйте шардирование с другими измерениями:

strategy:
  fail-fast: false
  matrix:
    browser: [chromium, firefox]
    shard: [1, 2, 3]
# Creates 6 parallel jobs: chromium-1, chromium-2, chromium-3, firefox-1, firefox-2, firefox-3

Параллельные задачи vs параллельные тесты

  • Параллельные задачи (матричная стратегия): каждая задача выполняется на отдельном раннере. Хорошо для изоляции и кросс-браузерного тестирования.
  • Параллельные тесты (внутри задачи): используйте многоядерные раннеры и инструменты вроде pytest-xdist или воркеров Jest. Хорошо для CPU-интенсивных unit-тестов.

Для максимальной скорости комбинируйте оба подхода: запустите 4 шарда на 4 раннерах, и внутри каждого шарда запускайте тесты на 2 ядрах CPU.

Стратегия быстрого отказа

Настройка fail-fast контролирует, отменяются ли другие задачи матрицы при сбое одной.

strategy:
  fail-fast: true   # Cancel all jobs if one fails
  fail-fast: false  # Let all jobs complete regardless

Когда использовать fail-fast: true:

  • Unit-тесты: если один шард падает, остальные, вероятно, тоже сломаны. Отмените их для экономии времени.
  • Линтинг/проверка типов: если линтинг не прошёл, нет смысла запускать тесты.

Когда использовать fail-fast: false:

  • Браузерные тесты: вы хотите знать полный масштаб сбоев по всем браузерам и шардам. Тест, падающий в Firefox, но проходящий в Chrome — это другой баг, нежели падающий везде.
  • Интеграционные тесты с внешними сервисами: сбои могут быть изолированы в конкретных тест-кейсах.

Условное выполнение

Пропускайте дорогую работу, когда она не нужна.

Фильтрация по путям

# Skip tests when only documentation changed
on:
  push:
    paths-ignore:
      - '**.md'
      - 'docs/**'
      - '.github/ISSUE_TEMPLATE/**'
      - 'LICENSE'

# Only run backend tests when backend code changed
on:
  push:
    paths:
      - 'src/api/**'
      - 'src/services/**'
      - 'tests/integration/**'

Условия на уровне задачи

# Only run browser tests on PRs targeting main
browser-tests:
  if: github.event_name == 'pull_request' && github.base_ref == 'main'

# Skip expensive tests for draft PRs
e2e-tests:
  if: github.event.pull_request.draft == false

Условия на уровне шага

# Only upload artifacts on failure
- uses: actions/upload-artifact@v4
  if: failure()

# Only run deployment on main branch
- run: npm run deploy
  if: github.ref == 'refs/heads/main'

Конфигурация пайплайна для производительности тестов

Переменные окружения, влияющие на скорость тестов:

env:
  TEST_TIMEOUT: 30000      # 30s timeout per test (prevent hanging tests)
  RETRY_COUNT: 2           # Retry failed tests twice (handle infrastructure flakes)
  HEADLESS: true           # Run browsers in headless mode (faster)
  SLOW_MO: 0               # No artificial delay between actions
  WORKERS: 4               # Number of parallel test workers

Логика повторных попыток

Повторные попытки должны справляться с инфраструктурными сбоями (таймауты сети, задержки запуска контейнеров), а не с багами тестов. Если тест постоянно требует повторных попыток для прохождения, он нестабильный и его нужно чинить.

# Playwright retry configuration
- run: npx playwright test --retries=2

# Job-level retry (GitHub Actions does not support this natively;
# use reusable workflows or third-party actions)

Настройки таймаутов

Всегда устанавливайте таймауты, чтобы зависшие задачи не занимали раннеры:

jobs:
  unit-tests:
    runs-on: ubuntu-latest
    timeout-minutes: 10     # Kill the job if it takes longer than 10 minutes

  browser-tests:
    runs-on: ubuntu-latest
    timeout-minutes: 30     # Browser tests get more time

Измерение производительности пайплайна

Отслеживайте эти метрики для выявления возможностей оптимизации:

Метрика Как измерять Цель
Время обратной связи PR Время от push до результатов unit-тестов < 5 минут
Время полного пайплайна Время от push до завершения всех проверок < 20 минут
Процент попаданий в кэш Проверяйте логи cache action > 90%
Процент нестабильных тестов Тесты, проходящие при повторной попытке < 2%
Время ожидания в очереди Время ожидания задач свободного раннера < 1 минуты

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

Продвинутые техники оптимизации

Переиспользуемые workflow

Выделяйте общие паттерны в переиспользуемые workflow, чтобы избежать дублирования:

# .github/workflows/reusable-test.yml
on:
  workflow_call:
    inputs:
      test-command:
        required: true
        type: string
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: ${{ inputs.test-command }}

Оптимизация графа зависимостей

Если ваш репозиторий содержит несколько пакетов (монорепо), тестируйте только пакеты, затронутые изменением:

# Use a tool like Nx or Turborepo to detect affected packages
- run: npx nx affected --target=test --base=origin/main --head=HEAD

Практическое упражнение

  1. Измерьте текущее время пайплайна от начала до конца. Запишите длительность каждого этапа.
  2. Добавьте кэширование зависимостей и браузеров Playwright. Измерьте улучшение.
  3. Добавьте шардирование тестов с 3-4 шардами. Измерьте улучшение.
  4. Добавьте фильтрацию по путям для пропуска тестов при изменении только документации.
  5. Установите соответствующие настройки fail-fast для каждого типа задачи.
  6. Установите таймауты на все задачи для предотвращения зависших пайплайнов.
  7. Сравните метрики до и после. Цель: 50% сокращение общего времени пайплайна.