面试官问“大文件上传与断点续传有哪些坑”,能问倒一大片,并不是因为这道题偏,而是很多人的文件上传经验停留在表单提交或者调用一个 multipart 接口。真到项目里要传 2GB 的压缩包、4GB 的数据库备份,或者在弱网环境里同步文件时,问题会立刻变成:请求超时怎么办,失败重传会不会重复写入,服务端内存会不会被撑爆,断点进度到底存在哪里。这篇文章不打算给你背一套面试答案,而是把从浏览器到服务端再到存储的完整链路拆开,讲清楚分片上传和断点续传真正难在哪里,以及实际落地时要避开哪些坑。
1. 先搞清楚:大文件上传难的不是“传输”,而是“状态”
很多人一上来就写分片代码,以为把文件切成几块、按顺序上传,再加个合并接口,就是断点续传。这个理解只对了一半。分片只是把一个大请求拆成了多个小请求,但断点续传的核心难点是:当链路在任意一环中断后,系统怎么知道哪些数据已经安全到达,哪些还需要重传,以及怎么保证恢复后的最终结果和一次上传成功时完全相同。
1.1 小文件上传是“请求问题”,大文件上传是“流程问题”
上传一个小文件,最坏情况是失败后让用户重新选一次文件。上传大文件时,让用户重传 2GB 是不现实的,所以必须把“一次传输”拆成“多次可重试的传输”。
这里有三个关键点:
- 每一次传输都需要有唯一标识,不能靠文件名或前端进度条判断。
- 每一片数据都要能被单独校验,不能依赖整包无误这个假设。
- 服务端必须记录已经接收了哪些分片,并能在后续请求中告诉客户端“你还需要传哪几片”。
也就是说,断点续传本质上是一个有状态流程。状态丢失,断点就无从谈起。前端任务刷新、浏览器关闭、后端重启、临时目录被清理,任何一环让状态丢失,都会导致续传变回重传。
1.2 上传链路里真正会出问题的四层
从浏览器里的 File 对象,到最终落盘在服务器或对象存储,中间至少隔了四层:
- 浏览器层:文件读取、分片生成、计算指纹、并发控制、请求重试。
- 网络层:代理超时、连接中断、网络限速、请求体大小限制。
- 服务端层:接收分片、临时存储、去重校验、合并分片、清理垃圾数据。
- 存储层:磁盘空间、文件系统限制、对象存储的分片接口、合并后的最终对象。
任何一个环节出问题,最后的表现都可能是“上传失败”或“上传成功了但文件打不开”。如果只在接口层调通一次,这些隐藏问题根本不会暴露出来。这也是为什么很多面试者能把分片逻辑背得很熟,但一追问“服务端怎么合并”“断点存哪里”“重复分片怎么办”,就答不上来。
2. 分片上传 + 断点续传:一套目前最常用的落地流程
先给出一个通用流程,后面再逐个拆坑。这套流程在 Web 大文件上传场景里非常常见,也基本能覆盖面试时的大部分问题。
2.1 前端分片的最小流程
在浏览器端,大文件上传的第一步通常是利用File.slice()将文件切成多个 Blob 分片。这个操作不会真正把文件复制成多份,只是创建了文件数据的引用,所以切片的成本相对可控。
示例结构大致如下:
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB let start = 0; let index = 0; while (start < file.size) { const chunk = file.slice(start, start + CHUNK_SIZE); chunks.push({ index: index++, start, end: Math.min(start + CHUNK_SIZE, file.size), data: chunk }); start += CHUNK_SIZE; }这里有几个很容易被忽略的点:
CHUNK_SIZE不一定要固定,可以根据网络环境动态调整,但生产环境里更常用的做法是固定大小,便于服务端判断分片是否完整。- 分片信息至少要包含
index、start、size等元数据,方便服务端校验和排序。 - 如果文件特别大,计算 MD5 或对每个分片做哈希时,不要同步卡住主线程。常见做法是把计算逻辑放进 Web Worker,否则页面会在“计算校验值”阶段僵住,给用户一种“上传已经死了”的错觉。
上传分片时,可以用FormData把分片文件和元数据一起提交,也可以把元数据放在 URL 或 Header 中。工程上更推荐把元数据放在 Header 或查询参数里,尽量保持请求体只有文件流本身,避免FormData在中间件解析时产生额外内存占用。
2.2 为什么不能每片裸传,需要“上传上下文”
如果只是把切片一股脑发到后端,后端接收到之后直接写磁盘,等到所有分片到齐再合并,那么出现断点的时候,客户端和服务端都不知道之前传到了哪里。
所以分片上传通常会建立一个“上传上下文”,也叫“上传任务”。典型的信息有:
uploadId:一次上传任务的全局唯一标识。fileName/fileSize/totalChunks/totalSize:文件元数据。chunkIndex:当前分片序号。chunkHash:当前分片的校验值,常用 MD5 或 SHA-1。uploadStatus:记录当前任务的状态,比如初始化、上传中、合并中、完成、失败。
客户端在实际发分片之前,先调用初始化接口创建任务;服务端返回一个uploadId。之后每次上传分片,都带上这个uploadId和分片序号。服务端可以通过这个上下文判断是否已经接收过同一片,从而做到“重复上传不重复写入”。
2.3 通用流程拆成三个动作:切片、校验、合并
整个大文件断点续传,可以简化成三个核心动作:
- 切片:客户端把大文件切成多个可独立传输的小分片。
- 校验:传输前或传输后,利用
uploadId、分片序号、分片大小或哈希确认这一段数据是否完整。 - 合并:所有分片确认接收完毕后,服务端按顺序把分片拼回完整文件,并做最终校验。
“秒传”在这个模型里并不神秘,也不是真正零秒写完文件。它可以理解为:服务端通过文件哈希发现,存储系统里已经存在相同内容的文件,于是把新上传任务直接指向已有文件,或者只创建一个引用记录,避免了重复传输和重复存储。面试时如果能把这个逻辑说清楚,比只说“秒传就是很快”更能体现理解。
3. 真正会翻车的细节,不在流程图上
流程图看起来不难,但工程落地时,很多问题恰恰出在那些“图上画不出来”的地方。
3.1 分片大小、并发数和重试策略要一起定
分片大小不是拍脑袋定的。分片太小,会产生大量 HTTP 请求,增加网络和数据库压力;分片太大,又会退化成接近整包上传,失去断点续传的意义。
一个比较常见的起点是:
- 小文件,比如 10MB 以下,可以不切片,直接走普通上传。
- 100MB 到 1GB,建议分片 5MB 到 20MB。
- 大于 1GB,可以适当提高分片大小,但也要考虑代理服务器和网关的超时限制。
并发数也需要控制。乐观地设置 10 个并发,看着好像更快,但如果服务器带宽有限,或者对象存储接口有频率限制,并发过高反而会触发限流,表现为大量请求失败。更务实的做法是:
- 先用 1 个片跑通全链路。
- 再在测试环境用 3 到 5 个并发验证速度和稳定性。
- 最后根据服务器指标、网络带宽、错误率调整并发上限。
重试策略要区分“可以重试”和“不能重试”。网络超时可以重试,但如果服务端已经返回“分片大小不正确”或“上传任务不存在”,重试再多次也不会成功,这时候应该检查元数据是否传错,而不是盲目重发。
3.2 分片校验别全部依赖文件 MD5
MD5 是校验数据完整性最常用的手段之一,但对大文件来说,一次性计算整个文件的 MD5 代价非常高。一个 10GB 的文件,即使不做其他操作,CPU 也会在哈希计算上花掉可观的时间,而且用户可能觉得页面卡住了。
工程化方案通常是两级校验:
- 上传前用每次不阻塞主线程的方式,在 Web Worker 里计算整个文件或每个分片的哈希。
- 上传后用分片大小、分片序号和分片哈希做即时校验,服务端也可以对接收到的流做校验。
面试时会有一个常见追问:“MD5 会不会冲突?”从工程实践看,单个文件的 MD5 冲突概率非常低,但如果你在做秒传功能,并且把 MD5 当作唯一判断依据,就需要清楚说明:这只是一种“高概率匹配”策略,并不是绝对安全。更严格的做法还要结合文件大小、文件内容抽样,或者使用更强的 SHA-256。
3.3 “断点”到底存在哪里
很多人把断点续传做成了“前端记录进度条”,一旦刷新页面,断点就丢了。真正的断点信息必须放在服务端或某种持久化存储里。
常见存储位置:
| 存储位置 | 优点 | 缺点 |
|---|---|---|
| 服务端内存 | 实现简单,适合单机演示 | 服务重启就丢失,无法应对多实例 |
| 本地临时目录 + 元数据文件 | 容易直读,适合中小文件 | 需要自己管理临时文件生命周期,磁盘占用不可控 |
| 数据库表 | 查询灵活,能实现任务状态管理 | 分片数量多时会产生大量记录 |
| Redis 等缓存 | 查询快,适合记录状态 | 大文件分片列表可能占用较多内存,需要设计过期策略 |
| 对象存储自带分片接口 | 状态由存储系统管理 | 受限于对象存储提供商的能力 |
如果只是后端接收分片后直接落临时目录,那么任务状态可以记录在数据库里:一张表记录上传任务,一张表记录已收到的分片。每次客户端来询问“还缺哪些分片”,后端直接查这张分片表即可。这里要特别注意的是,临时文件不是永久的,必须设计清理机制,否则一个上传失败的任务会留下大量半截文件,最终把磁盘塞满。
3.4 合并分片才是事故高发区
很多人以为服务端只要循环读分片、追加写入,就是一个完整文件。但这个环节的坑非常多。
首先,合并前必须校验所有分片是否齐全。如果前端声称传了 100 片,但实际上只到了 99 片,直接合并会产生一个损坏文件。
其次,合并时要考虑重复分片。网络重试可能导致同一个分片被提交两次,如果服务端没有做幂等处理,合并后文件大小会不对,甚至写入错位。
再次,合并过程不能一边合并一边清理临时分片。正确顺序是:
- 检查所有分片元数据。
- 按分片序号排序。
- 逐片写入最终文件,并记录写入情况。
- 全部写完后,对比最终文件大小与原始文件大小。
- 确认无误后,再清理临时分片。
注意:不要一上来就把并发和批量数拉满,先用一条样例确认输入、输出和日志都正常,再逐步放大。
4. 服务端与存储层:断点续传不是只有一套接口
前端分片只是整个流程的一半。服务端怎么做状态管理,以及底层存储怎么集成,决定了这套方案能不能真正支撑住生产环境。
4.1 临时目录、数据库和对象存储怎么搭配
中小系统里,比较常见的是“本地临时目录 + 数据库状态”的组合。后端接收分片后写入一个以uploadId命名的临时目录,分片文件按index命名。这样调试简单,也能直观看到分片是否到达。但缺点是,如果项目部署在多台机器上,负载均衡可能把不同分片打到不同节点,本地临时目录就不够用了。
另一种做法是把分片直接交给对象存储。很多对象存储都提供 multipart upload 一类的接口,它的思路和分片上传类似:先初始化一个分片上传任务,然后逐个上传分片,最后提交合并请求。这个方案的最大好处是:上传状态由存储系统管理,业务服务端不需要维护大量临时文件。
MinIO、阿里云 OSS、腾讯云 COS 这类产品通常都支持类似能力。如果项目已经用了 S3 兼容的对象存储,可以优先考虑用它的分片接口,而不是自己实现一套分片文件系统。这样可以减少临时文件、服务器磁盘、清理策略这些事,但会增加对存储服务商的依赖。
4.2 上传状态存数据库要注意什么
如果选择自己实现状态管理,数据库表设计至少要考虑:
- 一个上传任务对应多条分片记录,关系要清晰。
- 分片记录要包含任务 ID、分片序号、分片大小、存储路径、校验值、上传时间。
- 查询“哪些分片已上传”时,要能走任务 ID 索引,否则分片数量大了会慢。
- 任务完成后要删除或标记分片记录,避免数据量无限增长。
这里有个实际经验:不要为了“实时更新”而在每次分片上传时频繁更新任务表。更好的做法是:任务表只保存用户、文件名、文件大小、总片数、状态等不常变化的信息;每次分片到达时,新增一条分片记录。等到客户端来查询进度时,再通过COUNT或者其他聚合方式计算进度。
4.3 内存、临时文件和磁盘空间的边界
服务端最常见的 OOM 问题,往往不是并发太高,而是接收文件时一次性把整个文件读进内存。比如在 Java 或很多框架里,如果不注意使用流式读写,直接转成byte[],一个大文件分片就能撑爆内存。
在单分片接收时,更稳妥的思路是:
- 用输入流读取请求体。
- 用输出流直接写入临时文件或对象存储。
- 接收完成后,再读取文件大小或哈希做校验。
这样即使单分片有 20MB、50MB,内存占用也只会维持在一个相对稳定的水平,不会因为同时接几十个请求就瞬间飙升。磁盘边界的处理也一样:上传前先检查磁盘剩余空间,上传过程中记录临时文件总量,超出阈值就拒绝新任务或提示用户清理空间。
注意:临时目录需要放在有足够空间的磁盘上,不能只看根目录剩余容量,还要检查运行上传服务的用户有没有写权限。
5. 遇到上传失败,按这个顺序排查
如果面试追问“线上上传失败怎么办”,只回答“看日志”是不够的。一个合格的技术人应该能按链路分层排查。
5.1 先分现象:没发出去、发了失败、还是发完合并失败
上传失败的表现很多,但排查路径不一样:
- 前端没有发出请求:可能是文件读取失败、分片生成失败、JS 运行时报错。
- 请求发出但未到达后端:可能是网络断开、代理超时、跨域配置、请求体超限。
- 请求到达后端但返回错误:可能是分片大小不对、任务不存在、磁盘空间不足。
- 请求全部成功但最终文件损坏:问题很可能出在合并逻辑或分片校验上。
先定位“断在哪一层”,再深入排查,比一上来就翻日志更高效。
5.2 再看输入与状态:分片 ID、大小、offset 对不对
很多看似奇怪的合并失败,其实是前端把分片序号传错了。比如同一个分片被标记成两个 index,或者合并时排序条件写错。
排查时应当明确验证:
uploadId在初始化后是否保持不变。- 每个分片的
index、start、end、size是否连续。 - 文件切片是否出现重叠或空洞。
- 哈希校验时使用的字段是否与上传前一致。
如果是自己实现的分片,可以先在本地写一个检查脚本:把所有分片按 index 拼接,和原文件比对大小,再比对哈希。这一步能排除掉大量前端切片错误。
5.3 继续查环境:代理、网关、磁盘、依赖版本
后端明明没问题,但上传还是失败,常见原因包括:
- Nginx 或网关对请求体大小有限制,默认配置可能不允许大请求。
- 代理超时时间太短,分片上传没问题,但后续合并请求耗时过长被断开。
- 临时目录所在磁盘满,文件写入失败。
- 对象存储 SDK 版本与当前存储服务不兼容,导致分片接口返回异常。
排查顺序建议是:先看报错信息里有没有明确的状态码,再看网关日志和访问日志,再检查磁盘和依赖版本。不要第一步就去调业务代码。
5.4 最后查代码逻辑:超时、重试、并发、合并是否自洽
有些问题不是单点故障,而是逻辑不自洽。比如:
- 超时设置为 2 秒,但单个分片在弱网下需要 10 秒,导致频繁重试。
- 重试 5 次都是同一个分片,但服务端没有幂等校验,同一个分片被重复写入。
- 客户端并行上传 10 个分片,但服务端合并时直接按 Socket 接收顺序写入,没有按 index 排序。
- 查询“已上传分片列表”时没有按 index 排序,返回重复数据,导致前端重复上传。
这类问题在单测里很难暴露,需要模拟失败重试、服务重启、并发上传三种场景才能发现。
注意:接口做的越多,越要关注“重复调用是否安全”。分片上传里,每个接收分片的接口都应该是幂等的。
6. 回到面试:这道题到底在考什么
面试官问“大文件上传与断点续传有哪些坑”,表面上是考一个具体功能,实际上考的是三个能力:
- 能不能把一个大问题拆成小问题。
- 能不能识别出系统中哪些状态必须持久化。
- 能不能在方案设计时提前考虑失败、重复、并发和资源边界。
6.1 面试官真正想听的回答结构
我比较推荐按下面的结构回答:
- 先说清分片上传解决了什么:避免一次大请求超时、降低重试成本。
- 再说清断点续传解决什么:让失败恢复不需要从头开始。
- 然后描述完整流程:初始化上传任务、前端切片、上传分片、服务端校验、查询已传分片、补传缺失分片、服务端合并、最终校验。
- 最后说坑:分片大小、并发、超时、重试、幂等、临时文件清理、合并顺序、磁盘空间、对象存储选择。
只回答“前端切片、后端合并”是不够的。把“状态怎么存”和“失败了怎么恢复”也讲清楚,才算真正理解断点续传。
6.2 什么情况下不建议用断点续传
断点续传不是银弹。小文件根本不需要分片,额外引入分片、合并、状态管理反而增加复杂度。场景如果是低频、小文件、内网高速传输,直接普通上传更合适。
只有满足下面至少一个条件时,才值得投入做断点续传:
- 文件体积大,一次请求容易超时。
- 网络不稳定,失败概率高。
- 用户希望中断后能恢复,而不是从头再来。
- 上传耗时长,需要显示真实进度。
在技术选型时,先评估文件大小分布和网络环境,再决定要不要上分片。这个判断比“我会不会写分片代码”更重要。
6.3 一个值得记住的判断
大文件上传和断点续传,真正考验人的不是代码量,而是对失败场景的预判。能用小分片解决超时问题,能用状态管理解决断点问题,能用幂等逻辑解决重复问题,能用合并校验解决文件完整性问题,这套方案才算真正闭环。
下次有人再问这道题,不要急着背接口,先问自己一句:如果文件传到一半断网了,系统怎么证明它知道“哪一片已经传完”?把这个问题想明白,你就已经赢了很多人。