2.3 大文件分片与断点续传
场景:进度条走到 87% 的悲剧
传一个 2GB 的视频,进度条走到 87%,家里网络抖了一下,整个上传失败,从头再来。这种体验能把人逼疯,也是所有网盘类产品必须迈过的坎。我们最早没有分片,用户传大文件全靠运气,投诉里出现频率最高的词就是白传了。
分片上传解决两件事:把一个大文件切成小片并行传,长尾的网络快慢不影响整体;某片失败只需重传那一片,已成功的片不必再传。断点续传则是把切分进度记下来,关浏览器、断网、哪怕关机,下次打开接着传。两者是同一个机制的两个收益。
方案:切片大小与并发的权衡
分片不是越小越好。COS 按请求次数计费,每上传一片至少一次请求,片太小请求费会比流量费还贵。也不是越大越好,单片越大重传成本越高,并行度也上不去。我们压测了四档:
| 分片大小 | 1GB 请求数 | 失败重传成本 | 并行度 | 结论 |
|---|---|---|---|---|
| 1MB | 1024 | 极低 | 高 | 请求费爆炸,否 |
| 5MB | 205 | 低 | 高 | 移动网络适用 |
| 10MB | 103 | 中 | 中高 | 采用 |
| 50MB | 21 | 高 | 中 | 内网大文件适用 |
并发数同样要权衡,不是越多越快。浏览器对单域名有连接数上限,超了排队反而慢;客户端上传还会和用户家里的其他流量抢带宽:
| 并发数 | 实测速度 | 用户网络影响 | 结论 |
|---|---|---|---|
| 3 | 基准 | 轻微 | 保守场景 |
| 5 | 最快 | 明显但可接受 | 采用 |
| 10 | 无提升 | 卡网 | 否 |
配置:三步上传与续传记账
COS 分片上传是标准的初始化、传片、完成三步曲。Go 客户端核心逻辑(剥掉了错误处理):
// 1. 初始化,拿到 uploadId
init := cos.InitiateMultipartUpload(ctx, key)
// 2. 并发上传分片,记录每片的序号与返回的 ETag
for _, part := range chunks {
go func(p Part) {
resp := cos.UploadPart(ctx, key, init.UploadID, p.Num, p.Data)
record(p.Num, resp.Header.Get("ETag")) // 落盘续传记录
}(part)
}
// 3. 全部到位后通知 COS 合并
cos.CompleteMultipartUpload(ctx, key, init.UploadID, parts)
断点续传的关键是续传记录:把 uploadId、已成功的片号与 ETag 列表存在客户端本地(localStorage 或文件),同时后端也提供已传分片的查询接口:
// 恢复上传:先向 COS 查询 uploadId 下已有哪些片
list := cos.ListParts(ctx, key, uploadID)
for _, p := range list.Parts {
skip(p.PartNumber) // 跳过已成功的片
}
// 补齐缺失的片,然后走完成流程
踩坑:四个分片翻车现场
第一个坑,ETag 一定要存。完成合并时要提交所有片的 ETag 列表,而非只提交片号。如果客户端不落盘 ETag,恢复时只能整片重传,续传形同虚设。我们第一版漏了这个,续传功能实测并无续传效果。
第二个坑,客户端和后端各存一份进度。两边数据不一致时,到底信谁?结论是永远信 COS 的 ListParts——它是唯一的真相,客户端记录只是加速查询的缓存。对账时以服务端查询结果为准,能少走很多弯路。
第三个坑,大分片的超时设置。50MB 的分片在慢网络下可能传一两分钟,HTTP 客户端的默认超时会半路掐断已经传了 80% 的片。超时必须按预计最慢速度乘以分片大小来设,我们的经验值是按 100KB/s 估算,给两倍余量。
第四个坑,完成后不清理。分片上传中断后,已传的分片会残留在 COS 里继续计费,长期累积是看不见的成本。必须定时调用 SCF 清理超过 24 小时未完成的分片任务,这部分在 5.1 云函数回调一节有成套方案。
移动端弱网下的分片策略和桌面端不一样。手机网络波动大、可能在地铁里断断续续,我们把移动端默认分片降到 2MB、并发降到 3,并且开启自适应:连续成功五回就尝试加大,连续失败两回就缩小。这套逻辑让移动端的上传完成率从 85% 提到 96%,虽然代码丑了点,但数字漂亮。写业务代码就是这样,优雅永远让位于有效。
续传记录存在哪里也纠结过。存 localStorage 容量大但域名隔离,存 IndexedDB 更持久但 API 麻烦,存后端则要求用户登录且增加一次网络请求。最后选了 IndexedDB 加后端兜底的双写:本地优先,本地丢失时用 uploadId 从 COS 反查。用户无感知,实现复杂度也可控。
分片与秒传还有个协同优化:如果指纹命中走秒传,分片上传根本不用启动。我们在换证接口里先跑指纹检查,命中直接返回已上传完成,整个上传耗时变成一次 HTTP 往返。用户最惊讶的一次是传一个 4GB 的镜像,一秒就成功了,还以为是 bug 上门来问,我们解释完他当场决定把团队素材全迁过来。
暂停与继续功能值得一提。用户点暂停不是真的暂停网络请求,而是把当前在途分片完成或取消后中止调度,恢复时重新拉取 ListParts 对齐进度。这套实现复用了断点续传的全部设施,几乎零新增代码。功能上线后成为被夸最多的细节,虽然它底下只是状态机的一个按钮。 分片大小的最终取值我们让客户端按网络环境自选。默认 10MB,家里宽带用户可以调到 20MB,弱网用户降到 2MB,配置写在客户端里天上地下不发版。这种灵活性带来的维护成本很低,因为分片协议对片大小是透明的,COS 不关心你切多大,只关心单片不超过 5GB、总数不超过一万片这两条硬限制,别踩线即可。
断点续传还有一个反直觉的收益:它让取消上传变得可逆。用户传到一半后悔了,点取消,记录不删,下次想传同一个文件能从断点继续。这个交互是抄来的——某天我用某大厂网盘传一个 8GB 文件,取消再传居然从头开始,气得我当天晚上就把这个细节加进了需求表。对手的弱点就是你的功能点,这道理在产品层面同样成立。 最后说说分片上传在服务端的记账。每个上传中的文件在数据库里有一条状态记录:uploadId、总片数、已传片数、过期时间。这条记录同时是秒传与清理任务的数据源。早期没做这张表,清理只能扫 COS,慢且费请求;有了它,清理任务只查数据库,命中后再精准删 COS,接口调用量降到原来的二十分之一。数据在哪里,治理就在哪里,不能只依赖云端的列举接口。
给后来者一个忠告:分片上传的客户端实现是整个项目中代码量最大的一块,别想着一次写完美。我们的客户端分片模块迭代了四版才稳定,前三版分别栽在并发控制、续传记录、取消语义上。每一版都让真实用户先用着,反馈来了再改,比闭门造车快得多。 关于断点续传再补一句量化的数据。功能上线前,每天有 6% 的大文件上传最终失败,其中八成是网络波动。上线后失败率降到 1.5%,剩下的多是用户主动放弃。就这一个功能,把客服同学的工单量压掉了一半多。如果你在公司里只能做一个上传优化,做断点续传,投产比最高。
分片的世界里还有一个新手误区:分片上传不等于秒传。分片解决传输可靠性,秒传解决重复存储,两套机制共享指纹能力但目的完全不同。我们曾把它们混在一个流程里实现过,逻辑纠缠到无法维护,最后还是拆开,各走各的协议。职责清晰比代码短重要,这句话每个新模块开工前我都会默念一遍。 再补一个压测得到的反直觉结论:并发分片的速度收益存在拐点。五并发比三并发快约 40%,十并发比五并发只快不到 10%,但对用户网络的影响却是指数级。原因在于家用宽带的上行带宽才是瓶颈,并发再高也填不满水管。找到拐点就停在拐点前,给用户的网络留些余量,别让你的上传把人家的视频会议卡死。
分片大小的选择我们走过弯路。最初按文档示例设为五兆,小文件分片比正文还多;后来按文件大小分级:百兆以下不分片,大文件用八兆片。分级策略让上传耗时整体下降两成,传输开销降到最低。
断点续传的进度持久化也有讲究。进度最初只在内存里,用户切个网络就前功尽弃。后来把已分片列表落本地存储与服务器各一份,恢复上传时先对账。多写几行代码,换来的是用户不再骂街。
并发分片数也有学问。开八个并发遇到不少公司网络拥塞,调成四个稳健许多。上传参数做成可配置后,网络环境变化时改配置即可,不用发版。快速不等于并发多,稳才是快的真谛,这条经验后来被我们刻进了上传模块的默认配置。
小结
分片上传是网盘的基本功,考核指标就两条:快不快,断不断。快靠分片大小与并发的压测调优,断不了靠 uploadId 加 ETag 的持久化记账。这两样扎实了,2GB 文件和 2MB 文件对用户来说只是时间长短的区别,没有体验上的鸿沟。
