Як послідовно виявити вузьке місце продуктивності
Навантажувальний тест показав падіння: latency злетіла з 50 ms до 2000 ms, а логи мовчать. Ситуація знайома — ми бачили її на десятках проектів. В одному випадку причиною виявився повільний JSON.parse в гарячому шляху: заміна на simdjson знизила p95 latency з 200 ms до 30 ms — економія на інфраструктурі склала понад 60% (близько $6000 на місяць). Підхід один: виміряти → знайти → усунути → повторити.
Діагностичний фреймворк: від метрик до вузького місця
Насамперед дивимося на метрики верхнього рівня. Якщо p95 latency висока, а CPU завантажений менше ніж 70% — шукаємо проблему в БД або зовнішніх викликах. Якщо CPU 90–100% — профілюємо код. Якщо зростає memory і йде swap — шукаємо витік. Системні помилки (ENOMEM, EMFILE) — перевіряємо ліміти ОС. Помилки 502/504 — дивимося балансувальник.
Висока latency або помилки
│
├── p95 latency висока, CPU < 70%, memory OK
│ └── → База даних: повільні запити, блокування, 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, додаток OK
└── → 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;
За даними документації PostgreSQL, pg_stat_statements є стандартним розширенням для моніторингу запитів (PostgreSQL docs).
Причини деградації через N+1 запити
N+1 виникає, коли ORM для кожного батьківського об'єкта виконує окремий запит до пов'язаної таблиці. При 1000 користувачах це 1001 запит замість одного JOIN. Симптом: кількість active connections в БД дорівнює кількості віртуальних користувачів, а pg_stat_statements показує один і той самий запит з великою кількістю викликів. Рішення — eager loading, DataLoader або ручний JOIN. Використання pg_stat_statements для виявлення N+1 у 10 разів швидше за ручний аналіз логів. За даними документації k6, інструмент спеціалізується на навантажувальному тестуванні (k6 docs).
Профілювання додатку: Node.js, Python та connection pool
Профілювання CPU в Node.js
Найпростіший спосіб — увімкнути V8 profiler через сигнал. Запускаємо код під навантаженням, відправляємо kill -USR1 <pid>, через 30 секунд отримуємо cpu-profile.cpuprofile, відкриваємо в Chrome DevTools. V8 profiler дає на 50% більше детальної інформації порівняно з профілюванням на рівні застосунку.
// 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, налаштувати GC flags |
| Блокування на таблицях | wait_event = Lock в pg_stat |
Оптимізувати порядок операцій, NOWAIT |
Що входить у результат?
- Звіт з графіками часових рядів (latency, throughput, CPU, memory, БД-метрики)
- Скрипти для відтворення навантаження
- Детальний список вузьких місць з кодом та конфігами
- Рекомендації з оптимізації (пріоритет, трудозатрати)
- Верифікаційний тест після впровадження змін
- Консультація з архітектури для запобігання майбутнім проблемам
- Навчання команди (за потреби)
- Підтримка при впровадженні
- Сертифікат виконаних робіт
Результати аналізу та процес замовлення
Повний аналіз зі звітом та верифікацією займає 1–2 робочих дні. Вартість розраховується індивідуально залежно від складності системи. Зв'яжіться з нами — ми оцінимо ваш проект і підберемо оптимальний формат роботи. Наші інженери мають 10+ років досвіду в навантажувальному тестуванні та оптимізації, ми гарантуємо якість аналізу. Середня економія на хмарних ресурсах після наших оптимізацій становить від $3000 до $10000 на місяць.







