2GB 视频传到 99% 断了又要重传?vibe 项目的分片上传与断点续传实战
2GB 视频传到 99% 断了不该从头再传:分片协议怎么定、前端怎么切怎么传、服务端怎么合、秒传怎么做、三层失败处理怎么做,附上线前检查清单。

2GB 视频传到 99% 断了,又要从头再传?分片上传就是为这个而生的
几乎每个做内容类、工具类 vibe 项目的人都会撞上同一堵墙:用户要传一个几百 MB 的视频、一个设计源文件、一个数据包,进度条走到 99% 时网络抖了一下,上传失败,一切归零。用户骂一句就走了,再也不回来。问题不在用户网络,也不在服务器带宽,而在于上传方式本身:一次 HTTP 请求传整个文件,任何一次中断都意味着全部重来。分片上传就是把这个“全有或全无”改成“每次只丢一小片”,断点续传就是让中断之后从断掉的那片继续,而不是从头开始。
本文讲的是怎么给 vibe 项目做一套可用的分片上传与断点续传,覆盖分片设计、前端实现、服务端合并、秒传、失败处理与安全。范围先划清楚:本站已有的《文件上传实战》讲的是基础上传怎么不被传图撑爆服务器,《文件存储与 CDN 实战》讲的是预签名直传与存储三层架构,《图片处理管线实战》讲的是传完之后的图片处理。本篇只讲大文件场景下的分片、续传与合并,不重复基础上传的内容。以下示例均为虚构的教学示例,应用名、数字与代码片段均为为讲解而构造的简化版本。

一、什么时候需要分片:先算账,再动手
分片不是免费的,它带来分片记录、合并步骤、清理任务和更多的失败形态。小文件用分片纯属自找麻烦。决策之前先算三笔账。
| 场景 | 文件大小 | 推荐做法 |
|---|---|---|
| 头像、配图、普通附件 | 10MB 以下 | 单次上传就够了,别上分片 |
| 短视频、设计稿、数据包 | 10MB–500MB | 分片上传,支持断点续传 |
| 长视频、备份包、数据集 | 500MB 以上 | 分片上传 + 秒传 + 后台合并 |
| 弱网、移动端、跨国上传 | 任何大小 | 分片尺寸调小,并发数调低 |
决定做分片之前,还要想清楚它带来的额外成本。分片上传至少多出三个需要长期维护的东西:服务端的上传会话记录、分片的临时存储、以及定期清理孤儿数据的定时任务。团队只有一个人时,这些维护工作都要算进开发排期里,不能只算写代码的那几天。如果产品里的最大文件长期在 50MB 以下,或者用户基本都在稳定的宽带环境下上传,把单次上传的超时时间放宽、加一次整体重试,往往比上分片更划算。分片是为大文件和弱网准备的,不是所有上传的默认答案。
还有两个信号说明你已经拖不起了。第一,客服或反馈里出现“上传失败”“传到一半没了”这类抱怨,而且集中在大文件上。第二,服务器日志里出现大量接近完成又中断的上传请求,带宽花了,文件一个没落下来。两个信号出现一个,就可以动手了。
二、分片协议设计:五个决策一次定好
动手写代码之前,先把协议定下来。分片上传的协议很小,但五个决策一旦定错,后面处处是补丁。
第一,分片大小。常见选择是 5MB 或 10MB。分片太小会导致请求数爆炸,一个 2GB 文件切成 1MB 的片要发 2000 个请求,光握手就拖垮速度;分片太大则断点续传的收益变小,断一次要重传一大片。起步建议 5MB,弱网场景可以降到 2MB,内网或专线场景可以升到 10MB。分片大小要做成可配置的,不要写死在代码里。
第二,分片编号。用从 0 开始的连续整数编号。编号是合并时排序的依据,也是续传时判断“哪些片已经有了”的依据。不要用时间戳或随机值做分片标识,排序和对账都会变麻烦。
第三,上传会话标识。一次上传任务需要一个 uploadId,由服务端在初始化时发给前端,之后每个分片请求都带上它。uploadId 同时也是服务端存放临时分片的目录名或键前缀。上传完成或过期后,服务端按 uploadId 清理临时数据。
第四,校验方式。每个分片都要有校验值,常用的是 MD5 或 SHA-256。前端算出分片哈希随请求发过去,服务端收完分片后验算,对不上就要求重传这一片。整个文件也需要一个总哈希,在合并后校验一次,防止“每个分片都对、合起来错了”的情况。哈希计算放在 Web Worker 里做,别阻塞主线程。
第五,状态查询接口。前端在续传之前,要先问服务端“这个 uploadId 已经收到了哪些分片”,只补传缺失的片。这个接口是断点续传的核心,没有它,续传就无从谈起。服务端按 uploadId 查已收分片编号列表返回即可。
三、前端实现:切片、并发、进度、暂停
前端的核心是 File 对象的 slice 方法,浏览器原生支持,不需要任何库。把文件按分片大小切开,逐片上传,同时控制并发数。并发数一般取 3 到 5,太高会互相抢带宽、反而更慢,太低则速度上不去。进度条按“已确认收到的分片字节数 / 文件总字节数”来算,不要按“已发出的请求数”来算,否则失败重传会让进度条倒退或超过 100%。
// 前端分片上传的最小骨架(简化示例)
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
async function uploadFile(file) {
const { uploadId } = await initUpload({ name: file.name, size: file.size });
const total = Math.ceil(file.size / CHUNK_SIZE);
const done = new Set(await queryUploaded(uploadId)); // 续传:先问哪些片已有了
const queue = [];
for (let i = 0; i < total; i++) {
if (done.has(i)) continue;
queue.push(i);
}
await runWithConcurrency(queue, 4, async (index) => {
const chunk = file.slice(index * CHUNK_SIZE, (index + 1) * CHUNK_SIZE);
await uploadChunk(uploadId, index, chunk); // 失败时内部重试 3 次
});
await completeUpload(uploadId, total);
}
暂停、取消和续传是同一套机制的三种表现。暂停就是停止发新的分片请求,已经发出的等它完成;取消是在暂停的基础上告诉服务端删除临时分片;续传是重新初始化同一个任务,先查已收分片,再补传缺失的片。关键是刷新页面后还能续传:把 uploadId、文件名、文件大小和总哈希存在 localStorage 里,下次用户重新选择同一个文件时,先算总哈希,命中已有的上传任务就直接续传,而不是新建任务。
还有三个体验细节。第一,上传过程中不要锁死页面,允许用户继续浏览其他页面,上传状态放在一个常驻的小浮条里。第二,大文件上传前先做一次本地校验:文件类型、大小上限、磁盘空间提示,不合格的在本地就拦住,别等传了一半才报错。第三,进度显示要诚实:把“已上传字节数、总字节数、当前速度、预计剩余时间”一起给出来,用户能忍受慢,不能忍受不知道还要等多久。
四、服务端实现:接收、合并与清理
服务端有四件事:初始化、接收分片、合并、清理。初始化时生成 uploadId,记录文件名、总大小、分片总数和总哈希,返回给前端。接收分片时按 uploadId 和编号存放,验算分片哈希,对不上返回错误让前端重传这一片,不要把坏片留下来。合并时按编号顺序拼接所有分片,验算总哈希,通过后把文件移到正式存储,更新数据库记录。清理是最后一步也是最容易被忘的一步:合并完成后删除临时分片,上传任务过期后也要删除。
分片的临时存放有两种做法。一种是存在对象存储里,按 uploadId 做前缀,分片就是一个个小对象,合并时由服务端读出来拼接,或者直接用对象存储自带的分片上传能力。另一种是存在服务器本地磁盘上,适合小团队起步,但要注意磁盘空间和多实例部署的问题:如果有多个服务实例,分片可能落在不同的机器上,合并时找不到片。单机起步可以用本地磁盘,一旦要扩到多实例,就改成对象存储。
合并是整个流程里最脆弱的一环,要当作一个需要幂等和重试的操作来写。合并请求可能因为网络问题被前端发了两次,服务端必须保证重复合并不会产生两个文件;合并过程中断了,要能从断点继续或安全重做,而不是留下一个一半的文件就标记成功。最稳妥的做法是合并到临时位置,验算总哈希通过后再原子移动到正式位置,最后再更新数据库状态。
五、秒传:文件已经在服务器上,就别再传一遍
秒传是分片上传的免费附赠。前端在初始化时把文件的总哈希发给服务端,服务端查一下这个哈希对应的文件是不是已经存在,存在就直接返回成功,前端秒传完成。这个功能对团队协作场景特别有用:同一个素材被不同的人反复上传,第二次开始就不用再传了。
秒传有两个边界要讲清楚。第一,秒传不能绕过权限检查:用户 A 传过的文件,用户 B 只有在有权限访问时才能秒传,不能因为哈希相同就直接拿到别人的文件。第二,秒传不能绕过内容审核:文件第一次上传时要走的审核流程,秒传的文件也要走,不能因为“这个文件以前审过”就跳过。把秒传理解成“跳过传输”,而不是“跳过所有检查”,就不会踩这两个坑。
六、失败处理:重试、过期与一致性
分片上传的失败形态比单次上传多得多,要分三层处理。第一层是单个分片失败:网络抖动、服务端 500、超时,这些都由前端重试解决,重试 3 次,间隔指数退避,三次都失败再向用户报错。重试时只重传失败的那一片,不要重传整个文件。
第二层是会话过期:用户传了一半去吃饭,回来发现 uploadId 过期了。处理方式是给上传会话一个合理的有效期,比如 24 小时,过期后前端重新初始化任务,但已上传的分片如果还在,可以按总哈希找回并继续,而不是强制从头开始。有效期要写在产品文案里告诉用户,别让用户自己猜。
第三层是孤儿数据:用户取消上传、浏览器崩溃、合并失败,都会在服务端留下没人要的临时分片。处理方式是一个定时清理任务,每天扫描一次,把超过有效期且没有对应正式文件的临时分片删掉,并记录清理量。没有清理任务的分片上传,磁盘迟早会被孤儿分片占满。
七、安全与成本:别让上传口子变成别人的免费网盘
上传接口是最容易被滥用的接口之一,分片上传因为支持大文件,滥用成本更高。四个检查点缺一不可。第一,鉴权:初始化接口必须验证登录态和上传权限,未登录用户连 uploadId 都拿不到。第二,大小与类型限制:总大小上限、分片大小上限、文件类型白名单,三处都要在服务端再验一遍,前端校验只是体验优化。第三,速率与配额:按用户限流上传速度和每日上传总量,防止一个人把带宽打满。第四,内容扫描:合并完成的文件要过一遍病毒扫描或内容审核,再标记为可用。
成本方面,分片上传多出来的主要是请求数和临时存储。请求数可以用稍大的分片尺寸压下来,临时存储靠清理任务控制。还要留意对象存储的计费方式:有些服务按请求次数计费,分片太小会导致请求费用超过存储费用。上线前把“一个 2GB 文件完整上传一次要花多少钱”算出来,写在内部文档里,别等账单来了才发现。
八、一个完整走通的教学案例:从上传按钮到可播放
把前面七节串起来,走一遍完整流程。以下为虚构的教学示例:示例应用“微课盒子”是一个在线课程小站,讲师要上传一节 1.8GB 的录播课,家里是普通宽带,中间还断过一次网。
第一步,讲师在页面上选好文件,前端先在本地做校验:文件是 MP4、大小在上限内,通过后算出总哈希并向服务端初始化,拿到 uploadId。服务端查总哈希,发现这个文件没人传过,于是新建上传任务,返回 uploadId 和分片大小 5MB。第二步,前端把文件切成约 360 个分片,4 个并发往上发,浮条上实时显示已传字节、速度和剩余时间。传到第 200 个分片时家里断网了,前端把失败的分片重试三次仍然失败,停下来等网络恢复,浮条上显示“网络已断开,恢复后自动继续”。第三步,网络恢复后前端先问服务端哪些分片已经收到了,确认前 199 个分片都在,只补传第 200 个及以后的分片,之前传好的一个都不用重传。第四步,全部 360 个分片到齐,前端发合并请求,服务端按编号拼接,验算总哈希通过,把文件移到正式存储,触发转码任务,最后把课程状态改成“可播放”。整个过程讲师只点了一次上传按钮,中途断网也不用管,页面刷新了也能续传。
这个案例里最容易被忽略的是第三步的“先问再传”。很多第一版实现在断网恢复后直接从头重传,看起来也能跑,但用户在弱网下传一个 2GB 文件要传三四遍,体验和成本都输给了“先查已收分片再补传”这个多出来的小接口。断点续传的全部价值,就在这个小接口上。
九、上线前检查清单
按这张清单逐项过一遍,再发布分片上传功能。第一,分片大小、并发数、重试次数都可配置。第二,断网后刷新页面,选择同一文件能续传。第三,暂停、取消、恢复三个按钮都实测过。第四,合并是幂等的,重复提交不会产生重复文件。第五,总哈希校验在合并后执行,不通过不标记成功。第六,过期清理任务已配置,孤儿分片能被定期删除。第七,秒传经过权限与审核检查,不绕过任何一环。第八,上传速度、配额、文件类型限制都在服务端生效。第九,上传浮条在切换页面后仍然可见,进度数字诚实。第十,用 2GB 真实文件在弱网环境下完整跑过一次全流程。
分片上传不是什么高深技术,它的全部价值就一句话:让大文件上传从“一次定生死”变成“每次只丢一小片”。协议五个决策一次定好,前端按切片、并发、进度、暂停四件事写,服务端把初始化、接收、合并、清理四件事做扎实,再把重试、过期、孤儿清理三层失败处理补上,一个一人团队一周就能做出可用的版本。先从 5MB 分片、4 并发、3 次重试起步,上线后看真实数据再调,比一开始就追求完美参数要划算得多。
评论 (0)
相关文章

越用越慢、重启就好,十有八九是内存泄漏:三快照定位法找到引用链,七类藏身处对号入座修掉,再用同一套操作验证一遍,最后把内存预算和检查清单留下来防复发。

日志是产品的飞行记录仪,但前提是按规矩写。结构、级别、关联 ID、字段、脱敏、采样、留存:一个一人团队今天就能落地的七件事,出问题时才不会靠猜。

多轮对话拼的不是模型记性,而是状态、澄清、打断、回指和修复五件事:状态机先画好、澄清只问会办错的事、改主意一句就够、听错了分级修,聊完还能留下一份摘要。