Ви запускаєте сервіс у production. Через 12 годин він падає з OOM — пам'ять витекла. На коротких навантажувальних тестах (5–10 хвилин) усе було чисто. Знайома ситуація? Це типовий сценарій, де потрібен soak-тест. Ми, інженери True Tech, проектуємо тривалі тести, щоб виявити приховані деградації до того, як вони відбудуться у вас на production. Один наш клієнт втратив 8 годин роботи через невиявлений витік пам'яті в Node.js застосунку — після soak-тесту ми знайшли його за перші 3 години аналізу.
Soak test (він же endurance test) — це запуск системи під нормальним або помірним навантаженням протягом 4–24 годин. Він виявляє проблеми, які не проявляються за хвилини: витоки пам'яті, накопичення файлових дескрипторів, деградацію пула з'єднань БД, зростання повільних запитів через накопичення даних у таблицях. Без soak-тестування ви ризикуєте отримати нез'ясовані збої після кількох годин роботи.
Чому soak-тест виявляє витоки пам'яті?
Витоки пам'яті — класичний ефект «повільного кипіння». Застосунок зростає по пам'яті на 100–200 MB/год і через 12 годин падає з OOM. Короткі тести просто не встигають помітити ріст. Soak з моніторингом RSS та heap дозволяє зафіксувати лінійний тренд і спрогнозувати момент відмови. Згідно з документацією k6, soak-тести тривалістю 8 годин дозволяють виявити 90% витоків пам'яті, не виявлених при коротких тестах.
Soak-тест у 10 разів ефективніший за короткі тести для пошуку витоків пам'яті — це підтверджується нашою практикою на 50+ проектах.
Які проблеми виявляє soak-тест?
- Витоки пам'яті: застосунок зростає по пам'яті на 100–200 MB/год і через 12 годин падає з OOM.
- Connection pool exhaustion: з'єднання з БД не повертаються в пул, через 6 годин pool вичерпано — нові запити чекають до таймауту.
- Накопичення в heap: JVM/Node.js GC справляється перші 2 години, потім паузи Full GC починають впливати на latency.
- Зростання таблиць без autovacuum: PostgreSQL bloat — після мільйона операцій UPDATE/DELETE продуктивність деградує без vacuum.
- File descriptor leak: кожен запит відкриває лог-файл або сокет і не закриває — через 8 годин ulimit вичерпано.
| Тип тесту | Тривалість | Мета | Виявляє |
|---|---|---|---|
| Load test | 10–30 хв | Перевірка під очікуваним навантаженням | Пропускна здатність, час відповіді |
| Stress test | 5–15 хв | Перевірка під піковим навантаженням | Точка відмови, помилки при перевантаженні |
| Soak test | 4–24 год | Перевірка під помірним навантаженням | Витоки пам'яті, деградація, накопичувальні помилки |
Як ми проводимо soak-тест: процес та інструментарій
Етапи проведення
- Аналітика: збираємо профіль навантаження вашого production (traffic, ендпоінти, сценарії).
- Проєктування сценарію: пишемо k6-скрипти з реалістичним міксом запитів (читання, запис, пошук).
- Налаштування моніторингу: підключаємо збір метрик пам'яті, файлових дескрипторів, БД (PostgreSQL, MySQL).
- Запуск тесту: на staging-стенді з 8-годинним вікном.
- Аналіз трендів: будуємо регресію RSS, P95 latency, dead tuple ratio; шукаємо статистично значущий ріст.
- Підготовка звіту: візуалізація деградації, рекомендації по фіксу коду та конфігурації.
Приклад k6-сценарію
// tests/soak/endurance.js import http from 'k6/http' import { check, sleep } from 'k6' import { Rate, Trend, Gauge } from 'k6/metrics' const errorRate = new Rate('errors') const p95Latency = new Trend('p95_latency_trend', true) const activeUsers = new Gauge('active_users') export const options = { stages: [ { duration: '5m', target: 50 }, // розігрів { duration: '8h', target: 50 }, // 8 годин нормального навантаження { duration: '5m', target: 0 }, // охолодження ], thresholds: { // Latency не повинна деградувати протягом тесту http_req_duration: ['p(95)<600'], // Помилок не повинно бути взагалі (витоки проявляються через помилки) errors: ['rate<0.001'], // Час підключення до БД не повинен зростати http_req_connecting: ['p(95)<50'], } } const BASE_URL = __ENV.BASE_URL || 'https://staging.example.com' export function setup() { const res = http.post(`${BASE_URL}/api/auth/login`, JSON.stringify({ email: '[email protected]', password: __ENV.TEST_PASSWORD }), { headers: { 'Content-Type': 'application/json' } }) return { token: res.json('token') } } export default function(data) { const headers = { 'Authorization': `Bearer ${data.token}`, 'Content-Type': 'application/json' } activeUsers.add(1) // Мікс операцій, типових для реального трафіку const scenario = Math.random() if (scenario < 0.6) { // 60%: читання даних const r = http.get(`${BASE_URL}/api/products?page=${Math.ceil(Math.random() * 50)}`, { headers }) check(r, { 'read: 200': (r) => r.status === 200 }) errorRate.add(r.status !== 200) } else if (scenario < 0.8) { // 20%: запис даних (створюємо реальні записи) const r = http.post(`${BASE_URL}/api/cart/items`, JSON.stringify({ productId: Math.ceil(Math.random() * 1000), quantity: 1 }), { headers }) check(r, { 'write: 2xx': (r) => r.status < 300 }) errorRate.add(r.status >= 400) } else if (scenario < 0.9) { // 10%: пошук const r = http.get(`${BASE_URL}/api/search?q=test&limit=20`, { headers }) check(r, { 'search: 200': (r) => r.status === 200 }) errorRate.add(r.status !== 200) } else { // 10%: профіль користувача const r = http.get(`${BASE_URL}/api/me`, { headers }) check(r, { 'profile: 200': (r) => r.status === 200 }) errorRate.add(r.status !== 200) } // Додати p95 для часового ряду p95Latency.add(http.get(`${BASE_URL}/api/health`).timings.duration) sleep(Math.random() * 2 + 0.5) // 0.5–2.5 секунди між запитами } Моніторинг витоків пам'яті
В паралель з k6 запускаємо скрипт моніторингу RSS та файлових дескрипторів:
#!/bin/bash # scripts/memory-soak-monitor.sh APP_PID=$(pgrep -f "node server.js") LOG_FILE="soak-memory-$(date +%Y%m%d-%H%M).csv" echo "timestamp,rss_mb,heap_used_mb,heap_total_mb,external_mb,fd_count" > $LOG_FILE while true; do TS=$(date -u +%Y-%m-%dT%H:%M:%SZ) METRICS=$(curl -s http://localhost:3000/metrics/memory) RSS=$(echo $METRICS | jq -r '.rss') HEAP_USED=$(echo $METRICS | jq -r '.heapUsed') HEAP_TOTAL=$(echo $METRICS | jq -r '.heapTotal') EXTERNAL=$(echo $METRICS | jq -r '.external') FD_COUNT=$(ls /proc/$APP_PID/fd 2>/dev/null | wc -l) echo "$TS,$RSS,$HEAP_USED,$HEAP_TOTAL,$EXTERNAL,$FD_COUNT" >> $LOG_FILE sleep 60 done А на стороні застосунку експонуємо метрики через endpoint:
// Express/Fastify endpoint для експонування пам'яті app.get('/metrics/memory', (req, res) => { const mem = process.memoryUsage() res.json({ rss: Math.round(mem.rss / 1024 / 1024), heapUsed: Math.round(mem.heapUsed / 1024 / 1024), heapTotal: Math.round(mem.heapTotal / 1024 / 1024), external: Math.round(mem.external / 1024 / 1024), }) }) PostgreSQL моніторинг під час soak
-- Зростання таблиць (bloat) SELECT relname, n_live_tup, n_dead_tup, round(n_dead_tup::numeric / nullif(n_live_tup + n_dead_tup, 0) * 100, 1) AS dead_pct, last_vacuum, last_autovacuum FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 10; -- Накопичення idle транзакцій (connection leak) SELECT count(*), state, wait_event_type FROM pg_stat_activity WHERE pid != pg_backend_pid() GROUP BY state, wait_event_type ORDER BY count DESC; -- Зростання розмірів тимчасових файлів SELECT temp_files, temp_bytes FROM pg_stat_database WHERE datname = current_database(); Аналіз тренду деградації
Після тесту запускаємо Python-скрипт, що обчислює регресію RSS:
# analyze_soak.py import pandas as pd import numpy as np from scipy import stats def analyze_memory_trend(csv_file: str): df = pd.read_csv(csv_file, parse_dates=['timestamp']) df['minutes'] = (df['timestamp'] - df['timestamp'].iloc[0]).dt.total_seconds() / 60 slope, intercept, r_value, p_value, std_err = stats.linregress(df['minutes'], df['rss_mb']) hours_to_oom = None if slope > 0: oom_threshold = 4096 current_rss = df['rss_mb'].iloc[-1] hours_to_oom = (oom_threshold - current_rss) / (slope * 60) print(f"Memory growth rate: {slope:.2f} MB/min ({slope*60:.1f} MB/hour)") if hours_to_oom: print(f"Estimated OOM in: {hours_to_oom:.1f} hours") if p_value < 0.01 and slope > 0.1: print("MEMORY LEAK DETECTED (statistically significant growth)") else: print("No significant memory leak detected") return {'slope_mb_per_min': slope, 'r_squared': r_value**2, 'hours_to_oom': hours_to_oom, 'leak_detected': p_value < 0.01 and slope > 0.1} Як інтерпретувати результати soak-тесту?
Побудований часовий ряд RSS та P95 latency — ключ до виявлення деградації. Якщо нахил тренду RSS додатний і статистично значущий, це витік. P95 latency, що зростає після 2–4 годин, вказує на проблеми з GC або пулом з'єднань. Додатково перевіряємо dead tuple ratio в PostgreSQL: якщо він перевищує 10%, це bloat. Для кожної проблеми ми даємо конкретні рекомендації: від оптимізації коду до зміни конфігурації БД.
Що входить у роботу
- Аналіз архітектури та профілю навантаження вашого застосунку.
- Розробка k6-сценарію з реалістичними користувацькими сценаріями.
- Налаштування моніторингу пам'яті, з'єднань БД, файлових дескрипторів.
- Запуск soak-тесту тривалістю 8–24 години на staging-стенді.
- Побудова графіків часових рядів та регресійний аналіз трендів.
- Детальний звіт з виявленими деградаціями та рекомендаціями щодо їх усунення.
- Консультація по фіксам коду та конфігурації.
- Безкоштовний повторний тест, якщо проблему не було виявлено.
Типові знахідки та рішення
| Проблема | Симптом | Рішення |
|---|---|---|
| Витік EventEmitter (Node.js) | MaxListenersExceededWarning | Використовувати emitter.removeListener() або once() |
| Незакриті DB connections | Зростання з'єднань у pg_stat_activity | pool.release() у finally блоці або ORM-level connection pooling |
| Накопичення cron jobs | Дублювання фонових завдань | Додати mutex lock (Redis lock) |
| Redis pub/sub leak | Зростання підписок на канали | Відписуватися при завершенні з'єднання |
Наш досвід та гарантії
Ми займаємося навантажувальним тестуванням понад 7 років. На рахунку — 50+ проектів, де soak-тести запобігли критичним збоям на production. Гарантуємо якість: після виконання робіт ви отримуєте детальний звіт з графіками та рекомендаціями. Якщо проблема залишиться невиявленою — проведемо повторний тест безкоштовно.
Економія від запобігання одному інциденту OOM може сягати значної суми, а збитки від невиявленого витоку пам'яті — суттєві.
Терміни виконання: налаштування та запуск soak-тесту на 8–24 години з аналізом трендів — від 2 до 4 робочих днів. Оцінимо ваш проект за 1 день.
Замовте soak-тестування у наших інженерів — ми виявимо приховані деградації до того, як вони відбудуться на вашому продакшні. Отримайте консультацію та оцінку проекту, зв'язавшись з нами.







