跳到主要内容

调试与性能分析

问题

如何调试 Node.js 程序?如何排查内存泄漏和性能问题?

答案

Node.js 提供了内置调试器、Inspector 协议和性能分析工具,结合 Chrome DevTools 可以高效排查问题。


调试方法

1. console 调试

// 基本输出
console.log('变量值:', variable);
console.dir(obj, { depth: null, colors: true });
console.table([{ a: 1 }, { a: 2 }]);

// 计时
console.time('操作耗时');
await someOperation();
console.timeEnd('操作耗时'); // 操作耗时: 123.456ms

// 堆栈跟踪
console.trace('调用栈');

// 条件断言
console.assert(value > 0, 'value 应该大于 0');

2. 使用 --inspect

# 启动调试模式
node --inspect src/index.js # 默认 9229 端口
node --inspect=0.0.0.0:9229 src/index.js # 指定地址
node --inspect-brk src/index.js # 首行暂停

# Chrome DevTools 调试
# 打开 chrome://inspect

3. VS Code 调试

// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Program",
"program": "${workspaceFolder}/src/index.ts",
"preLaunchTask": "tsc: build",
"outFiles": ["${workspaceFolder}/dist/**/*.js"]
},
{
"type": "node",
"request": "attach",
"name": "Attach to Process",
"port": 9229
}
]
}

4. 调试器命令

# 使用内置调试器
node inspect src/index.js

# 常用命令
# c / cont - 继续执行
# n / next - 下一步
# s / step - 进入函数
# o / out - 跳出函数
# bt - 调用栈
# repl - 进入 REPL 查看变量

性能分析(Profiling)

CPU 性能分析

# 生成 CPU profile
node --prof app.js

# 处理 profile 文件
node --prof-process isolate-*.log > processed.txt
// 使用 v8-profiler-next
import * as v8Profiler from 'v8-profiler-next';
import * as fs from 'fs';

// 开始采样
v8Profiler.startProfiling('CPU Profile');

// ... 执行代码 ...

// 停止并保存
const profile = v8Profiler.stopProfiling('CPU Profile');
profile.export((error, result) => {
fs.writeFileSync('./cpu-profile.cpuprofile', result!);
profile.delete();
});

内存分析

// 查看内存使用
const used = process.memoryUsage();
console.log({
rss: `${Math.round(used.rss / 1024 / 1024)} MB`, // 总内存
heapTotal: `${Math.round(used.heapTotal / 1024 / 1024)} MB`, // 堆总量
heapUsed: `${Math.round(used.heapUsed / 1024 / 1024)} MB`, // 已用堆
external: `${Math.round(used.external / 1024 / 1024)} MB`, // C++ 对象
arrayBuffers: `${Math.round(used.arrayBuffers / 1024 / 1024)} MB`
});
// 生成堆快照
import * as v8 from 'v8';
import * as fs from 'fs';

function takeHeapSnapshot() {
const snapshotFile = `./heap-${Date.now()}.heapsnapshot`;
const snapshotStream = v8.writeHeapSnapshot(snapshotFile);
console.log(`Heap snapshot written to ${snapshotStream}`);
}

// 定期触发 GC(需要 --expose-gc 标志)
if (global.gc) {
global.gc();
}

内存泄漏排查

常见内存泄漏场景

// 1. 全局变量累积
const cache: Map<string, any> = new Map();
function processRequest(id: string, data: any) {
cache.set(id, data); // 永远不清理
}

// 2. 闭包引用
function createHandler() {
const largeData = new Array(1000000).fill('x');
return () => {
// largeData 被闭包引用,无法回收
console.log(largeData.length);
};
}

// 3. 事件监听未移除
class DataProcessor {
constructor() {
process.on('data', this.handleData);
}

handleData = () => {
// 处理数据
};

// 忘记在销毁时移除监听器
destroy() {
process.off('data', this.handleData); // 必须移除
}
}

// 4. 定时器未清理
function startPolling() {
const timer = setInterval(() => {
fetchData();
}, 1000);

// 必须在适当时机清理
return () => clearInterval(timer);
}

排查方法

# 1. 多次生成堆快照
node --inspect app.js

# 2. Chrome DevTools -> Memory -> Take heap snapshot
# 3. 对比快照,找出增长的对象
// 使用 heapdump
import * as heapdump from 'heapdump';

// 收到信号时生成快照
process.on('SIGUSR2', () => {
const filename = `./heap-${Date.now()}.heapsnapshot`;
heapdump.writeSnapshot(filename, (err) => {
if (err) console.error(err);
else console.log(`Heap snapshot: ${filename}`);
});
});

// 发送信号: kill -USR2 <pid>

性能监控

使用 perf_hooks

import { performance, PerformanceObserver } from 'perf_hooks';

// 性能观察者
const obs = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry) => {
console.log(`${entry.name}: ${entry.duration}ms`);
});
});
obs.observe({ entryTypes: ['measure', 'function'] });

// 手动标记
performance.mark('start');
await doSomething();
performance.mark('end');
performance.measure('doSomething', 'start', 'end');

// 函数计时包装
const timedFunction = performance.timerify(originalFunction);
timedFunction(args);

APM 工具

// 使用 clinic.js
// npm install -g clinic

// 诊断命令
// clinic doctor -- node app.js
// clinic bubbleprof -- node app.js
// clinic flame -- node app.js

常见面试问题

Q1: 如何排查 Node.js 内存泄漏?

答案

步骤

  1. 监控内存:观察 process.memoryUsage() 持续增长
  2. 生成堆快照:在不同时间点生成多个快照
  3. 对比分析:在 Chrome DevTools 中对比快照差异
  4. 定位问题:找出持续增长的对象和引用链
  5. 修复验证:修复后重新监控确认
// 监控脚本
setInterval(() => {
const used = process.memoryUsage();
console.log(`Heap: ${Math.round(used.heapUsed / 1024 / 1024)} MB`);

// 超过阈值报警
if (used.heapUsed > 500 * 1024 * 1024) {
console.warn('Memory usage high!');
}
}, 5000);

常见原因

  • 全局缓存无限增长
  • 事件监听器未移除
  • 闭包持有大对象引用
  • 定时器未清理

Q2: Node.js 如何分析 CPU 密集型问题?

答案

# 1. 使用 --prof 生成 profile
node --prof app.js
node --prof-process isolate-*.log > processed.txt

# 2. 使用 clinic flame 生成火焰图
clinic flame -- node app.js

# 3. Chrome DevTools CPU Profiler
node --inspect app.js
# 在 DevTools 中录制 CPU profile
// 识别阻塞事件循环的操作
import { monitorEventLoopDelay } from 'perf_hooks';

const h = monitorEventLoopDelay({ resolution: 20 });
h.enable();

setInterval(() => {
console.log({
min: h.min / 1e6, // ms
max: h.max / 1e6,
mean: h.mean / 1e6,
p99: h.percentile(99) / 1e6
});
}, 5000);

Q3: 如何优化 Node.js 性能?

答案

方向优化方法
I/O使用 Stream、批量操作、连接池
CPUWorker Threads、避免同步操作
内存及时释放、使用 Buffer 复用、WeakMap
网络Keep-Alive、压缩、CDN
代码缓存、避免重复计算、减少闭包
// 使用连接池
import { Pool } from 'pg';

const pool = new Pool({
max: 20, // 最大连接数
idleTimeoutMillis: 30000
});

// 使用 Stream 处理大文件
import { createReadStream, createWriteStream } from 'fs';
import { pipeline } from 'stream/promises';
import { createGzip } from 'zlib';

await pipeline(
createReadStream('input.txt'),
createGzip(),
createWriteStream('output.txt.gz')
);

Q4: process.nextTick 和 setImmediate 在调试时有什么区别?

答案

// 观察执行顺序
setImmediate(() => console.log('1. setImmediate'));
process.nextTick(() => console.log('2. nextTick'));
Promise.resolve().then(() => console.log('3. Promise'));

// 输出顺序:
// 2. nextTick
// 3. Promise
// 1. setImmediate

// process.nextTick 可能阻塞 I/O
function recursiveNextTick() {
process.nextTick(recursiveNextTick); // 危险!阻塞事件循环
}

// setImmediate 更安全
function recursiveImmediate() {
setImmediate(recursiveImmediate); // I/O 可以在间隙执行
}

Q5: 如何监控 Node.js 应用的健康状态?

答案

// 健康检查端点
app.get('/health', (req, res) => {
const health = {
uptime: process.uptime(),
memory: process.memoryUsage(),
status: 'OK',
timestamp: Date.now()
};

res.json(health);
});

// 更详细的健康检查
import { monitorEventLoopDelay } from 'perf_hooks';
import os from 'os';

const h = monitorEventLoopDelay({ resolution: 20 });
h.enable();

function getMetrics() {
return {
// 进程指标
process: {
uptime: process.uptime(),
memory: process.memoryUsage(),
cpu: process.cpuUsage()
},
// 事件循环延迟
eventLoop: {
min: h.min / 1e6,
max: h.max / 1e6,
mean: h.mean / 1e6,
p99: h.percentile(99) / 1e6
},
// 系统指标
system: {
loadAvg: os.loadavg(),
freeMemory: os.freemem(),
totalMemory: os.totalmem()
}
};
}

相关链接