首页 / 资讯中心 / 文章详情

基于WebUploader的大文件分块上传与断点续传方案

基于WebUploader的大文件分块上传与断点续传方案 ★ FEATURED ARTICLE
做内部系统的人应该都有过这种体验公司资料库要传几个GB的设计源文件千兆内网明明很快但传到一半某个分片卡住整个任务失败从头再来。我之前维护一个内部资料库系统时就被这个问题搞得焦头烂额最后是靠着WebUploader的分块上传加断点续传方案解决的这篇文章就把这套方案的完整思路、代码落地和踩坑记录全部梳理出来。WebUploader是百度开源的一个前端上传组件基于HTML5实现。它在不开启任何额外能力时就是一个普通的文件上传插件但一旦打开分块开关一个几十GB的文件就会被自动切成若干小块逐块上传。配合服务端做分块状态记录就能实现失败自动重试、重复分块跳过、刷新页面后继续传。如果你正在做后台管理、企业网盘、内部资料系统这类需要频繁处理大文件上传的场景这篇内容可以直接照着落地。简单交代一下项目背景我手上的系统是模拟的内部资料库主要面向一个设计团队日常要传的PSD、视频素材、压缩包单文件动辄5到20GB服务端用的是Node.js前端选型就是WebUploader。整个功能从零到可用前后踩了不少坑下面按模块拆开讲。1. 局域网大文件上传的痛不是带宽的问题是失败成本太高1.1 千兆内网的假象很多人觉得内网传文件根本不是问题。千兆甚至万兆局域网带宽比公网宽裕太多了传个大文件不就是几分钟的事吗但你要是真的传过几十GB的文件就会明白内网环境下的瓶颈从来不是带宽而是长时间传输过程中的可靠性。内网虽然带宽高但一个上百GB的文件需要连续传输很长时间中间任何一环出问题任务就可能失败。这里的风险点包括服务器磁盘的写入能力、内存和缓存波动、交换机偶尔丢包、防火墙对长连接的限制甚至可能是工位底下那根网线老化导致的不稳定。这些问题在传小文件时几乎感知不到但一旦传输时长进入小时级概率就被急剧放大。我当时做过一次验证用一个12GB的压缩包通过普通的HTTP表单方式直接上传。前30分钟一切正常到了90%多的时候服务器内存持续上涨最后进程重启上传失败。也就是说已经传了11GB瞬间全部作废只能从头开始。这种失败在局域网项目里反复出现逼着我去找更可靠的方案。1.2 表单上传的局限一次性买卖传统表单上传的方式要么把整个文件读入服务器内存要么依赖Web框架的临时文件机制处理。不管哪种方式都有几个绕不开的硬伤。第一文件读入内存意味着服务器要能承受整个文件的大小。12GB的文件就算服务器物理内存有32GBWeb框架在处理multipart格式时依然会产生多倍的内存占用非常危险这也是我上面那个12GB文件把进程搞崩的直接原因。第二整个上传是一个长连接请求中间一旦断开前面传输的所有数据都会丢失。Express、Tomcat、Apache这些Web服务默认都有请求超时限制内网虽然快但一个超大文件在规定时间内没能传完照样被服务端掐断。第三没有状态。失败就是失败客户端不知道从哪里继续服务端也无法告诉客户端“我已经有哪部分数据了”。没有状态就不可能谈续传。所以局域网场景下传输的可靠性远比极限速度更值得优先考虑。而要提高可靠性第一步就是分块。1.3 WebUploader在这个场景里的价值之所以选WebUploader是因为它恰好把分块上传需要的基础能力都内置了第一它能把大文件按固定大小切成若干分块每个分块是独立的HTTP请求。单个请求体只有几MB不会触发服务端大文件解析的逻辑内存压力骤减。第二它支持多分块并发上传合理利用内网带宽而不是靠一个长连接慢慢等。第三它提供了暂停、恢复、重试、进度回调等API我可以基于这些能力自己构建“跳过已上传分块”的续传逻辑。我习惯把它理解成“把一次豪赌拆成很多把稳赢的小赌局”。任何一个分块失败只需要重传那几MB而不是整个文件重新来一遍。分块是断点续传的基础没有分块服务端就无法精确记录“到底传到了哪里”。下面这部分就进入实现层面。2. 分块上传机制拆解chunked开关背后的处理链与参数选型2.1 一行配置开启分块WebUploader开启分块非常简单核心就是几个配置项var uploader WebUploader.create({ server: /api/upload/chunk, pick: #picker, chunked: true, chunkSize: 2 * 1024 * 1024, threads: 3, auto: false, compress: false });chunked置为true分块逻辑就生效了。chunkSize是每个分块的大小threads是同时传输的分块数。只要打开分块开关WebUploader就会自动把文件按chunkSize拆成多个分块并通过队列依次发送。分块之后每个请求发送的multipart数据里会自动带上几个关键字段chunk表示当前分块索引chunks表示总分块数name是原始文件名size是文件总大小。服务端完全可以根据这些参数判断当前分块属于哪个文件的哪一段。2.2 分块大小怎么定不要拍脑袋这里值得专门说清楚。WebUploader默认的chunkSize是2MB在局域网场景下1MB到5MB都是合理区间但具体取值需要权衡两个方面的成本。一个是重传成本。分块越小单个分块失败后重传的代价越低但请求次数会成倍增加。一个5GB的文件如果分块设为512KB就会有大约10000个请求。每个请求都有HTTP连接、消息解析等固定开销请求量过大时对服务器的压力反而明显。另一个是请求开销。分块越大总请求次数越少但任何一个分块出问题重传的数据量也更大。比如分块设为10MB一个100MB的分块失败重传就要重发10MB不够划算。我的实测经验是千兆内网、服务器磁盘写入正常情况下2MB到4MB最均衡。用3MB举例一个5GB文件大约1700个请求并发3个单个请求在几十毫秒内就能完成整体速度非常理想。这里可以用一个公式粗略估算总请求数总请求数 文件总大小 / 分块大小同样是5GB文件2MB分块会有2560个请求5MB分块只有1024个请求。两者在局域网里拉开的实际速度差距很小但请求越多服务端日志、中间件处理、错误重试的成本就越高。除非你的内网环境特别不稳定比如走Wi-Fi或者跨楼层无线否则我更建议把分块设置得稍微大一点比如4MB或5MB。2.3 并发数的取舍threads控制的是同时上传的分块数量。注意这里是分块级别的并发不是文件级别的并发。threads设为3或4在千兆内网已经能跑满不少带宽设大了比如10反而可能出现带宽争抢和分块乱序给服务端的合并逻辑增加不必要的压力。我的模拟项目里threads最终定在3配合4MB分块服务器的CPU和内存都保持平稳一个10GB的文件大约几分钟传完。期间我多次手动断开网络恢复后都能从断点继续整体表现稳定。2.4 分块上传时的关键事件实现过程中我主要用几个事件来跟踪分块状态uploader.on(uploadChunkSend, function(file, chunk) { console.log(准备发送分块, chunk.chunk); }); uploader.on(uploadChunkSuccess, function(file, chunk, response) { console.log(分块上传成功, chunk.chunk); }); uploader.on(uploadChunkError, function(file, chunk, response) { console.log(分块失败, chunk.chunk); });这三个事件是WebUploader暴露出来的分块级钩子。配合它们可以清楚感知每个分块的实时状态方便做错误计数和重试逻辑。uploadProgress事件则会根据分块完成情况自动计算整体进度百分比前端进度条可以直接绑定这个事件。3. 断点续传落地从文件指纹到服务端分块台账再到恢复逻辑3.1 文件唯一标识MD5是续传的钥匙分块上传解决的是“失败重传只重传小块”但还没有解决“刷新页面后接着传”。要实现后者必须有一个能唯一标识文件的东西让服务端认出“这是同一个文件”。最稳妥的方案是MD5。对大文件来说MD5计算通常需要几十秒具体耗时取决于文件大小和客户端机器性能。局域网场景下文件完全重复的概率不高只要计算出MD5再配合文件名和大小基本可以认为不可能重复。WebUploader官方提供了一个MD5插件即webuploader.md5.js用法很简洁uploader.on(fileQueued, function(file) { uploader.md5(file, { maxSize: 5 * 1024 * 1024 * 1024, prespic: function(frag) { // 计算进度可以显示在界面上 } }).then(function(val) { file.uniqueMd5 val; }); });MD5计算结果会异步返回并挂到file对象上。上传开始前把这个值传给服务端即可。实际项目里强烈建议把MD5计算的进度展示出来否则用户选中文件后看到界面半天没反应会以为程序卡死了。我在界面上加了一行“正在分析文件指纹”的提示体验立刻好了很多。3.2 服务端分块台账与合并逻辑断点续传的关键在服务端。我的做法是为每个文件的MD5建立一个分块目录记录哪些分块已经收到。说白了就是一张“分块台账”。接口设计上分为三块分别是分块接收、分块状态检查和分块合并。// 1. 分块接收POST /api/upload/chunk // 参数file(分块数据), chunk(索引), chunks(总数), md5(文件指纹), name(文件名) app.post(/api/upload/chunk, (req, res) { const { chunk, chunks, md5, name } req.body; const dir path.join(UPLOAD_DIR, md5); const chunkPath path.join(dir, chunk_${chunk}); // 将分块数据写入 chunkPath // 如果 chunkPath 已存在直接返回成功 res.json({ code: 0 }); }); // 2. 分块状态检查GET /api/upload/check // 参数md5, chunks // 返回已存在的分块索引列表例如 { uploaded: [0,1,2,3,5] } app.get(/api/upload/check, (req, res) { const { md5, chunks } req.query; const dir path.join(UPLOAD_DIR, md5); const uploaded []; for (let i 0; i Number(chunks); i) { if (fs.existsSync(path.join(dir, chunk_${i}))) { uploaded.push(i); } } res.json({ uploaded }); }); // 3. 合并POST /api/upload/merge // 参数md5, name // 按顺序读取所有分块拼接成完整文件 app.post(/api/upload/merge, (req, res) { const { md5, name } req.body; const dir path.join(UPLOAD_DIR, md5); const targetPath path.join(FINAL_DIR, name); // 从 chunk_0 到 chunk_N 依次写入 targetPath // 合并完成后删除分块目录 res.json({ code: 0, url: /download/ encodeURIComponent(name) }); });这三个接口逻辑都不复杂但有几个细节必须注意。分块目录的命名直接用MD5不要拼接文件名。因为不同用户可能上传同名的文件而且文件名在传输过程中可能经过转码或被修改。分块文件统一用chunk_0、chunk_1这种命名按索引排序合并顺序不会乱。合并时要校验分块完整性。我的做法是check接口返回已上传分块列表前端把所有缺失分块补传完再调用merge。服务端在merge时再检查一次分块数量是否等于chunks不满足就直接报错避免拼出一个缺块的不完整文件。另外局域网内多用户并发上传同一个文件的情况虽然不多但一旦出现MD5目录就会冲突。我处理的方式是在MD5后拼接一段随机串作为目录名上传完成后再按真实文件名落地从根上规避了并发目录冲突。3.3 前端恢复逻辑刷新页面后接着传刷新页面后怎么恢复完整流程是这样的用户重新选择同一个文件触发WebUploader的fileQueued事件。前端计算文件MD5。拿到MD5后调用check接口获取服务端已上传的分块列表。跳过已上传分块只上传缺失的部分。所有分块齐全后调用merge接口完成合并。第四步看起来简单实际操作中需要注意一个现实问题WebUploader内部的分块发送队列并没有提供“直接忽略某些分块”的公开参数。想要在前端精确跳过某个分块需要深入修改组件内部逻辑这种做法遇到组件升级很可能会失效维护成本很高。我最终没有选择前端精细跳过而是采用了一个更稳妥的方案服务端幂等。无论前端发来哪些分块服务端只要发现对应分块文件已存在就直接返回成功不重复写入。这样前端只需要负责“计算MD5、正常上传、偶尔主动查一下状态”已上传的分块会在请求到达服务端时被自动识别并跳过肉眼几乎无感。具体实现上把当前文件的MD5在上传开始时注入到统一表单参数里var currentMd5 null; uploader.on(fileQueued, function(file) { uploader.md5(file).then(function(val) { currentMd5 val; }); }); // 在开始上传当前文件时把当前文件的md5和文件名注入全局formData uploader.on(uploadStart, function(file) { uploader.option(formData, { md5: currentMd5, name: file.name, size: file.size }); });这里有一个细节因为formData是全局的如果多个文件同时上传后一个文件的MD5会覆盖前一个。我的处理方式是让同一时间只处理一个文件上传分块之间的并发不受影响因为同一文件的所有分块使用的MD5是一致的不会串。3.4 合并完成后的收尾合并成功后服务端要及时清理分块目录否则长时间运行会积累大量临时文件。我的清理策略是merge接口执行成功后立即删除分块目录另外再加一个定时任务删除超过24小时还没完成合并的分块目录防止用户传了一半就不回来把磁盘撑爆。这个点很容易被忽略但实际跑起来非常关键。另外merge接口需要做并发保护。两个请求同时触发合并或者合并期间又有新分块上传都可能出现读写冲突。我在服务端用一个全局集合记录正在合并的MD5合并期间如果再次收到同一个MD5的合并请求就直接拒绝保证同一时间只有一个合并任务。4. 局域网实测记录超时、内存与并发数的那些坑4.1 上传到100%但一直不触发合并这是整段开发里最诡异的坑。现象是界面显示所有分块都传完了进度条100%但服务端一直没有调用merge日志里也没有任何报错。排查下来发现问题出在分块数量和实际发送数量对不上。WebUploader对chunks的计算依赖于我设置的chunkSize前端按4MB切分但某些大文件的最后一个分块只有几十KB这个分块在排队时因为某些未知原因丢失了整体进度却显示100%。处理方法很简单前端不要只依赖uploadProgress事件判断完成在上传进度到90%以上时主动调用一次check接口核对已上传分块数量是否等于chunks。如果不相等把缺失分块重新加入队列补传。这个兜底逻辑比任何一个组件参数都重要。4.2 并发线程堆高内存一开始我图快把threads调到8结果服务器内存直线上升。原因很好理解8个分块同时写入磁盘每个分块读取都占用缓冲区再加上服务端框架解析multipart的开销内存压力成倍增加。降到3之后内存稳定在一个合理水位传输速度却没有明显下降。千兆局域网下WebUploader的多线程优势很容易被磁盘IO瓶颈抵消线程数并不是越高越好。我给本地测试的结果汇总了一个参考表并发数内存水位传输速度感受适用场景1很低较慢服务器配置很弱3稳定良好普通局域网服务器5-6偏高提升不明显磁盘写入性能较好的机器8以上明显上涨反而可能下降不推荐4.3 局域网里别忽略Nginx的body大小限制虽然分块后单个请求只有几MB但如果部署了Nginx做反向代理默认的client_max_body_size只有1MB会直接把分块请求挡在外面前端表现就是上传失败或者收到413错误。解决方式是在Nginx配置里调整一下client_max_body_size 10m;这个坑非常经典。分块大小如果设置成2MB或4MB默认的1MB限制就会拦下所有分块。很多人在局域网环境排查半天最后发现是Nginx把请求拒之门外。其他中间件如网关、负载均衡如果有类似的请求体大小限制也要一并检查。4.4 大文件指纹计算的耗时和体验前面提到MD5计算可能耗时较长。实测一个12GB文件MD5计算大约用时40多秒。计算期间上传按钮最好置灰或显示计算进度否则用户会反复点击造成不必要的困惑。更好的思路是把指纹计算和上传并行起来。具体做法是用户选中文件后先让它进入队列后台异步计算MD5算完再真正启动上传。这样用户感觉文件“已经排上了”而实际上上传动作是等指纹就绪后才开始整体体验流畅不少。5. 做完断点续传之后的加分项秒传与任务状态提示5.1 秒传原来这么简单断点续传的方案搭好之后秒传几乎是白送的。逻辑上就是一个分支如果check接口返回的已上传分块列表是完整的那就不需要再传输任何数据直接调用merge接口合并前端表现就是秒传。这个功能在内部资料系统里价值非常大。设计团队经常反复上传同一份素材有了MD5指纹同一个文件第二次上传直接秒传不仅节省时间还减少了服务器不必要的写入压力。而且秒传和断点续传使用的是同一套代码只是判断路径不同没有任何额外开发成本。5.2 任务状态提示让用户知道现在在干嘛断点续传做好了但用户体验还有一半取决于工作状态的可视化。建议至少展示这些信息正在计算指纹显示百分比进度正在上传显示整体百分比、已上传分块数和总分块数、实时速度分块失败重试显示重试次数上传完成显示“合并中”而不是直接跳转到成功页这些在前端都可以通过uploadProgress、uploadChunkError等事件拼出来。局域网里传输速度可能一瞬间拉满也可能突然卡住十几秒用户需要明确知道自己当前处在哪个阶段而不是对着一个静止的进度条干着急。5.3 暂停与取消的策略WebUploader本身支持stop()和upload()来实现暂停恢复但要注意stop()只是暂停发送后续分块已经在传输途中的分块会继续完成或超时。如果要做“真正的暂停”必须结合服务端的幂等逻辑让超时分块重试时直接被判定为已存在这样暂停和恢复才不会造成数据错乱。取消上传则是把文件从队列中移除并调用一个取消接口让服务端清理该文件对应的未完成分块目录。如果用户明确取消了一个1GB传了一半的文件服务端的临时分块要立刻清理避免长期占用磁盘空间。最后说几句实在的WebUploader的坑不算少文档更新滞后、新浏览器兼容性偶尔出问题这些都是实际会遇到的。但从整体架构和API设计来看它依然是内网系统做分块上传时的可靠选择。做完这个模拟项目之后我最大的体会是前端组件只是开始服务端设计才是核心。断点续传能不能做成关键看服务端的分块台账是否可靠前端只要把MD5算明白、把分块发出去任务其实已经完成了九成。如果后续要继续扩展我会从三个方向下手把分块上传的状态放进数据库便于多节点共享利用本地缓存记录已完成的分块索引进一步减少重复请求优化合并流程的IO路径避免合并大文件时二次读取写满磁盘缓存。到那时这套方案就不仅仅适用于局域网搬到公网环境做传输底座也完全站得住。
阅读完成 · 觉得有帮助?
咨询建站