Connection pooling в Node.js: postgres, redis, HTTP keep-alive
Когда ваш Node.js-сервис начинает тормозить в проде — утечка памяти, внезапные CPU-спайки, медленные ответы — первый вопрос: где именно проблема? Логи молчат, метрики показывают только симптомы. Профилирование в production — это не роскошь, а необходимость. В этой статье разберём три главных инструмента: Clinic.js, 0x и pprof — и научимся читать flamegraph так, чтобы находить bottleneck за минуты, а не дни.
Зачем профилировать в проде: реальные кейсы
Типичная история: сервис работал месяцами, и вдруг p99-латентность выросла с 50ms до 800ms. Мониторинг показывает, что CPU скачет до 100%, но какой именно код жрёт циклы — неясно. Или другой сценарий: память растёт на 50MB в час, через сутки процесс падает с OOM. Heap snapshot в Chrome DevTools показывает миллион объектов, но какой из них — утечка?
Профилирование отвечает на вопрос «где». Не «почему медленно», а «какая функция съедает 80% времени». Не «есть ли утечка», а «какой event listener висит 10 000 раз». Конкретные примеры из практики:
- JWT-верификация дважды на запрос — middleware проверял токен, потом route handler проверял снова. Flamegraph показал два одинаковых стека по 40ms. Убрали дубль — латентность упала вдвое.
- Синхронный JSON.parse огромного payload — клиент слал 5MB JSON, парсинг блокировал event loop на 200ms. Flamegraph показал широкий блок
JSON.parse. Решение: streaming parser или ограничение размера. - Event listener на каждый запрос — middleware добавлял
process.on('unhandledRejection', ...), но никогда не удалял. Через 10k запросов — 10k слушателей. Heap snapshot показал массив listeners. Фикс:onceвместоon.
Главное правило: профилируй под нагрузкой. Flamegraph idle-сервера бесполезен — там только epoll_wait. Нужен реальный трафик или нагрузочный тест (autocannon, k6). Только тогда горячие пути проявятся.
Clinic.js: doctor, flame, bubbleprof
Clinic.js — это швейцарский нож для диагностики Node.js. Три инструмента, каждый решает свою задачу. Установка глобально:
npm install -g clinic
clinic doctor — первичная диагностика
Запускаешь, когда не знаешь, в чём проблема. Doctor собирает четыре метрики: event loop delay, CPU, память, активные handles. Затем выдаёт вердикт: «CPU-bound», «I/O-bound», «memory issue» или «event loop lag».
clinic doctor -- node server.js
Генерируешь нагрузку (например, autocannon -c 100 -d 30 http://localhost:3000), жмёшь Ctrl+C — clinic открывает HTML-отчёт. Четыре панели: event loop delay (верхний левый), CPU (верхний правый), память (нижний левый), handles (нижний правый). Если event loop delay коррелирует с CPU — это синхронный hotspot, иди в clinic flame. Если CPU низкий, а delay высокий — это I/O, иди в clinic bubbleprof.
Важно: doctor — это staging-инструмент, не production. Overhead ощутимый, файлы отчётов большие. Для прода используй perf_hooks.monitorEventLoopDelay() из Node.js core — zero overhead, экспортируй в Prometheus.
clinic flame — CPU-профилирование
Когда doctor сказал «CPU-bound», запускай flame. Это flamegraph-генератор с чистым UI:
clinic flame -- node server.js
Flamegraph читается снизу вверх: внизу — entry point (например, http.Server.emit), вверху — листовые функции. Ширина блока = CPU-время. Ищи широкие блоки — это hotspot. Пример: если bcrypt.hashSync занимает 60% ширины, значит, 60% CPU уходит на хеширование. Решение: async-версия (bcrypt.hash) или воркеры.
Clinic flame позволяет фильтровать по имени функции и скрывать Node.js internals — критично, чтобы видеть только свой код. Если видишь стену epoll_wait или uv__io_poll — это не CPU-проблема, это I/O-wait. Возвращайся к doctor.
clinic bubbleprof — async-bottleneck
Уникальная фича Clinic.js. Bubbleprof строит граф async-операций: промисы, коллбеки, таймеры. Показывает не CPU-время, а время ожидания. Если три запроса к БД идут последовательно, хотя могли бы параллельно — bubbleprof это покажет.
clinic bubbleprof -- node server.js
Граф выглядит как пузыри, соединённые стрелками. Большой пузырь = долгая операция. Цепочка пузырей = последовательное выполнение. Если видишь три пузыря «DB query» подряд — это N+1 или отсутствие Promise.all. Решение: батчинг или параллелизация.
Пример из практики: эндпоинт делал await getUser(), потом await getOrders(), потом await getProfile(). Три запроса по 50ms = 150ms total. Bubbleprof показал цепочку. Переписали на Promise.all([getUser(), getOrders(), getProfile()]) — латентность упала до 50ms.
pprof + 0x: flamegraph на практике
0x — однокомандный flamegraph
Если нужен только flamegraph без doctor/bubbleprof, используй 0x. Это самый быстрый способ получить интерактивный HTML с flame-визуализацией:
npm install -g 0x
0x -- node server.js
Генерируешь нагрузку, жмёшь Ctrl+C — 0x создаёт папку PID.0x с flamegraph.html. Открываешь в Chrome, видишь стеки. Можно автоматизировать с нагрузочным тестом:
0x -P 'autocannon -c 100 -d 30 localhost:$PORT' server.js
Флаг -P запускает команду, когда сервер откроет порт, затем автоматически останавливает процесс и генерирует flamegraph. Удобно для CI/CD: профилируешь каждый релиз, сравниваешь flamegraph — регрессии видны сразу.
Как читать flamegraph: ищи плато — широкие горизонтальные участки. Это функции, которые сами или через вызовы потомков съедают много CPU. Если видишь узкую башню — это глубокий call stack, но мало времени. Если видишь широкую низкую гору — это hotspot. Кликай на блок — 0x покажет процент от total time.
pprof — Google-формат профилей
pprof — это npm-пакет от Google для сбора CPU и heap-профилей в формате protobuf. Удобен, если у вас уже есть инфраструктура для pprof (например, Google Cloud Profiler).
npm install pprof
Сбор CPU-профиля:
const pprof = require('pprof');
const fs = require('fs');
async function profile() {
const profile = await pprof.time.profile({
durationMillis: 10000 // 10 секунд
});
const buf = await pprof.encode(profile);
fs.writeFileSync('cpu.pb.gz', buf);
}
profile();
Просмотр через CLI:
pprof -http=:8080 cpu.pb.gz
Откроется веб-интерфейс с flamegraph, top-функциями, call graph. pprof умеет сравнивать два профиля — полезно для A/B-тестирования оптимизаций. Heap-профили собираются аналогично через pprof.heap.profile().
Минус pprof: нужен нативный модуль (node-gyp), на некоторых окружениях сборка падает. Плюс: интеграция с облачными профайлерами, долгосрочное хранение профилей.
Сравнение инструментов: когда что использовать
| Проблема | Инструмент | Что покажет |
|---|---|---|
| Не знаю, в чём проблема | clinic doctor | Категория: CPU / I/O / memory / event loop |
| CPU высокий | 0x или clinic flame | Какие функции жрут циклы |
| Сервис медленный, но CPU низкий | clinic bubbleprof | Async-цепочки, последовательные операции |
| Память растёт | Heap snapshot (Chrome DevTools) | Какие объекты не удаляются |
| Event loop lag | perf_hooks.monitorEventLoopDelay + clinic flame | Синхронные блокировки |
| Нужен профиль для CI/CD | 0x с автотестом | Flamegraph каждого билда |
| Интеграция с облаком | pprof | Protobuf-профили для GCP/AWS |
Правило большого пальца: начинай с clinic doctor в staging. Он скажет, куда копать. Если CPU — иди в 0x (быстрее) или clinic flame (красивее). Если I/O — иди в bubbleprof. Если память — делай heap snapshot до и после нагрузки, сравнивай в Chrome DevTools (Memory → Comparison).
Чеклист: что делать при деградации перформанса
- Воспроизведи проблему локально или в staging. Профилирование production-процесса опасно (overhead, риск падения). Если нельзя воспроизвести — используй
perf_hooksдля сбора метрик в проде, анализируй офлайн. - Запусти clinic doctor под нагрузкой. Используй autocannon или k6 для генерации трафика, максимально близкого к продовому. Прочитай вердикт: CPU / I/O / memory / event loop.
- Если CPU-bound: запусти
0xилиclinic flame, найди широкие блоки в flamegraph. Типичные виновники: синхронный crypto (bcrypt, JWT), JSON.parse больших payload, регулярки на длинных строках, циклы по большим массивам. - Если I/O-bound: запусти
clinic bubbleprof, найди последовательные async-операции. Типичные виновники: N+1 queries, отсутствиеPromise.all, медленные внешние API без таймаутов. - Если memory issue: сделай heap snapshot до нагрузки, после нагрузки, сравни в Chrome DevTools. Ищи объекты, количество которых растёт. Типичные виновники: event listeners без
removeListener, кеши без TTL, замыкания, держащие большие объекты. - Если event loop lag: добавь мониторинг
perf_hooks.monitorEventLoopDelay({ resolution: 10 }), экспортируй в метрики. Если lag > 50ms — ищи синхронные операции черезclinic flame. Типичные виновники:fs.readFileSync,crypto.pbkdf2Sync, тяжёлые вычисления в main thread. - Примени фикс, измерь снова. Профилируй до и после — если латентность не упала, фикс не сработал. Не оптимизируй на глаз, только по данным.
- Добавь инструментацию. После фикса добавь
performance.mark/performance.measureвокруг критичного кода, экспортируй в мониторинг. Если проблема вернётся — увидишь сразу.
Подводные камни
- Не профилируй idle-процесс. Flamegraph без нагрузки покажет только event loop internals. Нужен реальный трафик.
- Не оптимизируй то, что уже быстро. Flamegraph показывает, где 80% времени. Не трать неделю на оптимизацию функции, которая занимает 2%.
- Async-проблемы не видны в CPU-профайлере. Если CPU низкий, а сервис медленный — это не CPU-проблема. Используй
bubbleprofили логируй время каждогоawait. - Clinic.js может не работать на новых версиях Node. Проект не активно поддерживается с 2023 года. Если clinic падает — используй
0xили встроенныйnode --prof. - Heap snapshot огромного процесса может съесть всю память. Если heap 2GB — snapshot будет 4GB+. Делай snapshot на staging с меньшим dataset.
Вывод
Профилирование Node.js — это не магия, а инженерный процесс. Clinic.js даёт быструю диагностику, 0x — простой flamegraph, pprof — интеграцию с облаком. Flamegraph читается просто: широкий блок = много времени, узкий = мало. Начинай с clinic doctor, следуй его рекомендациям, измеряй до и после фикса. Главное правило: профилируй под нагрузкой, оптимизируй по данным, а не по интуиции. Один час с flamegraph экономит неделю гаданий на кофейной гуще.
Когда в следующий раз увидишь CPU-спайк в Grafana — не гадай, запусти 0x. Flamegraph покажет виновника за пять минут. Это работает.