Как последовательно выявить узкое место в производительности
Нагрузочный тест показал падение: latency взлетела с 50 ms до 2000 ms, а логи молчат. Ситуация знакомая — мы видели её на десятках проектов. В одном случае причиной оказался медленный JSON.parse в горячем пути: замена на simdjson снизила p95 latency с 200 ms до 30 ms — экономия на инфраструктуре составила более 60%. Подход один: измерить → найти → устранить → повторить.
Диагностический фреймворк: от метрик к узкому месту
Первым делом смотрим на метрики верхнего уровня. Если p95 latency высокая, а CPU загружен менее 70% — ищем проблему в БД или внешних вызовах. Если CPU 90–100% — профилируем код. Если растёт memory и идёт swap — ищем утечку. Системные ошибки (ENOMEM, EMFILE) — проверяем лимиты ОС. Ошибки 502/504 — смотрим балансировщик.
Высокая 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
Оптимизация базы данных: медленные запросы и N+1
Как найти медленные запросы в PostgreSQL?
Во время нагрузочного теста выполняем следующие запросы. Первый покажет активные запросы с длительностью. Второй — блокировки (кто кого ждёт). Третий — самые тяжёлые запросы по aggregate времени. Четвёртый — таблицы с sequential scans (потенциальные missing indexes).
-- Запущенные запросы прямо сейчас (выполнять во время теста)
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;
Почему N+1 запросы — частая причина деградации?
N+1 возникает, когда ORM для каждого родительского объекта выполняет отдельный запрос к связанной таблице. При 1000 пользователях это 1001 запрос вместо одного JOIN. Симптом: количество active connections в БД равно числу виртуальных пользователей, а pg_stat_statements показывает один и тот же запрос с большим количеством вызовов. Решение — eager loading, DataLoader или ручной JOIN. Использование pg_stat_statements для выявления N+1 в 10 раз быстрее ручного анализа логов.
Профилирование приложения: Node.js, Python и connection pool
Профилирование CPU в Node.js
Самый простой способ — включить V8 profiler через сигнал. Запускаем код под нагрузкой, отправляем kill -USR1 <pid>, через 30 секунд получаем cpu-profile.cpuprofile, открываем в Chrome DevTools.
// 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
Альтернатива — flamegraph через утилиту 0x. Она собирает стектрейсы и рисует интерактивный граф, где ширина полосы — время выполнения. Типичные находки: JSON.parse/stringify в hot path, bcrypt с высоким cost factor, некешированные regex, синхронные файловые операции.
npm install -g 0x
0x --output-dir profile node server.js &
APP_PID=$!
k6 run tests/load/main.js
kill -USR2 $APP_PID
# Откроется flamegraph.html
Профилирование Python под нагрузкой
Для production используем pyinstrument — он не требует перезапуска и даёт детальный отчёт по каждому запросу. Добавляем middleware, которая включает профилирование по параметру ?profile=true.
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()
response.data = g.profiler.output_html()
response.content_type = 'text/html'
return response
# Запрос с профилированием: GET /api/posts?profile=true
Анализ connection pool
Если клиенты ждут соединения (cl_waiting > 0 в pgBouncer), пул мал. Проверяем через SHOW POOLS; или SQL-запросом к pg_stat_activity.
-- 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: как найти момент деградации?
Скрипт анализирует временной ряд p95 latency и находит первую минуту, когда значение превысило порог (например, 500 мс). Это помогает привязать деградацию к конкретному моменту теста.
Скрипт для анализа временных рядов k6 (нажмите, чтобы развернуть)
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_df = df[df['metric'] == 'http_req_duration'].copy()
p95_df = p95_df.set_index('timestamp').resample('1min')['value'].quantile(0.95)
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
Инструменты и типичные оптимизации
Сравнение инструментов профилирования
| Инструмент | Область | Глубина | Влияние на production |
|---|---|---|---|
| pg_stat_statements | PostgreSQL | Высокая | Нет |
| V8 profiler | Node.js | Высокая | Минимальное |
| pyinstrument | Python | Средняя | Нет |
| 0x | Node.js | Высокая | Требует перезапуска |
| flamegraph | Общая | Высокая | Зависит от сборщика |
Типичные оптимизации после анализа
| Узкое место | Симптом | Решение |
|---|---|---|
| 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 |
Результаты анализа и процесс заказа
После анализа мы предоставляем:
- Отчёт с графиками временных рядов (latency, throughput, CPU, memory, БД-метрики)
- Скрипты для воспроизведения нагрузки
- Детальный список узких мест с кодом и конфигами
- Рекомендации по оптимизации (приоритет, трудозатраты)
- Верификационный тест после внедрения изменений
- Консультацию по архитектуре для предотвращения будущих проблем
Полный анализ с отчётом и верификацией занимает 1–2 рабочих дня. Стоимость рассчитывается индивидуально в зависимости от сложности системы. Свяжитесь с нами — мы оценим ваш проект и подберём оптимальный формат работы. Наши инженеры имеют 10+ лет опыта в нагрузочном тестировании и оптимизации. Средняя экономия на облачных ресурсах после наших оптимизаций составляет от $3000 до $10000 в месяц.







