lenec ru

← все посты

Профилирование Node.js приложений в production: clinic.js, 0x и встроенный profiler

13K

Когда production-приложение на Node.js начинает тормозить, догадки не помогут — нужны данные. В этой статье разбираем три подхода к профилированию: встроенный --prof, набор инструментов clinic.js и flamegraph-генератор 0x. Покажем overhead каждого метода и найдём реальный bottleneck в Express API.

Встроенный профайлер: --prof и flamegraph

Node.js поставляется с CPU-профайлером V8 из коробки. Никаких зависимостей:

node --prof app.js

Запускаем приложение, генерируем нагрузку (curl в цикле, autocannon, k6), останавливаем процесс. В директории появится файл isolate-0x123456-v8.log. Обрабатываем его:

node --prof-process isolate-0x*.log > profile.txt

Открываем profile.txt — увидим статистику вызовов функций с процентами CPU-времени. Формат текстовый, но читаемый. Топ функций покажет, где процессор проводит больше всего времени.

Для визуализации можно использовать --cpu-prof (Node.js 12+), который генерирует .cpuprofile — его открывают в Chrome DevTools → Performance → Load profile. Получаем flame chart с call stack'ами.

Программный доступ через v8-profiler-next:

const profiler = require('v8-profiler-next');
profiler.startProfiling('prod-sample', true);

setTimeout(() => {
  const profile = profiler.stopProfiling('prod-sample');
  profile.export((err, result) => {
    fs.writeFileSync(`profile-${Date.now()}.cpuprofile`, result);
    profile.delete();
  });
}, 30000); // 30 секунд профилирования

Этот подход безопасен для production: overhead сэмплирующего профайлера — около 1–3% CPU. Главное — ограничить время профилирования и не писать файлы на локальный диск в контейнерах (лучше в S3/GCS).

clinic.js: doctor, flame, bubbleprof

Clinic.js — это швейцарский нож для диагностики Node.js. Три инструмента под разные задачи:

clinic doctor — диагностика типа проблемы

Doctor не строит flamegraph. Он собирает четыре метрики (event loop delay, CPU, memory, active handles) и применяет эвристики, чтобы сказать, в какой категории проблема: event loop lag, I/O, GC или CPU.

npm install -g clinic
clinic doctor --on-port 'autocannon -c 100 -d 20 localhost:$PORT' -- node server.js

После завершения нагрузки (Ctrl+C) clinic откроет HTML-отчёт с цветным баннером-вердиктом. Если баннер красный и говорит "CPU-bound" — переходим к clinic flame. Если "I/O-bound" — к clinic bubbleprof. Не стоит строить flamegraph для I/O-проблемы — увидите только стену epoll_wait.

Важно: Doctor — это инструмент для staging, не для production. Overhead значительный, отчёты большие, паттерн SIGINT несовместим с graceful shutdown в Kubernetes. Для production используйте perf_hooks.monitorEventLoopDelay (встроен в Node.js 11.10+, overhead ~0%):

const { monitorEventLoopDelay } = require('perf_hooks');
const h = monitorEventLoopDelay({ resolution: 10 });
h.enable();

setInterval(() => {
  console.log({
    min: h.min / 1e6, // в миллисекундах
    max: h.max / 1e6,
    mean: h.mean / 1e6,
    p99: h.percentile(99) / 1e6
  });
}, 10000);

Экспортируйте эти метрики в Prometheus/Datadog. Алерт на p99 > 50ms — сигнал запускать clinic doctor в staging.

clinic flame — поиск горячих функций

Flame строит интерактивный flamegraph. Ось X — CPU-время (не wall-clock), ось Y — глубина стека. Широкие блоки наверху — ваши узкие места.

clinic flame -- node server.js

Генерируем нагрузку, останавливаем (Ctrl+C), открываем .clinic/flamegraph.html. Пример из практики: flamegraph показал, что JWT-верификация занимает 40ms на запрос — оказалось, мы делали её дважды (в middleware и в route handler). Двадцать минут анализа графика сэкономили месяцы неправильных предположений.

clinic bubbleprof — async-операции

Bubbleprof визуализирует async-операции: промисы, коллбэки, таймеры. Показывает, где async-цепочки тормозят. Полезен, когда doctor говорит "I/O-bound", но непонятно, какой именно I/O.

clinic bubbleprof -- node server.js

Отчёт покажет пузыри — размер пропорционален времени ожидания. Большой пузырь на database query — N+1 проблема или медленный запрос.

0x: flamegraph для production

0x генерирует flamegraph на основе Linux perf (но работает на всех платформах, включая macOS и Windows). Включает нативный код (C++ extensions), который clinic flame может пропустить.

npm install -g 0x
0x server.js

Генерируем нагрузку, жмём Ctrl+C. Получаем интерактивный SVG-flamegraph. Правила чтения те же: широкий блок = медленно, высокий стек = глубокая вложенность.

Production-режим: 0x можно запустить с автоматической нагрузкой:

0x -P 'autocannon -c 50 -d 10 localhost:$PORT' server.js

После завершения autocannon процесс получит SIGINT, и flamegraph сгенерируется автоматически. Overhead сэмплирования — 1–3% CPU, как у встроенного профайлера.

Сравнение overhead

ИнструментOverhead CPUOverhead памятиProduction-safe
--prof1–3%НизкийДа (короткие сессии)
clinic doctor10–20%ВысокийНет
clinic flame5–10%СреднийStaging
clinic bubbleprof15–25%ВысокийНет
0x1–3%НизкийДа (короткие сессии)

Clinic doctor и bubbleprof — тяжёлые инструменты. Используйте их в staging под production-like нагрузкой. Для production подходят --prof, 0x и программный v8-profiler-next с ограничением по времени.

Реальный кейс: находим bottleneck в Express API

Симптом: эндпоинт GET /api/users/:id отвечает 200–300ms вместо ожидаемых 50ms. Метрики показывают p99 event loop delay = 120ms.

Шаг 1: Запускаем clinic doctor в staging:

clinic doctor --on-port 'autocannon -c 100 -d 30 localhost:$PORT/api/users/123' -- node server.js

Вердикт: "CPU-bound issue detected". Event loop delay коррелирует с CPU-спайками.

Шаг 2: Строим flamegraph через clinic flame:

clinic flame --on-port 'autocannon -c 100 -d 30 localhost:$PORT/api/users/123' -- node server.js

Открываем flamegraph. Самый широкий блок — JSON.parse внутри middleware. Оказывается, мы парсим большой JSON-конфиг (5MB) на каждом запросе вместо того, чтобы сделать это один раз при старте.

Шаг 3: Фиксим — выносим парсинг в инициализацию:

// Было:
app.use((req, res, next) => {
  const config = JSON.parse(fs.readFileSync('./config.json', 'utf8'));
  req.config = config;
  next();
});

// Стало:
const config = JSON.parse(fs.readFileSync('./config.json', 'utf8'));
app.use((req, res, next) => {
  req.config = config;
  next();
});

Шаг 4: Проверяем через 0x в production (30 секунд профилирования):

0x -P 'autocannon -c 50 -d 30 localhost:$PORT/api/users/123' server.js

Flamegraph чистый, JSON.parse исчез из топа. Latency упала до 45ms, p99 event loop delay — 8ms.

Подводные камни

  • Не профилируйте на пустом приложении. Запускайте реалистичную нагрузку — иначе flamegraph покажет инициализацию, а не рабочий код.
  • Clinic.js не поддерживается активно с 2024 года. Инструменты работают, но могут быть неточности на новых версиях Node.js (проверено на Node 16–20).
  • 0x требует Chrome для открытия flamegraph. В headless-окружениях сохраняйте SVG и открывайте локально.
  • Flamegraph не покажет I/O-проблемы. Если bottleneck в базе данных — используйте EXPLAIN ANALYZE для SQL или bubbleprof для async-цепочек.
  • GC может маскировать CPU-проблемы. Если в flamegraph много Scavenge/MarkSweep — проблема в памяти, а не в коде. Проверьте heap snapshots в Chrome DevTools.

Чек-лист для production-профилирования

  1. Добавьте perf_hooks.monitorEventLoopDelay в метрики. Алерт на p99 > 50ms.
  2. При срабатывании алерта — воспроизведите нагрузку в staging и запустите clinic doctor.
  3. Если doctor говорит "CPU-bound" — используйте clinic flame или 0x.
  4. Если "I/O-bound" — clinic bubbleprof + проверка query plans в БД.
  5. Для production-профилирования — 0x или программный v8-profiler-next с лимитом 30–60 секунд.
  6. Проверьте, что синхронные операции (fs.readFileSync, большие JSON.parse) не попали в hot path.
  7. Измерьте до и после — без baseline невозможно понять, помогла ли оптимизация.

Итого

Три инструмента — три сценария. --prof и 0x — лёгкие, подходят для production с ограничением по времени. Clinic.js — мощный набор для staging, но тяжёлый. Doctor триажирует проблему, flame находит горячие функции, bubbleprof — async-bottleneck'и. Не оптимизируйте без измерений — профилируйте сначала, гадайте никогда.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.