题目
设计高并发Node.js文件上传服务,支持大文件分片上传和断点续传
信息
- 类型:问答
- 难度:⭐⭐⭐
考点
Node.js流处理,异步并发控制,文件系统操作,错误处理与恢复,性能优化
快速回答
实现要点:
- 使用
multipart/form-data分片上传,前端将文件切割为固定大小块 - 服务端通过
fs.createWriteStream以流式写入分片文件,避免内存溢出 - 用Redis记录分片上传状态(文件ID、分片索引、MD5校验值)
- 合并文件时使用
fs.createReadStream管道流按序拼接 - 通过背压机制控制并发流量,结合
p-limit限制并行操作数
1. 核心原理
大文件上传需解决内存限制和网络中断问题:
- 分片上传:将文件切割为2-5MB的块,并行上传提升速度
- 断点续传:服务端记录已接收分片,客户端中断后仅需重传缺失分片
- 流式处理:Node.js的Stream API实现非阻塞I/O,处理GB级文件不占内存
2. 代码实现示例
分片上传接口 (Express.js):
const fs = require('fs');
const pipeline = util.promisify(stream.pipeline);
app.post('/upload-chunk', async (req, res) => {
const { fileId, chunkIndex, totalChunks } = req.body;
const chunkStream = req.files.chunk.data;
// 写入分片
const chunkPath = `./temp/${fileId}_${chunkIndex}`;
await pipeline(
chunkStream,
fs.createWriteStream(chunkPath)
);
// 记录到Redis
await redisClient.hSet(`file:${fileId}`, `chunk_${chunkIndex}`, 'completed');
res.status(200).json({ status: 'success' });
});文件合并接口:
app.post('/merge', async (req, res) => {
const { fileId, fileName } = req.body;
const finalPath = `./uploads/${fileName}`;
// 获取所有分片路径并排序
const chunks = await fsPromises.readdir(`./temp`);
const targetChunks = chunks
.filter(name => name.startsWith(fileId))
.sort((a, b) => {
return parseInt(a.split('_')[1]) - parseInt(b.split('_')[1]);
});
// 流式合并
const writeStream = fs.createWriteStream(finalPath);
for (const chunk of targetChunks) {
const readStream = fs.createReadStream(`./temp/${chunk}`);
await new Promise((resolve) => {
readStream.pipe(writeStream, { end: false });
readStream.on('end', resolve);
});
}
writeStream.end();
// 清理临时文件
await Promise.all(targetChunks.map(chunk =>
fsPromises.unlink(`./temp/${chunk}`)
));
});3. 最佳实践
- 内存优化:始终使用Stream API,避免
fs.readFile加载整个文件 - 并发控制:使用
p-limit限制并行分片处理数量(建议CPU核心数×2) - 数据一致性:
- 分片上传前计算MD5,合并时校验
- 使用数据库事务保证状态一致性
- 错误恢复:
- 为每个上传任务创建唯一ID
- Redis存储结构:
HSET file:1234 chunk_0 'completed' - 客户端查询缺失分片重传
4. 常见错误
- 内存泄漏:未处理背压导致Buffer堆积,需监听
drain事件 - 文件损坏:分片合并顺序错误,必须按索引排序后拼接
- 僵尸文件:未清理中断上传的临时分片,应添加定时任务清理超时文件
- 并发冲突:多进程同时写入同一文件导致崩溃,需用文件锁(
fs-extra的lockfile)
5. 扩展知识
- 流量整形:通过
stream.throttle限制上传带宽 - 分布式存储:分片存储到不同服务器(如MinIO),通过gRPC合并
- 前端优化:Web Worker计算文件hash,
File.slice()切割分片 - 云原生方案:AWS S3分片上传API(
CreateMultipartUpload)