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
| const data = fs.readFileSync('large-file.zip');
const hugeArray = []; for (let i = 0; i < 10000000; i++) { hugeArray.push({ id: i, data: new Array(1000).fill(i) }); }
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 node --max-old-space-size=8192 app.js
|
PM2 配置:
1 2 3 4 5 6 7 8 9 10
| 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 });
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));
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); });
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; }
const LRU = require('lru-cache'); const cache = new LRU({ max: 500, ttl: 1000 * 60 * 60 });
|
泄漏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); });
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 }); }
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);
|
堆快照分析
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}`); }); });
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
| node --trace-gc app.js
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
| 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.4GB,不够用就用
--max-old-space-size 调整
- 流式处理:大文件用 Stream,别一次性加载
- 分批处理:大数据分批查、分批处理
- 防泄漏:注意事件监听、全局缓存、闭包
- 加监控:定时记录内存,超阈值时告警或打堆快照
做到这些,Node.js 服务在生产环境基本能稳得住。