最近在项目中遇到一个棘手问题:当使用archiver压缩几个GB的日志文件时,Node.js进程内存占用直接飙到上限然后崩溃。排查后发现是流式处理不当导致的内存泄漏,这也是许多中高级开发者常踩的坑。今天我们就来深度剖析archiver的流处理机制,分享几种经过实战验证的优化方案。
先看一个典型的内存泄漏案例:
javascript复制const archive = archiver('zip');
archive.pipe(fs.createWriteStream('output.zip'));
// 危险操作:直接读取大文件到内存
const data = fs.readFileSync('huge-file.log');
archive.append(data, { name: 'file.log' });
archive.finalize();
这种写法会把整个文件内容加载到内存,当文件大于Node.js默认内存限制时就会崩溃。其根本原因在于没有正确利用Node.js的流式处理能力。
| 输入方式 | 内存占用 | 适用场景 | 风险提示 |
|---|---|---|---|
fs.readFileSync |
高 | 小文件(<10MB) | 大文件会导致OOM |
fs.createReadStream |
低 | 大文件 | 需正确处理流事件 |
archive.file() |
最低 | 文件路径已知 | 内部自动处理流 |
关键发现:archive.append(buffer)的内存开销是archive.file()的3-5倍(实测2GB文件前者占用2.1GB内存,后者仅400MB)
这是最推荐的优化方案:
javascript复制const archive = archiver('zip', {
zlib: { level: 6 } // 适度压缩级别
});
archive.pipe(output);
// 最优写法:直接传入文件路径
archive.file('/path/to/large-file.log', {
name: 'compressed.log'
});
archive.on('progress', (progress) => {
console.log(`处理进度: ${progress.fs.processedBytes}/${progress.fs.totalBytes}`);
});
await archive.finalize();
注意:
archive.file()内部会自动创建文件流,比手动创建readStream更高效
当需要特殊处理文件内容时:
javascript复制const readStream = fs.createReadStream('large-file.log');
const transformStream = new Transform({
transform(chunk, encoding, callback) {
// 在这里进行流式数据处理
callback(null, chunk);
}
});
readStream
.pipe(transformStream)
.on('error', (err) => console.error('转换出错', err))
.pipe(archive.createOutputStream({ name: 'file.log' }))
.on('error', (err) => console.error('压缩出错', err));
对于包含数千个小文件的场景:
javascript复制// 使用glob模式批量添加
archive.glob('logs/**/*.log', {
cwd: __dirname,
ignore: ['**/skip-this/*']
});
// 设置并发限制
archive.setConcurrency(10);
archive.on('entry', (entry) => {
if (entry.stats.size > 100 * 1024 * 1024) {
console.warn(`大文件警告: ${entry.name}`);
}
});
javascript复制const archive = archiver('zip');
// 内存监控
setInterval(() => {
const usage = process.memoryUsage();
if (usage.heapUsed > 500 * 1024 * 1024) {
archive.setConcurrency(5); // 自动降低并发
}
}, 1000);
// 压缩级别动态调整
function getOptimalLevel(fileSize) {
return fileSize > 1024 * 1024 * 1024 ? 3 : 6;
}
对于超大文件(>10GB):
javascript复制const chunkSize = 500 * 1024 * 1024; // 500MB分片
let chunkIndex = 0;
function compressChunk(startByte) {
const endByte = startByte + chunkSize - 1;
const range = `bytes=${startByte}-${endByte}`;
const stream = fs.createReadStream('huge-file.bin', {
start: startByte,
end: endByte
});
archive.append(stream, {
name: `part-${chunkIndex++}.bin`,
range
});
stream.on('end', () => {
if (endByte < fileSize) {
compressChunk(endByte + 1);
} else {
archive.finalize();
}
});
}
在一次电商日志归档项目中,我们遇到了这样的问题:
archive.glob()配合setConcurrency(20)另一个常见误区是忽略错误处理:
javascript复制// 必须处理的错误事件
archive.on('error', (err) => {
console.error('归档失败:', err);
process.exitCode = 1;
});
output.on('error', (err) => {
console.error('写入失败:', err);
archive.abort();
});