У високонавантажених промислових середовищах класичні методи налагодження через надмірне логування (console.log) або підключення важких інтерактивних дебагерів є категорично неприпустимими. Спроба підключення дебагера з точками зупину (Breakpoints) заморожує однопотоковий цикл подій (Event Loop), що викликає негайний збій перевірок готовності контейнерів (Kubernetes Liveness Probes) та лавиноподібний перезапуск подів у кластері.
Діагностика під реальним навантаженням спирається на неінвазивні методи з мінімальними накладними витратами (Overhead менше 2%):
Doctor, Flame, Bubbleprof), що використовує внутрішні хуки Node.js (async_hooks, perf_hooks) для зіставлення затримок введення-виведення, затримок Event Loop та активності системних дескрипторів.lldb та плагіна llnode інженер реконструює точний стек викликів JavaScript та стан змінних мертвого процесу.Інструменти діагностики поділяються за етапами життєвого циклу процесу: моніторинг живого інстансу в режимі онлайн та посмертний аналіз аварійних дампів пам'яті.
[ Інстанс Node.js у Production ]
|
+---------------+---------------+
| (Процес працює) | (Аварійний крах / SegFault)
v v
[ Агент діагностики ] [ Ядро Linux ]
- node:inspector - ulimit -c unlimited
- v8.writeHeapSnapshot - kernel.core_pattern
| |
+---------------+ v
| | [ Файл core.[pid] ]
v v |
[ Профіль CPU ] [ Знімок купи ] v
(.cpuprofile) (.heapsnapshot) [ lldb + llnode ]
| | |
v v v
[ Clinica Flame / Chrome DevTools ] [ Реконструкція стека та пам'яті ]Анатомія флеймграфа (Flamegraph):
v8.writeHeapSnapshot()) за сигналом SIGUSR2 з інтервалом в 1 годину та диференційний аналіз у Chrome DevTools для виявлення невідписаних слухачів подій.lldb/llnode під час краху модулів обробки зображень (sharp / libvips) через пошкодження вказівників пам'яті.Clinica Bubbleprof для виявлення повільних ланцюжків неоптимізованих запитів до СУБД, що фрагментують асинхронний потік виконання.Промислова діагностика базується на вбудованому агенті керування сесіями node:inspector та потоковому збереженні знімків пам'яті на диск.
import inspector from "node:inspector";
import fs from "node:fs";
import path from "node:path";
import v8 from "node:v8";
export class ProductionDiagnosticsAgent {
private session: inspector.Session | null = null;
private isProfiling = false;
private readonly storageDir: string;
constructor(storageDir = "/tmp/diagnostics") {
this.storageDir = storageDir;
if (!fs.existsSync(this.storageDir)) {
fs.mkdirSync(this.storageDir, { recursive: true });
}
}
/**
* Запуск статистичного профілювання CPU на заданий час без перезапуску інстансу
*/
public async captureCpuProfile(durationSeconds = 10): Promise<string> {
if (this.isProfiling) {
throw new Error("CPU profiling session is already in progress");
}
this.isProfiling = true;
const session = new inspector.Session();
this.session = session;
session.connect();
return new Promise<string>((resolve, reject) => {
// Активація профайлера V8
session.post("Profiler.enable", (err) => {
if (err) {
this.cleanupSession();
return reject(err);
}
// Встановлення інтервалу семплування у мікросекундах (1000 мкс = 1 мс)
session.post("Profiler.setSamplingInterval", { interval: 1000 }, (intervalErr) => {
if (intervalErr) {
this.cleanupSession();
return reject(intervalErr);
}
// Старт запису семплів викликів
session.post("Profiler.start", (startErr) => {
if (startErr) {
this.cleanupSession();
return reject(startErr);
}
console.info(`[Diagnostics] CPU Profiling started for ${durationSeconds}s...`);
// Очікування тривалості запису
setTimeout(() => {
session.post("Profiler.stop", (stopErr, { profile }) => {
this.cleanupSession();
if (stopErr) {
return reject(stopErr);
}
const filename = `cpu-profile-${process.pid}-${Date.now()}.cpuprofile`;
const fullPath = path.join(this.storageDir, filename);
fs.writeFileSync(fullPath, JSON.stringify(profile));
console.info(`[Diagnostics] CPU Profile saved to ${fullPath}`);
resolve(fullPath);
});
}, durationSeconds * 1000);
});
});
});
});
}
/**
* Створення моментального знімка купи (Heap Snapshot) із потоковим записом
*/
public captureHeapSnapshot(): string {
const filename = `heap-snapshot-${process.pid}-${Date.now()}.heapsnapshot`;
const fullPath = path.join(this.storageDir, filename);
console.info(`[Diagnostics] Writing Heap Snapshot to ${fullPath}...`);
// Нативний метод V8: скидає дані пам'яті безпосередньо на диск через шар C++ без алокацій у купі JS
v8.writeHeapSnapshot(fullPath);
console.info(`[Diagnostics] Heap Snapshot completed: ${fullPath}`);
return fullPath;
}
private cleanupSession(): void {
if (this.session) {
this.session.post("Profiler.disable");
this.session.disconnect();
this.session = null;
}
this.isProfiling = false;
}
}import { ProductionDiagnosticsAgent } from "./diagnostics-agent";
export function initializeDiagnosticsSignals(): void {
const agent = new ProductionDiagnosticsAgent();
// Сигнал SIGUSR2: зняття Heap Snapshot на вимогу без зупинки обробки запитів
process.on("SIGUSR2", () => {
try {
console.warn(`[Process ${process.pid}] Received SIGUSR2: triggering Heap Snapshot`);
const snapshotPath = agent.captureHeapSnapshot();
console.warn(`[Process ${process.pid}] Heap Snapshot ready: ${snapshotPath}`);
} catch (error) {
console.error("[Diagnostics] Failed to capture snapshot on SIGUSR2:", error);
}
});
console.info(`[Diagnostics] Signal listeners attached to PID ${process.pid}. Send SIGUSR2 for Heap Snapshot.`);
}Процес отримує сигнал SIGSEGV або фатальну помилку під прапорцем --abort-on-uncaught-exception.
Ядро ОС зберігає віртуальну пам'ять процесу у файл за шаблоном kernel.core_pattern.
Запуск lldb із вказанням бінарного файлу node та згенерованого core-файлу.
Завантаження плагіна llnode для інтерпретації структур пам'яті C++ у JavaScript типи.
Виконання v8 bt для реконструкції стека викликів JavaScript та інспекції аргументів.
FROM node:20-bookworm-slim
# Встановлення системних залежностей для збірки llnode та відладчика LLDB
RUN apt-get update && apt-get install -y \
lldb \
gdb \
python3 \
python3-pip \
make \
g++ \
curl \
&& rm -rf /var/lib/apt/lists/*
# Встановлення llnode для інспекції структур V8
RUN npm install -g llnode --unsafe-perm
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
# Створення директорії для збору Core Dumps із правами запису
RUN mkdir -p /var/cores && chmod 777 /var/cores
# Прапорці середовища Node.js:
# --abort-on-uncaught-exception: примусове створення Core Dump при необроблених помилках
# --diagnostic-report-on-fatalerror: одночасне формування звіту в JSON
ENV NODE_OPTIONS="--abort-on-uncaught-exception --diagnostic-report-on-fatalerror --diagnostic-report-path=/var/cores"
CMD ["node", "dist/main.js"]Використання застарілих пакетів на кшталт heapdump, що буферизують знімок купи всередині масивів JavaScript перед збереженням на диск. Якщо процес уже використовує 1.6 ГБ із виділеного ліміту 2 ГБ, буферизація знімка потребуватиме додатковий 1 ГБ RAM. Ядро Linux знищує контейнер через OOM-Killer до завершення запису. Рішення: використовувати виключно нативний метод v8.writeHeapSnapshot(), що виконує потокову серіалізацію на диск на рівні C++ без навантаження на купу JS.
Виконання clinica bubbleprof у продакшені при 5000 RPS. Bubbleprof перехоплює кожну асинхронну операцію через async_hooks. За високої щільності мікрозадач накладні витрати на відстеження контекстів збільшують затримку додатку на 30–50%, провокуючи деградацію сервісу. Інструмент призначений для staging-середовищ або точкового ізольованого аналізу канареєчного поду.
Зняття профілю CPU в збірці, де код TypeScript мініфіковано без збереження Source Maps (source-map-support). У флеймграфі інженер бачить тисячі безіменних викликів виду e(t, r), що унеможливлює локалізацію проблемного фрагмента коду. Завжди зберігайте зрозумілі ідентифікатори функцій або підключайте приховані sourcemaps.
У переважній більшості стандартних оточень Docker та Kubernetes розмір core-файлів за замовчуванням обмежений нулем (ulimit -c 0). У разі аварії Segmentation Fault операційна система не створить файл дампу. Для увімкнення генерації необхідно явно додати конфігурацію ulimits: { core: -1 } у маніфест запуску контейнера.
Фіксація сплеску Event Loop Delay або навантаження CPU за метриками Prometheus/Grafana
Запуск 10-секундного статистичного профілювання CPU через ProductionDiagnosticsAgent без рестарту
Експорт .cpuprofile у Chrome DevTools або Clinica Flame для пошуку вершинних плато
Зняття парних Heap Snapshots з інтервалом в 1 годину для виявлення конструкторів, що витікають
Оптимізація коду та верифікація нормалізації затримок p99 на дашбордах