资讯详情

资讯详情

大文件分片上传全解析:时序图、断点续传与工程实践

做上传功能做到后期基本都会碰到同一个问题文件稍微大一点一个请求直接传整个文件传着传着就断断了就重来用户心态崩后端日志刷屏。我自己接过几个大文件上传的需求最早也是拿单请求硬扛后来被现实教育老老实实改成了分片上传。而真正让我把这套逻辑彻底理顺的不是代码是那张分片上传时序图。时序图这个词在硬件领域也很常见比如 I2C 通信协议里画时钟和数据线的时序关系但在软件上传这个场景里时序图画的不是电平高低而是客户端、服务端、存储之间消息往来的先后顺序。分片上传时序图就是把“大文件怎么拆、怎么传、传完怎么合”的全过程用消息顺序固定下来动手写代码之前先让每一步交互、每一个返回值、每一条失败分支都摆在桌面上。这篇文章根据我在实际项目里画时序图、调接口、排查线上问题的经验把分片上传的核心流程、时序设计、关键参数和常见坑一次性讲清楚。不管你是后端要接这个功能还是前端要实现上传组件都可以拿它当参考。别小看这张图我见过太多项目因为没画时序图前后端联调时互相扯皮问题定位全靠翻日志。1. 分片上传的核心设计思路1.1 为什么要拆成片来传先捋一个基础问题为什么不直接一个 PUT 把整个文件丢给服务端很多人一开始就是这么干的小文件没毛病一旦遇到几百 MB 甚至几个 GB 的文件问题全出来了。网络不稳定导致连接中断整个文件作废上传没有进度概念用户看到的是一个漫长的等待条后端接收大文件时内存和负载都吃不消一两个并发就能把进程扛挂。这些问题不是靠优化代码就能解决的而是传输模型本身就不合适。分片上传的核心思想就是把这个大任务拆成多个可以独立完成的小任务。用搬家类比就是你有一屋子东西要搬不会叫一辆卡车一次性全拉走而是打成若干包裹分批运到了再组装。单包裹丢了就重寄那个包裹不用全屋重来。这个类比里包裹就是分片组装就是合并重寄包裹就是分片重试。这样做带来的直接收益有三个一是断点续传已经传成功的分片不需要重传二是并发加速多个分片可以同时上传充分利用带宽三是失败隔离单个分片失败只重试这一片代价极小。尤其是断点续传在移动端场景里几乎是刚需用户切个后台、过个隧道连接一断续传体验直接决定功能能不能用。1.2 标准流程只有四步不管用哪家云存储还是自己写文件服务分片上传的骨架基本一致就四个环节初始化上传任务客户端把文件名、文件大小、分片大小发给服务端服务端创建一条上传记录返回一个全局唯一的 uploadId。客户端本地切分文件按事先约定好的分片大小把文件切成从 1 到 N 的编号分片每个分片单独计算 MD5 等校验值。逐片上传客户端携带 uploadId 和 partNumber把分片数据传上去服务端负责存储并登记该分片已上传。通知合并所有分片上传完毕客户端调用合并接口服务端按 partNumber 顺序把分片拼回完整文件。这套流程最重要的是前两步的顺序不能搞反。先有 uploadId后面所有分片才能挂靠到同一个任务上先算好 chunkCount服务端才知道什么时候分片齐了。很多初版实现图省事跳过初始化直接上传分片结果合并时校验不了完整性后面全是坑。2. 时序图核心环节拆解2.1 画时序图到底画什么时序图Sequence Diagram在这个场景里就是把上面四步中的每一个消息往来都具象化。画的时候参与的角色一般是三个前端客户端、业务后端、文件存储层可能是对象存储也可能是本地磁盘服务。我习惯先把消息清单列出来再画线。分片上传的消息就五类createUpload、uploadPart、completeUpload、queryProgress、以及可能存在的取消任务。下面这张图是我在项目设计阶段画的简版时序图用文字方式呈现工具无关重点是消息顺序你可以对照着理解前端客户端 业务后端 文件存储/对象存储 | createUpload(fileName, size, chunkSize) | |-------------------| | | |--- 创建 uploadId ----| |-------------------| 返回 uploadId | | | | | uploadPart(uploadId, partNumber, data) | |-------------------| | | |--- 存储第N个分片 -----| |-------------------| 返回 partEtag | | ... 重复N次 ... | | | | | | completeUpload(uploadId, partList) | |-------------------| | | |--- 检查分片完整性 ----| | |--- 触发异步合并 ------| |-------------------| 返回合并中状态 | | | | | queryProgress(uploadId) 轮询 | |-------------------| | |-------------------| 返回合并结果/下载URL |这张图看着简单但它把三个关键点定死了所有分片必须带上 uploadId 才能被归属到同一个任务合并动作在分片全部到位之后才触发合并结果通过查询接口异步获取而不是在合并接口里同步等待。我在评审会上通常只讲这张图就能把前后端的职责边界说清楚。2.2 五类消息逐个讲透createUpload 这步前端提交文件名、文件大小、分片大小、分片数量后端生成 uploadId。注意这一步服务端一般不会真的去检查文件是否存在它只负责登记元数据。返回的时候除了 uploadId还应该带上当前已上传的分片列表方便后续断点续传。这个返回值的细节很多初版会漏但它是续传体验的起点。uploadPart 是并发压力最大的一步。每个分片请求要带 uploadId、partNumber、分片数据和校验值。服务端收到后存分片返回一个分片标识partEtag。partNumber 从 1 开始而不是从 0 开始这个细节很多规范里都这么定义主要是为了合并时排序直观、和人类计数习惯一致。别小看编号约定前后端如果一边从 0 数一边从 1 数合并出来的文件里就会莫名多一段空数据。completeUpload 触发之后服务端要做的第一件事不是合并而是校验。校验分片数量够不够每个分片是否存在大小合计对不对。校验通过了才进入合并。合并本身是重操作尤其是几个 GB 的文件一个同步请求从开始等到结束非常不靠谱所以几乎都是异步合并前端靠轮询 queryProgress 拿结果。queryProgress 的设计常常被忽略但它是断点续传的命门。前端不知道哪些分片失败了重新上传时如果全量重传分片的意义就少了一半。让前端随时能查到当前任务已上传的分片编号续传时跳过这些分片体验会好很多。这个接口不复杂一个列表查询而已但它决定了整套机制的上限。2.3 画图时最容易漏的两个分支我画时序图踩过最大的坑是只画成功路径不画失败分支。哪怕你消息顺序画得再准一旦某个环节失败整个图的逻辑就不完整了。至少有两个分支必须提前考虑。第一个是 uploadPart 失败后的重试分支。时序图上要明确前端收到网络异常或超时后对同一个 partNumber 重新发起 uploadPart服务端要做幂等处理。判断幂等的依据就是这个分片已经存在且校验值一致时直接返回已存的分片标识而不是报错。否则你重试一次存储里就多一份重复数据合并时就会出现内容错位。第二个是 completeUpload 之后合并任务失败的查询分支。合并是异步的失败后前端怎么知道所以要约定状态机上传中、合并中、成功、失败。前端轮询 queryProgress 时如果返回失败状态需要允许重新触发 completeUpload所以合并任务也要做成可重入的。这里的状态机定义就是在画时序图阶段定下来的写代码时照着状态机走基本不会乱。3. 实操过程与核心环节实现3.1 前端分片与并发控制前端主要负责两件事切分文件和调度上传。切分用 File 对象的 slice 方法就好浏览器原生能力不用引第三方库。核心变量就两个chunkSize 和 partNumber。比如一个 100MB 的文件按 4MB 一片切一共 25 片编号从 1 到 25。关键代码逻辑大概是这样的const CHUNK_SIZE 4 * 1024 * 1024; // 4MB const file fileInput.files[0]; const chunkCount Math.ceil(file.size / CHUNK_SIZE); for (let i 0; i chunkCount; i) { const start i * CHUNK_SIZE; const end Math.min(file.size, start CHUNK_SIZE); const blob file.slice(start, end); // blob 就是第 i1 个分片 // 计算 md5 后调用 uploadPart 接口 }上传调度有两个细节值得多说。一是并发数要控制。有人贪快一次性把 25 个分片全并发小带宽直接被塞满慢的请求把快的拖死。我一般控制在 3 到 5 个并发实测在普通家用宽带上既能跑满带宽又不容易触发服务端连接数上限。实现上就是用一个简单的计数器或者 Promise 池做并发限制代码量不大但效果非常明显。二是失败重试要做退避。单个分片失败后不要立刻重试等 1 秒、2 秒、4 秒这样递增重试超过 3 次就直接标记该分片失败让用户决定是否继续。无脑无限重试服务端压力会成倍放大而且重试太密集反而容易把已经拥塞的网络打得更死。我给前端同事的规范就一句话重试要退避失败要标记续传要查询。3.2 服务端接口设计与校验服务端接口设计建议至少四个初始化、上传分片、合并、查询进度。用表格把接口参数列清楚开发和联调会省很多事。接口方法关键参数返回值初始化POST /upload/createfileName、size、chunkSizeuploadId、已上传分片列表上传分片POST /upload/partuploadId、partNumber、file、md5partEtag合并POST /upload/completeuploadId、partListtaskId、状态查询进度GET /upload/progressuploadId已上传分片列表、合并状态服务端校验要做的几件事我按优先级排一下。第一partNumber 必须在 1 到 chunkCount 之间越界直接拒绝。这能挡掉大部分瞎传的请求。chunkCount 在初始化时落库就是为这一步准备的不需要每次请求再去文件系统里数分片。第二每个分片的 md5 要和客户端传的一致不一致说明传输过程中数据损坏这个分片要重传。大文件场景下 TCP 本身有校验但加上应用层校验更稳妥尤其是走公网的时候。MD5 计算对大文件有点性能压力但分片场景下每片 4MB计算代价完全可接受。第三合并之前必须校验已上传分片数量等于 chunkCount并且所有 partEtag 都存在。数量对不上就返回缺失分片列表前端按这个列表续传。这个校验逻辑放在合并接口里是最后一道防线不能省。3.3 合并策略与参数选择分片大小怎么定这是每个项目都要拍板的事没有绝对标准但有经验范围。我用过的组合里4MB 到 8MB 是性价比最高的区间局域网或内网场景可以放大到 16MB公网弱网环境建议用 1MB 到 2MB。为什么这么选分片太小比如 512KB分片数量膨胀每个分片都要建一次连接、传一次元数据服务端 IO 次数暴增反而拖慢整体速度。分片太大比如 64MB又回到单请求的老问题失败一次要重传的量太大。4MB 在内存占用、请求数量和重试粒度之间比较平衡后端临时目录也不会被单个文件撑爆。合并策略上我建议能流式合并就不要先落盘再合并。服务端读取所有分片按 partNumber 顺序写入最终文件边读边写避免把所有分片一次性加载进内存。实现上其实就是开一个输出流遍历分片路径逐个写入代码不复杂但内存占用是稳定的。我见过有人图省事把分片全部读到 ByteArray 里再一次性写文件文件一上 GB 就直接 OOM。还有一点非常关键合并完成之前临时分片不要急着删。万一合并过程中某个分片读取失败有了临时分片还能重新触发合并合并确认成功之后再用一个定时任务统一清理这样最稳。4. 常见问题与排查技巧实录4.1 上传到一半断了怎么续线上最常见的反馈就是传着传着断网了又要从头传。如果时序设计里没有 queryProgress这个问题就无解只能全量重传。有了查询接口流程就变成前端重新打开页面先调 createUpload 或者直接查询已有任务拿到已上传分片列表把每个分片状态标成已完成然后从第一个缺失分片继续传。这里有个小细节重新初始化时服务端应该支持按文件 MD5 和大小找已有任务的能力否则用户重新选同一个文件系统又不知道之前传过会重新生成一个 uploadId之前传的分片就成了僵尸文件。我每次设计这个功能都会把文件指纹查询一并做上省掉很多存储浪费。4.2 合并时提示分片缺失或校验失败合并失败的原因排在第一位的就是分片确实少传了。前端以为传完了但某个分片实际上因为网络问题丢在了路上只是请求重试逻辑没正确标记。这种问题用 queryProgress 一查就知道返回的缺失列表里是第几片前端对着补传就行。第二个常见原因是 MD5 不一致。公网环境偶发数据篡改或乱序客户端算的 md5 和服务端存完再算的对不上。排查手段是看服务端日志里该分片的大小和 md5 记录和客户端本地记录对比通常能定位是哪个环节出的问题。这类问题最怕前后端各说各话好在时序图上已经标明了校验的位置直接顺着图查就行。4.3 服务端临时空间被撑爆分片上传会把临时文件堆在服务端目录里如果只有上传没有清理磁盘迟早告警。这个问题一定要在架构上提前解决。我推荐的做法是每条上传任务落库时带一个创建时间定时任务扫描超过 24 小时、状态还不是已完成的任务直接删除临时分片和任务记录。另外单个任务的总大小也要设上限比如超过 10GB 直接拒绝初始化防止恶意请求把磁盘写满。4.4 从时序图反推问题排查线上问题的时候时序图是最好的参照物。我处理过一个诡异的上报用户说合并成功后下载的文件打不开一看日志合并时少了第 7 片。按照时序图反查发现是前端的并发上传里第 7 片失败了但重试逻辑因为 MD5 计算方式写错重传的其实是第 8 片的数据。如果没有时序图把每个步骤的消息和校验关系标清楚这种错位很难定位。我的排查套路是固定三步先查 uploadId 找到整条任务记录再看每个 partNumber 的分片是否存在且校验值匹配最后看合并任务的状态和日志。沿着时序图的线走问题基本都能落到具体某一个环节上而不是在日志里大海捞针。4.5 问题速查表现象可能原因处理方式上传进度一直卡在某个分片该分片请求超时未重试检查并发数和重试策略加入指数退避合并后文件大小不对分片编号错乱或缺失用 queryProgress 核对已上传分片列表合并接口超时同步等待合并完成改为异步合并 前端轮询查询状态磁盘空间骤降临时分片未清理加定时清理任务限制单任务大小上限重复上传产生僵尸文件未按文件指纹复用任务初始化时按 MD5 和大小查已有任务分片上传这件事代码复杂度其实远没有想象中高真正难的是流程设计。我在几个项目里反复打磨之后总结出一条经验画时序图的时候每个消息下面都问一句如果这一步失败了会怎样能排掉一大半线上问题。断点续传、幂等重试、异步合并这些特性都不是后来补上去的而是在时序图阶段就想清楚的。最后再分享一个小技巧把分片上传的接口设计成可查询、可重试、可幂等三原则。可查询指随时能拿到上传进度可重试指所有上传接口对重复请求都友好可幂等指同一分片传两次不会产生副作用。做到这三点这个功能基本就稳了。希望这篇文章能把你也带到这个思路上来至少在下次排大文件上传的坑时能少掉一些头发。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →