Node.js 内存管理踩坑记录

Node.js 的内存问题在生产环境经常遇到,尤其是处理大文件或大批量数据时。这里记录了我踩过的坑和解决方法。

V8 内存限制

64位系统默认堆内存约 1.4GB,32位只有 0.7GB。

查看当前限制:

1
2
const v8 = require('v8');
console.log(v8.getHeapStatistics());

输出:

1
2
3
4
5
{
"total_heap_size": 11304960,
"total_available_size": 2191369456,
"heap_size_limit": 2197815296
}

内存结构

1
2
3
4
5
6
Node.js 内存分布:
├─ 堆内存 (Heap)
│ ├─ 新生代 (New Space) ~32MB
│ └─ 老生代 (Old Space) ~1400MB
├─ 栈内存 (Stack) 1-2MB
└─ C++ 内存 (Buffer等) 不受V8限制

内存溢出报错

1
FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory

常见触发场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1. 一次性读大文件
const data = fs.readFileSync('large-file.zip'); // 几百MB

// 2. 大数据量处理
const hugeArray = [];
for (let i = 0; i < 10000000; i++) {
hugeArray.push({ id: i, data: new Array(1000).fill(i) });
}

// 3. 递归太深
function deep(n) {
if (n <= 0) return 0;
return n + deep(n - 1);
}
deep(100000); // 爆栈

调整内存限制

命令行方式:

1
2
node --max-old-space-size=4096 app.js   # 老生代4GB
node --max-old-space-size=8192 app.js # 老生代8GB

PM2 配置:

1
2
3
4
5
6
7
8
9
10
// ecosystem.config.js
module.exports = {
apps: [{
name: 'my-app',
script: './app.js',
node_args: '--max-old-space-size=4096',
instances: 4,
exec_mode: 'cluster'
}]
};

或者启动时:

1
pm2 start app.js --node-args="--max-old-space-size=4096"

环境变量方式:

1
2
export NODE_OPTIONS="--max-old-space-size=4096"
node app.js

内存优化实践

1. 大文件用流处理

错误示范:

1
2
// 一次性加载整个文件
const data = fs.readFileSync('huge-file.txt');

正确做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
const stream = fs.createReadStream('huge-file.txt', {
encoding: 'utf8',
highWaterMark: 64 * 1024 // 64KB 缓冲区
});

const rl = readline.createInterface({
input: stream,
crlfDelay: Infinity
});

rl.on('line', (line) => {
processLine(line); // 逐行处理
});

2. 大数据分批处理

错误示范:

1
2
3
4
5
6
async function processAll() {
const all = await db.query('SELECT * FROM huge_table');
for (const item of all) {
await process(item);
}
}

正确做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
async function processBatch(batchSize = 1000) {
let offset = 0;
while (true) {
const batch = await db.query(
'SELECT * FROM huge_table LIMIT ? OFFSET ?',
[batchSize, offset]
);
if (batch.length === 0) break;

await Promise.all(batch.map(process));

// 手动触发GC(可选)
if (global.gc && offset % 10000 === 0) {
global.gc();
}

offset += batchSize;
}
}

3. Buffer 处理二进制

Buffer 分配在 C++ 内存,不占用 V8 堆内存。

1
2
3
4
5
6
7
8
9
10
11
const chunks = [];
const stream = fs.createReadStream(filePath);

stream.on('data', (chunk) => {
chunks.push(chunk); // chunk 是 Buffer
});

stream.on('end', () => {
const result = Buffer.concat(chunks);
console.log('文件大小:', result.length);
});

4. 避免内存泄漏

泄漏1:全局缓存无限增长

1
2
3
4
5
6
7
8
9
10
11
12
// 错误
const cache = {};
function process(id, data) {
cache[id] = data; // 无限增长
}

// 正确:用 LRU
const LRU = require('lru-cache');
const cache = new LRU({
max: 500,
ttl: 1000 * 60 * 60 // 1小时过期
});

泄漏2:事件监听器没清理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 错误
emitter.on('data', (data) => {
res.write(data);
});
// 请求结束没移除监听器

// 正确
const onData = (data) => res.write(data);
emitter.on('data', onData);
req.on('close', () => {
emitter.off('data', onData);
});

// 或者直接用 once
emitter.once('data', (data) => res.write(data));

5. 优化数据结构

对象数组 vs TypedArray:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 内存占用高
const users = [];
for (let i = 0; i < 1000000; i++) {
users.push({ id: i, name: `user${i}`, age: 20 });
}
// 约 200MB

// 内存优化
class UserStore {
constructor(size) {
this.ids = new Uint32Array(size);
this.ages = new Uint8Array(size);
this.names = [];
}
}

内存监控

基础监控

1
2
3
4
5
6
7
8
9
10
11
12
13
const v8 = require('v8');

function logMemory() {
const usage = process.memoryUsage();
console.log(`RSS: ${(usage.rss / 1024 / 1024).toFixed(2)} MB`);
console.log(`Heap Used: ${(usage.heapUsed / 1024 / 1024).toFixed(2)} MB`);
console.log(`Heap Total: ${(usage.heapTotal / 1024 / 1024).toFixed(2)} MB`);

const heapStats = v8.getHeapStatistics();
console.log(`Heap Limit: ${(heapStats.heap_size_limit / 1024 / 1024).toFixed(2)} MB`);
}

setInterval(logMemory, 30000); // 每30秒输出

堆快照分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const heapdump = require('heapdump');

// 手动触发
app.get('/heapdump', (req, res) => {
const filename = `heap-${Date.now()}.heapsnapshot`;
heapdump.writeSnapshot(filename, (err) => {
if (err) return res.status(500).send('Failed');
res.send(`Snapshot: ${filename}`);
});
});

// 自动触发(超过1GB)
const THRESHOLD = 1024 * 1024 * 1024;
setInterval(() => {
if (process.memoryUsage().heapUsed > THRESHOLD) {
heapdump.writeSnapshot(`auto-heap-${Date.now()}.heapsnapshot`);
}
}, 60000);

用 Chrome DevTools 的 Memory 面板加载分析。

垃圾回收监控

1
2
3
4
5
# 启动时加 --trace-gc 参数
node --trace-gc app.js

# 启用手动 GC
node --expose-gc app.js
1
2
3
4
5
6
if (global.gc) {
setInterval(() => {
global.gc();
console.log('GC executed');
}, 30000);
}

生产环境配置

Docker

1
2
3
4
5
6
7
8
9
FROM node:16-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
ENV NODE_OPTIONS="--max-old-space-size=2048"
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "app.js"]
1
2
3
4
5
6
7
8
9
10
11
# docker-compose.yml
version: '3.8'
services:
app:
build: .
deploy:
resources:
limits:
memory: 2.5G
environment:
- NODE_OPTIONS=--max-old-space-size=2048

Kubernetes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: apps/v1
kind: Deployment
metadata:
name: nodejs-app
spec:
replicas: 3
template:
spec:
containers:
- name: app
image: myapp:latest
resources:
limits:
memory: "2Gi"
requests:
memory: "1Gi"
env:
- name: NODE_OPTIONS
value: "--max-old-space-size=1792"

健康检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
app.get('/health', (req, res) => {
const usage = process.memoryUsage();
const usedMB = usage.heapUsed / 1024 / 1024;
const totalMB = usage.heapTotal / 1024 / 1024;
const percent = (usedMB / totalMB) * 100;

if (percent > 90) {
res.status(503).json({ status: 'unhealthy', memory: percent.toFixed(2) + '%' });
} else {
res.json({ status: 'healthy', memory: percent.toFixed(2) + '%' });
}
});

// 内存告警
setInterval(() => {
const ratio = process.memoryUsage().heapUsed / process.memoryUsage().heapTotal;
if (ratio > 0.8) {
console.warn(`内存告警: ${(ratio * 100).toFixed(2)}%`);
}
}, 60000);

总结

Node.js 内存管理的经验:

  1. 知道限制:默认 1.4GB,不够用就用 --max-old-space-size 调整
  2. 流式处理:大文件用 Stream,别一次性加载
  3. 分批处理:大数据分批查、分批处理
  4. 防泄漏:注意事件监听、全局缓存、闭包
  5. 加监控:定时记录内存,超阈值时告警或打堆快照

做到这些,Node.js 服务在生产环境基本能稳得住。