Prometheus и Grafana
Свяжите process memory, Event Loop delay и лабораторную утечку с настоящим Prometheus endpoint и готовым Grafana dashboard.
Временная шкала
Разбираем: Prometheus и Grafana
Этот раздел можно читать до запуска опыта. После теории вернитесь к live trace и сопоставьте каждый шаг с реальным событием.
Метрика — это регулярно измеряемое число с метками и временем. Приложение публикует текущие значения, Prometheus периодически их забирает и хранит как временные ряды, а Grafana строит панели и помогает увидеть тренд. Ни Grafana, ни Prometheus сами по себе не исправляют утечку.
Лаборатория отдаёт Prometheus text exposition на `/api/metrics`: память главного процесса и memory child, Event Loop utilization/delay, активные запуски и ошибки. В optional Docker Compose Prometheus scrape-ит endpoint каждые пять секунд, а Grafana автоматически получает datasource и готовый dashboard.
Термины этого эксперимента
Сначала поймите слова — затем порядок выполнения.
Metric
Числовое измерение во времени; gauge меняется в обе стороны, counter обычно только растёт.
Time series
Последовательность samples одного metric name и уникального набора labels.
Scrape
HTTP-запрос Prometheus к metrics endpoint для получения текущих samples.
Cardinality
Число уникальных label combinations. userId/requestId в labels способны взорвать стоимость хранения.
SLI
Измеримый показатель качества сервиса, например доля успешных запросов или latency.
Alert
Правило над временным рядом, которое срабатывает при устойчивом условии, а не обязательно на одиночном sample.
Что происходит по шагам
Каждый шаг соответствует наблюдаемому состоянию runtime.
- 01Приложение измеряет
process.memoryUsage и perf_hooks дают runtime signals, а бизнес-код увеличивает counters.
- 02Endpoint публикует
`/api/metrics` возвращает HELP/TYPE и samples в Prometheus text format.
- 03Prometheus scrape-ит
Через равные интервалы он сохраняет значения с timestamp в локальную TSDB.
- 04PromQL вычисляет
rate(counter[window]), max_over_time и сравнения превращают samples в диагностические сигналы.
- 05Grafana визуализирует
Provisioned dashboard показывает память main/child, Event Loop delay, ELU и частоту запусков.
- 06Alert ведёт к расследованию
Устойчивый рост открывает runbook: workload, logs, snapshot безопасной реплики, retainer path, исправление.
Где результат требует оговорки
Эти детали объясняют, почему похожий код иногда даёт другой trace.
Heap slope важнее одного числа
Высокий стабильный heap может быть нормой; подозрителен новый растущий baseline после сопоставимых циклов нагрузки и GC.
Event Loop delay и CPU не взаимозаменяемы
Высокий delay может дать синхронный I/O или пауза GC, а CPU процесса включает работу вне главного Event Loop. Коррелируйте сигналы.
Counter читают через rate/increase
Абсолютное число запусков растёт с uptime. Для текущей интенсивности используйте rate на окне; restart counter учитывается Prometheus.
Labels должны быть ограниченными
mode или outcome имеют малую cardinality. URL с id, email, stack trace и requestId оставляйте логам/traces.
Локальный dashboard — не вся production-система
Для реального сервиса добавьте HTTP RED metrics, DB/queue pool saturation, cgroup memory, restarts/OOMKill, alert routing и retention policy.
Сначала разберитесь, какие части Node участвуют в выполнении.
Затем уберите служебные детали и рассмотрите только главную идею.
После этого сопоставьте модель с кодом, который создаёт live trace.
Минимальная модель без служебного кода
import { monitorEventLoopDelay } from 'node:perf_hooks';
const delay = monitorEventLoopDelay({ resolution: 20 });
delay.enable();
// Prometheus scrape:
// GET /api/metrics
// node_loop_lab_process_resident_memory_bytes 123456789
// node_loop_lab_event_loop_delay_p95_seconds 0.012
console.log(delay.percentile(95) / 1e6, 'ms');Полный код, который выполняет сценарий
Это не альтернативный пример: ниже показаны функции и файлы, используемые кнопкой запуска.
Код сформирован из реальной серверной функции. Для сценариев с отдельным процессом или Worker показаны все участвующие файлы.
import {
monitorEventLoopDelay,
performance,
} from 'node:perf_hooks';
const sleep = (ms) =>
new Promise((resolve) => setTimeout(resolve, ms));
function blockMainThread(durationMs) {
const startedAt = performance.now();
let iterations = 0;
// Намеренная блокировка для учебного сценария. В production так делать нельзя.
while (performance.now() - startedAt < durationMs) {
iterations += Math.sqrt((iterations % 10_000) + 1);
}
return Math.round(iterations);
}
async function observabilitySignals(emit) {
const histogram = monitorEventLoopDelay({ resolution: 10 });
histogram.enable();
const eluBefore = performance.eventLoopUtilization();
const memoryBefore = process.memoryUsage();
emit(
'metrics',
'sample',
`До нагрузки: RSS=${Math.round(memoryBefore.rss / 1024 / 1024)} MB, heapUsed=${Math.round(
memoryBefore.heapUsed / 1024 / 1024,
)} MB`,
);
emit(
'prometheus',
'info',
'Prometheus получает эти process/runtime/memory-lab ряды через GET /api/metrics',
);
// Даём monitorEventLoopDelay установить свой sampling timer до блокировки.
await sleep(35);
emit(
'event-loop',
'warning',
'Создаём короткую контролируемую блокировку, чтобы метрика delay получила сигнал',
);
blockMainThread(140);
await sleep(35);
const elu = performance.eventLoopUtilization(eluBefore);
const p95Ms = Number.isFinite(histogram.percentile(95))
? histogram.percentile(95) / 1e6
: 0;
const maxMs = Number.isFinite(histogram.max) ? histogram.max / 1e6 : 0;
histogram.disable();
emit(
'metrics',
'result',
`Event Loop: p95 delay=${p95Ms.toFixed(1)} мс, max=${maxMs.toFixed(
1,
)} мс, ELU=${(elu.utilization * 100).toFixed(1)}%`,
);
emit(
'grafana',
'result',
'Grafana не измеряет процесс сама: она строит панели по временным рядам, которые собрал Prometheus',
);
}Именно вызовы emit(...) превращаются в строки live trace. await и Promise удерживают HTTP-поток открытым до завершения сценария.
Популярные заблуждения
Миф слева, корректная модель справа.
Grafana собирает метрики приложения.
В этой схеме приложение публикует, Prometheus собирает и хранит, Grafana запрашивает и визуализирует.
Alert должен сработать при первом высоком sample.
Одиночный spike часто нормален; обычно задают окно, порог, duration и runbook.
RSS можно сложить с heapUsed и external.
RSS уже является общей резидентной картиной, а компоненты частично перекрываются.
В label полезно положить как можно больше контекста.
Неограниченные значения создают новые time series и могут перегрузить monitoring раньше приложения.
Красивый dashboard доказывает отсутствие утечки.
Он помогает заметить симптом. Причину подтверждают контролируемой нагрузкой, profiles/snapshots и retainer path.
Ответьте своими словами
Если ответ получается объяснить без терминов из документации, ментальная модель уже начала складываться.
- Как отличить полезно выросший bounded cache от memory leak по временным рядам?
- Почему requestId нельзя использовать как Prometheus label?
- Какой runbook должен открываться при росте heap и Event Loop delay?
- Каких HTTP-метрик пока не хватает этой учебной лаборатории для настоящего SLO?