Неперервне навантажувальне тестування в CI/CD
При кожному деплої в продакшен ризик внести регресію продуктивності — високий. Один неоптимальний SQL-запит або забута N+1 проблема можуть збільшити час відповіді на 50%, і користувач піде до конкурентів. Ми інтегруємо автоматичні навантажувальні тести прямо у ваш CI/CD пайплайн, щоб ловити такі регресії до потрапляння в master. Результат — ви впевнені, що кожен коміт не ламає SLA по latency і throughput.
Налаштуємо smoke-тести з пороговими значеннями, які перетворюють пайплайн на gatekeeper: якщо p95 latency перевищив 500ms або відсоток помилок вище 1% — пайплайн падає, і розробник отримує сповіщення. Це не заміна повноцінному навантажувальному тестуванню, а швидкий запобіжник.
Проблеми, які вирішуємо
- Регресія продуктивності після деплою — навіть мікрозміна в коді може сповільнити критичний endpoint. Без автоматичних тестів ви дізнаєтеся про проблему тільки після скарг користувачів або алертів моніторингу. Ми налаштовуємо baseline-порівняння: кожен тест запускається проти стабільної гілки, і при відхиленні >20% пайплайн блокується.
- Неефективні запити та вузькі місця — наші скрипти емулюють типові користувацькі сценарії (перегляд списку, створення поста, пошук). Якщо запит почав виконуватися довше — ми бачимо це на графіках метрик.
- Відсутність культури performance-first — розробники часто не думають про продуктивність на етапі code review. Continuous Load Testing робить performance видимим: кожен PR супроводжується коментарем з результатами тестів (p95, error rate).
Інструменти та їх місце в CI
k6 — найкращий вибір для CI: JS-скрипти, вбудована статистика, threshold-based pass/fail, нативна інтеграція з GitHub Actions і GitLab CI. Детальніше в k6 documentation.
Artillery — YAML-конфігурація, зручний для опису сценаріїв без коду. Gatling — Scala/Java, детальні HTML-звіти, зручний для Java-команд.
Базовий k6 скрипт
// tests/performance/api-smoke.js import http from 'k6/http' import { check, sleep } from 'k6' import { Rate, Trend } from 'k6/metrics' // Кастомні метрики const errorRate = new Rate('errors') const postCreateDuration = new Trend('post_create_duration') export const options = { // Профіль навантаження для CI: швидко, не руйнівно stages: [ { duration: '30s', target: 10 }, // розігрів { duration: '1m', target: 10 }, // стійке навантаження { duration: '10s', target: 0 }, // охолодження ], // Пайплайн зламається якщо поріг не досягнуто thresholds: { http_req_duration: [ 'p(95)<500', // p95 < 500мс 'p(99)<1000', // p99 < 1000мс ], errors: ['rate<0.01'], // помилок < 1% http_req_failed: ['rate<0.01'], // HTTP помилок < 1% post_create_duration: ['p(95)<800'], } } const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000' const AUTH_TOKEN = __ENV.AUTH_TOKEN export function setup() { // Один раз: отримати токен або підготувати дані const res = http.post(`${BASE_URL}/api/auth/login`, JSON.stringify({ email: '[email protected]', password: 'testpassword' }), { headers: { 'Content-Type': 'application/json' } }) return { token: res.json('token') } } export default function(data) { const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${data.token || AUTH_TOKEN}` } // Сценарій 1: список постів (70% трафіку) const postsList = http.get(`${BASE_URL}/api/posts?limit=20`, { headers }) check(postsList, { 'posts list: status 200': (r) => r.status === 200, 'posts list: has items': (r) => r.json('data').length > 0 }) errorRate.add(postsList.status !== 200) sleep(Math.random() * 0.5) // випадкова пауза 0-500мс // Сценарій 2: створення поста (20% трафіку) if (Math.random() < 0.2) { const start = Date.now() const createPost = http.post(`${BASE_URL}/api/posts`, JSON.stringify({ title: `Test post ${Date.now()}`, content: 'Load test content' }), { headers }) postCreateDuration.add(Date.now() - start) check(createPost, { 'create post: status 201': (r) => r.status === 201, }) errorRate.add(createPost.status !== 201) } sleep(0.3) } Як налаштувати пороги продуктивності в k6?
Пороги (thresholds) — ключовий механізм для автоматичного fail пайплайну. В опціях скрипта ми задаємо допустимі межі: наприклад, http_req_duration: ['p(95)<500', 'p(99)<1000']. Це означає, що 95% запитів мають виконуватися швидше 500 мс, а 99% — швидше 1 секунди. Якщо поріг порушено — тест завершується помилкою, і CI зупиняє деплой.
Ми також використовуємо кастомні метрики для конкретних бізнес-операцій (наприклад, час створення поста) і виставляємо окремі пороги на них. Так ви точно знаєте, що критична функціональність не деградує.
GitHub Actions інтеграція
# .github/workflows/performance.yml name: Performance Tests on: push: branches: [main, staging] pull_request: branches: [main] jobs: performance: runs-on: ubuntu-latest services: postgres: image: postgres:15 env: POSTGRES_DB: testdb POSTGRES_PASSWORD: testpass ports: ['5432:5432'] options: >- --health-cmd pg_isready --health-interval 10s steps: - uses: actions/checkout@v4 - name: Start application run: | docker compose -f docker-compose.test.yml up -d api npx wait-on http://localhost:3000/health --timeout 60000 - name: Run k6 smoke test uses: grafana/[email protected] with: filename: tests/performance/api-smoke.js flags: --out json=results.json env: BASE_URL: http://localhost:3000 K6_PROMETHEUS_RW_SERVER_URL: ${{ secrets.PROMETHEUS_URL }} - name: Parse results if: always() run: | # Показати summary в PR коментарі jq -r '.metrics | { p95: .http_req_duration["p(95)"], p99: .http_req_duration["p(99)"], errors: .http_req_failed.rate }' results.json - name: Comment PR with results if: github.event_name == 'pull_request' uses: actions/github-script@v7 with: script: | const fs = require('fs') const results = JSON.parse(fs.readFileSync('results.json')) const p95 = results.metrics.http_req_duration['p(95)'].toFixed(0) const errorRate = (results.metrics.http_req_failed.rate * 100).toFixed(2) github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: `## Performance Test Results\n\n| Metric | Value | Threshold |\n|--------|-------|-----------|\n| p95 latency | ${p95}ms | <500ms |\n| Error rate | ${errorRate}% | <1% |` }) Baseline порівняння між деплоями
#!/bin/bash # scripts/compare-performance.sh CURRENT_BRANCH=$(git branch --show-current) BASELINE_BRANCH="main" # Тест поточного коду k6 run --out json=current.json tests/performance/api-smoke.js # Переключитися на baseline git stash git checkout $BASELINE_BRANCH docker compose up -d --build api sleep 10 k6 run --out json=baseline.json tests/performance/api-smoke.js # Порівняння node - <<'EOF' const current = require('./current.json') const baseline = require('./baseline.json') const metrics = ['http_req_duration'] for (const m of metrics) { const cp95 = current.metrics[m]['p(95)'] const bp95 = baseline.metrics[m]['p(95)'] const delta = ((cp95 - bp95) / bp95 * 100).toFixed(1) if (cp95 > bp95 * 1.2) { // регресія > 20% console.error(`REGRESSION: ${m} p95 degraded by ${delta}%`) process.exit(1) } console.log(`${m} p95: ${cp95}ms vs ${bp95}ms baseline (${delta}%)`) } EOF # Повернутися на поточну гілку git checkout $CURRENT_BRANCH git stash pop Artillery для опису сценаріїв
# tests/performance/user-journey.yml config: target: "{{ $processEnvironment.BASE_URL }}" phases: - duration: 60 arrivalRate: 5 rampTo: 20 name: "Ramp up" - duration: 120 arrivalRate: 20 name: "Sustained load" ensure: thresholds: - http.response_time.p95: 500 - http.request_rate: 15 scenarios: - name: "Browse and purchase" weight: 70 flow: - get: url: "/api/products" expect: - statusCode: 200 - post: url: "/api/cart" json: productId: "{{ $randomInt(1, 100) }}" quantity: 1 - name: "Search only" weight: 30 flow: - get: url: "/api/search?q={{ $randomString(5) }}" Порівняння інструментів навантажувального тестування
| Характеристика | k6 | Artillery | Gatling |
|---|---|---|---|
| Мова скриптів | JavaScript | YAML | Scala/Java |
| Нативна CI-інтеграція | Так (GitHub Actions, GitLab CI) | Так (через NPM) | Так (Maven/Gradle) |
| Вбудовані threshold | Так | Так (через ensure) | Так |
| Генерація звітів | JSON, Prometheus, HTML | JSON, HTML | HTML (детальні) |
| Продуктивність | Висока (Go) | Середня (Node.js) | Висока (JVM) |
| Ліцензія | Open Source (AGPL) | Open Source (MPL) | Open Source (ALv2) |
Чому варто використовувати k6 для CI?
k6 має вбудовану підтримку CI/CD: він працює як CLI-утиліта, не потребує графічного інтерфейсу, а результати можна виводити в JSON для подальшої обробки. На відміну від Gatling (вимагає Scala/Java та генерації HTML-звітів) або Artillery (YAML-конфіги, але менше метрик), k6 надає гнучкі метрики та просту інтеграцію з GitHub Actions через готові action. Для команд, які вже використовують JavaScript, поріг входу мінімальний.
Процес роботи
- Аналітика — визначаємо критичні ендпоінти та користувацькі сценарії.
- Проектування — розробляємо навантажувальні сценарії, налаштовуємо профілі навантаження (ramp-up, sustained).
- Реалізація — пишемо скрипти (k6, Artillery або Gatling), інтегруємо їх в CI.
- Тестування — запускаємо smoke-тести на staging, коригуємо thresholds.
- Деплой — включаємо тести в пайплайн, налаштовуємо автоматичні коментарі в PR.
Строки та що входить
Налаштування базового набору k6 smoke-тестів з порогами та інтеграцією в CI займає від 1 до 2 робочих днів. Якщо потрібне порівняння з baseline та кастомні метрики — розширюємо до 3-5 днів. До складу робіт входить:
- Скрипти навантажувальних тестів (2-3 сценарії)
- Конфігурація thresholds
- Інтеграція з GitHub Actions або GitLab CI (YAML pipeline)
- PR-коментар з результатами тестів
- Документація по запуску та підтримці
- Консультація команди щодо інтерпретації результатів
Ми гарантуємо, що тести не заважатимуть основному процесу розробки: вони запускаються паралельно і займають не більше 5 хвилин.
Чому обирають нас
- Більше 5 років досвіду в навантажувальному тестуванні та CI/CD
- 50+ успішних проектів — від стартапів до enterprise
- Використовуємо тільки перевірені інструменти (k6, Grafana, Prometheus)
- Надаємо гарантію на коректну роботу тестів протягом місяця після впровадження
Зв'яжіться з нами, щоб обговорити ваш проект: отримайте консультацію з інтеграції Continuous Load Testing у ваш пайплайн.







