Performance hooks, профайлинг, телеметрия
Под прод-нагрузкой Node-приложение либо отвечает в рамках SLO, либо нет. Если нет, нужны инструменты, чтобы понять причину: CPU занят? GC давит? event loop встал? сеть медленная? зависимый сервис тормозит? Этот урок про инструменты и про их выбор.
Мы НЕ будем доходить до ассемблера V8 (есть отдельная тема для тех,
кому надо). Урок про инструменты, которые покрывают 95% задач:
perf_hooks, --inspect, flamegraph, heap snapshot, OpenTelemetry.
1. Карта типов узких мест
Перед инструментами, словарь:
| Тип | Симптом | Где смотреть |
|---|---|---|
| CPU-bound | 100% CPU, latency растёт линейно с RPS | flamegraph, CPU profile |
| I/O-bound | CPU 5-20%, latency держится на сети/диске | observability на зависимостях, traces |
| Memory-bound | RSS растёт, OOM, GC pauses | heap snapshot, allocation timeline |
| Event loop bound | latency у быстрых ручек растёт | 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.