Аналіз вузьких місць за результатами нагрузного тесту
Нагрузний тест показав деградацію — залишилось знайти причину. Послідовний аналіз: мережевий рівень → програма → база даних → інфраструктура. Вузьке місце завжди одне: виправити його, запустити тест знову.
Діагностична послідовність
Висока latency або помилки
│
├── Висока p95 latency, CPU < 70%, пам'ять ОК
│ └── → База даних: повільні запити, блокування, N+1
│
├── CPU 90–100%, latency растет пропорційно
│ └── → Обчислювальний bottleneck: профілювати CPU-hot paths
│
├── Пам'ять растет, swap активний
│ └── → Витік пам'яти або heap занадто малий
│
├── ENOMEM / EMFILE / ECONNREFUSED
│ └── → Системні ліміти: ulimit, file descriptors, TCP backlog
│
└── 502/504 помилки, програма ОК
└── → Nginx upstream, load balancer timeout
Аналіз PostgreSQL під навантаженням
-- Запущені запити прямо зараз (виконувати під час тесту)
SELECT pid, now() - query_start AS duration,
state, wait_event_type, wait_event,
left(query, 100) AS query_preview
FROM pg_stat_activity
WHERE state != 'idle'
AND query NOT LIKE '%pg_stat_activity%'
ORDER BY duration DESC;
-- Блокування: хто кого блокує
SELECT blocked.pid, blocked.query,
blocking.pid AS blocking_pid,
blocking.query AS blocking_query
FROM pg_stat_activity blocked
JOIN pg_stat_activity blocking
ON blocking.pid = ANY(pg_blocking_pids(blocked.pid))
WHERE blocked.cardinality(pg_blocking_pids(blocked.pid)) > 0;
-- Найважчі запити (pg_stat_statements)
SELECT query, calls, mean_exec_time, total_exec_time,
stddev_exec_time, rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;
-- Відсутні індекси: sequential scans на великих таблицях
SELECT relname, seq_scan, seq_tup_read,
idx_scan, seq_tup_read / nullif(seq_scan, 0) AS avg_rows_per_seqscan
FROM pg_stat_user_tables
WHERE seq_scan > 100
AND seq_tup_read > 10000
ORDER BY seq_tup_read DESC;
Node.js: CPU профілювщик
// server.js — вмикнути V8 profiling через сигнал
process.on('SIGUSR1', () => {
const { Session } = require('inspector')
const session = new Session()
session.connect()
session.post('Profiler.enable')
session.post('Profiler.start')
// Профілювати 30 секунд
setTimeout(() => {
session.post('Profiler.stop', (err, { profile }) => {
require('fs').writeFileSync('./cpu-profile.cpuprofile', JSON.stringify(profile))
console.log('CPU profile saved to cpu-profile.cpuprofile')
session.disconnect()
})
}, 30000)
})
// Запустити під навантаженням: kill -USR1 <pid>
// Відкрити в Chrome DevTools → More Tools → JavaScript Profiler
# Альтернатива: 0x flamegraph
npm install -g 0x
0x --output-dir profile node server.js &
APP_PID=$!
# Запустити k6 тест
k6 run tests/load/main.js
# Зупинити та отримати flamegraph
kill -USR2 $APP_PID
# Відкриється flamegraph.html у браузері
Flamegraph: що шукати
Широкі плоскі смуги в середині — довга CPU-робота. Типові знахідки:
- JSON.parse/stringify в hot path — переключитися на streaming або schema-based serializer
- bcrypt з високим cost factor — зменшити cost або кешувати сесії
- Regex без компіляції — винести за межі функції
- Синхронна файлова операція (fs.readFileSync) у запиті
Python: профілювщик під навантаженням
# pyinstrument — non-intrusive profiler для production
pip install pyinstrument
# Middleware для Flask
from pyinstrument import Profiler
from flask import request, g
@app.before_request
def start_profiler():
if request.args.get('profile') == 'true':
g.profiler = Profiler()
g.profiler.start()
@app.after_request
def stop_profiler(response):
if hasattr(g, 'profiler'):
g.profiler.stop()
# Вернути HTML-звіт у відповіді
response.data = g.profiler.output_html()
response.content_type = 'text/html'
return response
# Запит з профілюванням: GET /api/posts?profile=true
Аналіз пула соединень
# Моніторинг pgBouncer під час тесту
psql -h localhost -p 6432 pgbouncer -c "SHOW POOLS;"
# Що дивитися:
# cl_active: клієнти активно працюють
# cl_waiting: клієнти чекають соединення (>0 = проблема)
# sv_active: серверні соединення активні
# sv_idle: простаювальні соединення у пулі
# maxwait: максимальний час очікування (сек)
-- PostgreSQL: статистика пула соединень
SELECT datname, count(*) AS total_connections,
count(*) FILTER (WHERE state = 'active') AS active,
count(*) FILTER (WHERE state = 'idle') AS idle,
count(*) FILTER (WHERE wait_event_type = 'Lock') AS waiting_lock
FROM pg_stat_activity
GROUP BY datname;
Аналіз результатів k6: знайти момент деградації
# parse_k6_results.py
import json
import pandas as pd
def find_degradation_point(json_results: str):
"""Знайти момент деградації по часовому ряду метрик"""
records = []
with open(json_results) as f:
for line in f:
try:
record = json.loads(line)
if record.get('type') == 'Point':
records.append({
'timestamp': record['data']['time'],
'metric': record['metric'],
'value': record['data']['value']
})
except:
continue
df = pd.DataFrame(records)
df['timestamp'] = pd.to_datetime(df['timestamp'])
# Аналізувати p95 latency по 1-хвилинних вікнах
p95_df = df[df['metric'] == 'http_req_duration'].copy()
p95_df = p95_df.set_index('timestamp').resample('1min')['value'].quantile(0.95)
# Знайти першу хвилину, коли p95 перевищив поріг
threshold = 500 # ms
degradation = p95_df[p95_df > threshold]
if not degradation.empty:
print(f"Degradation detected at: {degradation.index[0]}")
print(f"p95 at degradation: {degradation.iloc[0]:.0f}ms")
else:
print("No degradation detected (all within threshold)")
return p95_df
Типові оптимізації після аналізу
| Вузьке місце | Симптом | Рішення |
|---|---|---|
| N+1 запити до БД | DB active queries >> VU count | DataLoader / eager loading / JOIN |
| Відсутній індекс | SeqScan на великій таблиці | CREATE INDEX CONCURRENTLY |
| Повільна JSON серіалізація | CPU висока, hot path в serialize | Protobuf / simdjson / msgpack |
| Connection pool overflow | cl_waiting > 0 у pgBouncer |
Збільшити pool_size або додати replicas |
| GC паузи | Spiky latency без CPU навантаження | Збільшити heap, tune GC flags |
| Блокування таблиць | wait_event = Lock у pg_stat |
Оптимізувати порядок операцій, NOWAIT |
Часовий графік
Повний аналіз вузьких місць за результатами нагрузного тесту з рекомендаціями та верифікаційним тестом — 1–2 робочих дні.







