Анализ узких мест по результатам нагрузочного теста
Нагрузочный тест показал деградацию — осталось найти причину. Последовательный анализ: сетевой уровень → приложение → база данных → инфраструктура. Узкое место всегда одно: устранить его, запустить тест снова.
Диагностическая последовательность
Высокая latency или ошибки
│
├── p95 latency высокая, CPU < 70%, memory ОК
│ └── → База данных: медленные запросы, блокировки, N+1
│
├── CPU 90–100%, latency растёт пропорционально
│ └── → Вычислительный bottleneck: профилировать CPU-hot paths
│
├── Memory растёт, swap активен
│ └── → Утечка памяти или heap too small
│
├── 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;
-- Missing indexes: 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
Анализ connection pool
# Мониторинг 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: найти moment деградации
# 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 # мс
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 serialize | 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 рабочих дня.







