Как последовательно выявить узкое место в производительности
Нагрузочный тест показал падение: 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 в месяц.







