Раздел 6 · Node.js

Performance hooks, профайлинг, телеметрия

middle-senior~35 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Performance hooks, профайлинг, телеметрия

Под прод-нагрузкой Node-приложение либо отвечает в рамках SLO, либо нет. Если нет, нужны инструменты, чтобы понять причину: CPU занят? GC давит? event loop встал? сеть медленная? зависимый сервис тормозит? Этот урок про инструменты и про их выбор.

Мы НЕ будем доходить до ассемблера V8 (есть отдельная тема для тех, кому надо). Урок про инструменты, которые покрывают 95% задач: perf_hooks, --inspect, flamegraph, heap snapshot, OpenTelemetry.

1. Карта типов узких мест

Перед инструментами, словарь:

ТипСимптомГде смотреть
CPU-bound100% CPU, latency растёт линейно с RPSflamegraph, CPU profile
I/O-boundCPU 5-20%, latency держится на сети/дискеobservability на зависимостях, traces
Memory-boundRSS растёт, OOM, GC pausesheap snapshot, allocation timeline
Event loop boundlatency у быстрых ручек растётmonitorEventLoopDelay, async-stack
Lock contentionнизкая утилизация при росте нагрузкиtrace + profiler с lock tracking

Первый шаг диагностики, понять, в каком из этих квадрантов проблема. Без этого можно час смотреть flamegraph, который показывает 80% времени в epoll_wait, и не понимать, что это I/O.

2. perf_hooks: метрики в коде

node:perf_hooks даёт три полезные штуки:

import { performance, PerformanceObserver, monitorEventLoopDelay } from 'node:perf_hooks';

// 1. mark/measure для профилирования участков кода
performance.mark('a');
await heavyOperation();
performance.mark('b');
performance.measure('heavyOperation', 'a', 'b');

// 2. observer для агрегации
const obs = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(entry.name, entry.duration);
  }
});
obs.observe({ entryTypes: ['measure'] });

// 3. event loop delay histogram
const histogram = monitorEventLoopDelay({ resolution: 20 });
histogram.enable();
setInterval(() => {
  console.log({
    p50: histogram.percentile(50),
    p99: histogram.percentile(99),
    max: histogram.max,
  });
  histogram.reset();
}, 5_000);

mark/measure дают именованные интервалы. В приложении это эквивалент тегированных таймеров: где-то поставил mark, в другом месте measure, observer собрал данные.

monitorEventLoopDelay важнее всего из этих трёх. Он измеряет, на сколько event loop опаздывает с исполнением запланированных задач. Если p99 > 100 ms, у тебя где-то блокируется поток (синхронный JSON.parse, тяжёлый crypto.scryptSync, regex с catastrophic backtracking).

3. process.cpuUsage и memoryUsage

Базовые системные метрики прямо из process:

const before = process.cpuUsage();
// ... работа ...
const after = process.cpuUsage(before); // { user: 1234, system: 56 } в микросекундах

const mem = process.memoryUsage();
// rss, heapTotal, heapUsed, external, arrayBuffers

memoryUsage:

  • rss (resident set size): сколько памяти процесс реально держит в RAM (с кодом, heap, native).
  • heapTotal: размер выделенного V8-heap.
  • heapUsed: реально занято объектами JS внутри heap.
  • external: память, которой владеют C++ объекты (Buffer без pool, native modules).
  • arrayBuffers: ArrayBuffer-ы, отдельно от external.

Что отслеживать:

  • Если heapUsed растёт до heapTotal и не падает после GC, утечка в JS-объектах.
  • Если external растёт независимо от heapUsed, утечка в Buffer или native-модуле.
  • Если rss сильно больше heapTotal, много кода/native-памяти, не обязательно проблема.

4. --inspect и Chrome DevTools

Самый быстрый способ заглянуть внутрь работающего процесса:

$ node --inspect server.js
Debugger listening on ws://127.0.0.1:9229/...
For help, see: https://nodejs.org/en/docs/inspector

Открываешь chrome://inspect, находишь Node-процесс, кликаешь “inspect”. Получаешь полный DevTools:

  • Sources: точки останова, async stack-trace, conditional breakpoints (“сработай только когда userId === '123'”).
  • Profiler -> CPU profile: запиши сэмпл за минуту, увидишь flamegraph по функциям.
  • Memory -> Heap snapshot: один раз сделать срез heap, посмотреть, что в нём. Дважды, посмотреть delta.
  • Memory -> Allocation timeline: построчный учёт, где код выделил память.
  • Memory -> Allocation sampling: лёгкий профайлер аллокаций (можно оставить на минуты).

--inspect слушает на 127.0.0.1, не открывай --inspect=0.0.0.0 в проде (это полный RCE). Если нужен debugging прод-инстанса, делаем SSH-tunnel.

5. CPU profile через --prof

Для production-debugging часто нужен оффлайновый профиль, без интерактивных DevTools.

# собрать V8-tick-профиль
$ node --prof server.js
# ... нагрузка ...
^C

# обработать в человеческий формат
$ node --prof-process isolate-0xNNN-v8.log > profile.txt

В profile.txt будет список тиков по функциям. Полезно для grep-а: “какой код был на CPU в момент инцидента”.

Для flamegraph удобнее 0x:

$ npx 0x server.js

0x запускает Node под профайлером, после Ctrl+C рисует HTML с интерактивным flamegraph. Подходит для локального debugging и для single-host-нагрузочных тестов.

6. clinic.js: doctor, flame, bubbleprof

clinic.js, набор инструментов с диагностикой над профайлами:

# doctor: запускает нагрузку, смотрит несколько метрик одновременно,
# подсказывает, где ловит. Хороший первый шаг.
$ npx clinic doctor -- node server.js

# flame: flamegraph (как 0x), но интегрированный с doctor
$ npx clinic flame -- node server.js

# bubbleprof: визуализация async-операций (timeline пересекающихся
# Promise-цепочек). Помогает увидеть, кто тормозит pipeline.
$ npx clinic bubbleprof -- node server.js

clinic doctor, мой выбор для первого исследования. Часто он сразу говорит “у тебя event loop встал”, “GC давит”, “I/O ждёт” с подсветкой графиков.

дальше после входа

Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.

Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.