Node.js Internals
Profiling & Debugging: Диагностика Node.js
3 часа ночи. Production сервер жрёт 100% CPU. Пользователи не могут залогиниться. PM пишет в Slack: 'WTF???'. Вы открываете мониторинг - графики похожи на кардиограмму умирающего. CPU spike. Memory растёт. Response time 10 секунд. Но КТО виноват? КАКАЯ функция? ГДЕ в коде? Без профайлера вы слепы. С профайлером - один flame graph, 30 секунд анализа, и вы видите: 85% CPU в функции validateRegex(). Hotfix: заменили регулярку. CPU упал до 10%. Инцидент закрыт. Профилирование - разница между 3 часами debugging'а и 3 минутами.
- **E-commerce Black Friday:** API начал падать под нагрузкой. CPU 100%, response time 5 секунд. Запустили 0x profiler на canary инстансе. Flame graph показал: 90% времени в JSON.stringify() огромных объектов Product с circular references. Фикс: сериализация только нужных полей. CPU с 100% → 20%, выдержали пик без scaling.
- **SaaS платформа память утекла:** Heap растёт на 200MB/день. Через неделю OOM. Heap snapshot comparison: +500K объектов EventEmitter в старой версии библиотеки. Retention path: global → app → router → middleware → EventEmitter. Оказалось, middleware регистрировал listeners при каждом request. Фикс: вынести регистрацию из middleware. Утечка устранена.
- **Real-time чат latency spike:** Users жалуются на задержки сообщений до 3 секунд. CPU в норме, memory в норме. Clinic.js Bubbleprof показал: 10 последовательных DB queries для загрузки истории чата (N+1 problem). Каждый query 250ms → total 2.5s. Фикс: один JOIN query вместо N+1. Latency с 2.5s → 250ms.
Зачем профилировать Node.js
Ваш API вдруг начал жрать 90% CPU. Или respond time вырос с 50ms до 2 секунд. Или память растёт на 100MB в час. **Без профилировщика вы слепы** - можете только гадать: база тормозит? V8 GC сошёл с ума? Где-то бесконечный цикл? Профилирование - это рентген производительности: показывает точно, какая функция сжирает CPU, где аллоцируется память, почему event loop стоит.
**Типы профилирования:** **CPU profiling** - какая функция сколько времени выполняется (flame graph). **Memory profiling** - кто аллоцирует объекты и почему они не освобождаются (heap snapshots, allocation timeline). **Event Loop profiling** - почему event loop тормозит (Clinic.js Doctor). Каждый тип решает свой класс проблем.
**Node.js Inspector Protocol** - это стандартизированный протокол для дебага и профилирования. Он работает поверх WebSocket и совместим с Chrome DevTools, VS Code Debugger, и всеми профайлерами экосистемы. Запускаете с флагом `--inspect` - получаете полный доступ к движку V8: breakpoints, CPU/memory profiling, heap snapshots, async stack traces.
Какой тип профилирования нужен, если ваше приложение медленно отвечает на запросы, но CPU и память в норме?
Inspector Protocol и Chrome DevTools
**Inspector Protocol - это мост между Node.js и инструментами разработчика.** Когда вы запускаете `node --inspect app.js`, Node открывает WebSocket на порту 9229 и ждёт подключения клиента (Chrome DevTools, VS Code, etc). Протокол поддерживает 100+ команд: от установки breakpoint до снятия heap snapshot. Это тот же протокол, что Chrome использует для дебага браузерного JS.
**Флаги запуска:** `--inspect` - запускает Inspector на 127.0.0.1:9229 (только localhost). `--inspect=0.0.0.0:9229` - доступ с любого IP (опасно без туннеля!). `--inspect-brk` - стартует с паузой на первой строке (для дебага старта приложения). `--inspect-publish-uid=http` - публикует URL в stdout для автоподключения.
**Chrome DevTools для Node.js:** После подключения доступны все вкладки: **Console** (REPL в контексте приложения), **Sources** (breakpoints, step-through debugging), **Memory** (heap snapshots, allocation profiling), **Profiler** (CPU flame graphs). Можно даже выполнять произвольный код в контексте приложения через Console - мощный инструмент для hotfix в продакшене.
Почему --inspect=0.0.0.0:9229 опасен без SSH туннеля в продакшене?
CPU Profiling: Flame Graphs и V8 Profiler
**CPU profiling показывает, где процессор тратит время.** V8 profiler сэмплирует call stack каждые ~1ms и строит дерево вызовов с процентами времени. **Flame graph** - это визуализация: ширина блока = время в функции, высота = глубина стека. Если видите широкий блок - это hotspot, который жрёт CPU. Flame graph мгновенно показывает, что `validateInput()` занимает 80% времени запроса.
**Как читать flame graph:** Ось X - не время, а алфавитный порядок (или сортировка). Ширина блока - процент CPU time. Ось Y - глубина call stack (снизу - root, сверху - leaf функции). **Цвета** обычно случайные для различения, но некоторые инструменты подсвечивают: красный - JavaScript, жёлтый - C++ (V8 internals), зелёный - system calls.
**Интерпретация результатов:** Если функция в flame graph **широкая, но мелкая** (низко в стеке) - она сама медленная (heavy computation). Если **узкая, но глубокая** - она быстрая, но вызывается миллион раз. Оптимизация стратегии разные: для первого - алгоритм, для второго - кэширование или батчинг.
Что означает, если функция в flame graph занимает 50% ширины, но находится глубоко в стеке (много функций под ней)?
Memory Profiling: Heap Snapshots и Allocation Timeline
**Memory profiling - это поиск того, кто жрёт память и почему не отдаёт.** Два основных инструмента: **Heap Snapshot** (статичная картина памяти в момент времени) и **Allocation Timeline** (запись аллокаций во времени). Heap snapshot показывает **что** сейчас в памяти, allocation timeline - **когда** и **где** создавались объекты. Для поиска утечек используют **snapshot comparison** - diff между двумя snapshot'ами.
**Heap Snapshot содержит:** Все объекты в heap с их размерами (shallow size - сам объект, retained size - объект + всё, что он держит). Retention paths (цепочки ссылок от GC root). Группировку по constructor (сколько объектов типа Array, Map, User, etc). **Allocation Timeline:** Записывает каждую аллокацию с timestamp и call stack. Можно увидеть, что за последние 10 секунд создалось 100K объектов User в функции handleRequest.
**Retention Path - ключ к пониманию утечек.** Если объект в snapshot имеет retention path через несколько промежуточных объектов до GC root - значит он достижим и GC не удалит его. Типичный path утечки: `window/global → EventEmitter → listeners array → callback closure → captured variables → утекший объект`. Разорвав любую ссылку в цепи, освобождаете всё downstream.
В чём разница между Heap Snapshot и Allocation Timeline?
Clinic.js: Doctor, Flame, Bubbleprof
**Clinic.js - это швейцарский нож для диагностики Node.js.** Три инструмента: **Clinic Doctor** (детектирует проблемы: Event Loop delay, I/O, memory), **Clinic Flame** (CPU flame graphs с V8 optimization info), **Clinic Bubbleprof** (async operations визуализация). Doctor говорит **что** сломано, Flame показывает **где** в коде, Bubbleprof объясняет **почему** асинхронные операции тормозят.
**Clinic Doctor детектирует:** Event Loop задержки (синхронные блокировки), I/O bottlenecks (медленные операции чтения/записи), Memory issues (рост heap, частые GC). **Clinic Flame:** Расширенный flame graph с пометками V8 optimization states (optimized/not optimized/deoptimized). **Clinic Bubbleprof:** Визуализация async operations как пузыри - размер = latency, цвет = тип (I/O, timers, etc).
**Когда что использовать:** **Doctor** - первый шаг диагностики, быстро показывает класс проблемы (CPU/Memory/I/O). **Flame** - когда Doctor показал CPU issue, flame graph находит конкретную функцию. **Bubbleprof** - когда latency высокая, но CPU не загружен → проблема в async операциях или их orchestration. Вместе они дают полную картину.
Clinic.js Bubbleprof показывает большой пузырь (250ms) для операции 'DB Query', выполняемой 10 раз последовательно. Что это означает?
Профилирование в продакшене
**Профилирование в продакшене - это искусство минимального overhead.** Нельзя просто включить `--inspect` на всех серверах - это дыра в безопасности. Нельзя запустить Clinic.js - он замедлит приложение на 30%. Нужны **production-safe** инструменты: sampling profiling (минимальный overhead), on-demand включение через сигналы, автоматический сбор метрик без остановки сервиса.
**Production-safe стратегии:** **Sampling CPU profiling** - сэмплирование call stack каждые 10ms (overhead <5%). **Heap snapshots on-demand** - снимаем snapshot по SIGUSR2, не постоянно. **Continuous profiling** - отправка профилей в систему мониторинга (DataDog, Grafana Pyroscope). **Canary profiling** - профилируем 1% инстансов, остальные работают нормально.
**Безопасность в продакшене:** Никогда не открывайте Inspector на 0.0.0.0. Используйте Unix sockets (`--inspect=/tmp/node-inspect.sock`) или SSH tunnels. Heap snapshots содержат sensitive data (токены, пароли в памяти) - шифруйте перед отправкой. Ротируйте старые профили автоматически (retention policy 7 дней). Логируйте все действия профилирования в audit log.
Профилирование нужно только когда что-то сломалось
Continuous profiling в продакшене позволяет детектировать регрессии до того, как они станут критичными
Если включать профайлер только при инциденте, вы видите последствия, а не причину. Continuous profiling строит базовый уровень производительности и автоматически детектирует отклонения. Например, новый деплой ввёл регрессию - функция стала занимать 15% CPU вместо 5%. Без continuous profiling вы узнаете об этом через неделю, когда пользователи начнут жаловаться. С continuous profiling - через минуту после деплоя через diff flame graphs между версиями.
Почему continuous CPU profiling в продакшене использует sampling с интервалом 10ms, а не 1ms?
Ключевые идеи
- **Inspector Protocol — универсальный протокол для дебага и профилирования Node.js.** Работает через WebSocket, совместим с Chrome DevTools. Флаги: --inspect (localhost), --inspect-brk (пауза на старте). В продакшене только через SSH tunnel, никогда 0.0.0.0.
- **CPU profiling показывает hotspots через flame graphs.** Ширина блока = CPU time, высота = call stack. Инструменты: Chrome DevTools Profiler, 0x (автоматический flamegraph), Clinic Flame (с V8 optimization info). Ищем широкие блоки — там проблема.
- **Memory profiling: Heap Snapshots (что в памяти) + Allocation Timeline (где создаются объекты).** Heap snapshot comparison находит утечки через diff. Retention path показывает, кто держит объект в живых. WeakMap/WeakRef для автоматической очистки.
- **Clinic.js — три инструмента: Doctor (что сломано), Flame (где в коде), Bubbleprof (почему async тормозит).** Doctor детектирует Event Loop delay, I/O bottlenecks, memory issues. Bubbleprof визуализирует async operations — большие пузыри показывают медленные операции.
- **Production profiling: on-demand через сигналы, continuous profiling (DataDog/Pyroscope), canary (10% инстансов).** Sampling с 10ms = ~5% overhead. Heap snapshots по SIGUSR2. Никогда --inspect на 0.0.0.0. Шифровать профили — они содержат sensitive data.
Связанные темы
Profiling связан со всеми аспектами производительности Node.js - от Event Loop до Memory Management:
- Performance Hooks — Performance Hooks измеряют время операций (mark/measure), profiling показывает, где это время тратится (flame graphs). Используйте вместе: mark() для custom метрик + CPU profiler для детального анализа.
- Memory Management — Memory profiling (heap snapshots, allocation timeline) - это инструменты для поиска утечек, описанных в Memory Management. GC паузы видны в CPU profiler как блоки V8 GC.
- Event Loop — Event Loop delay показывает, что приложение тормозит. CPU profiler объясняет, какая функция блокирует loop. Clinic Doctor автоматически связывает Event Loop metrics с hotspots.
Вопросы для размышления
- Ваш API медленно отвечает, но CPU и память в норме. Какой тип профилирования вы запустите первым и почему? Подсказка: если CPU не загружен, где тратится время?
- Flame graph показывает, что 60% CPU в функции parseJSON, но эта функция вызывается из 10 разных мест. Как найти, какой из 10 вызовов самый частый? Подсказка: посмотрите на call stack выше parseJSON.
- Heap snapshot comparison показывает +100K объектов Promise с retention path через global.pendingRequests. Какие архитектурные паттерны могли привести к этой утечке? Как исправить без изменения логики приложения?