大文件上传与断点续传:从分片到合并的避坑指南
面试官问“大文件上传与断点续传有哪些坑”能问倒一大片并不是因为这道题偏而是很多人的文件上传经验停留在表单提交或者调用一个 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 分片校验别全部依赖文件 MD5MD5 是校验数据完整性最常用的手段之一但对大文件来说一次性计算整个文件的 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 一个值得记住的判断大文件上传和断点续传真正考验人的不是代码量而是对失败场景的预判。能用小分片解决超时问题能用状态管理解决断点问题能用幂等逻辑解决重复问题能用合并校验解决文件完整性问题这套方案才算真正闭环。下次有人再问这道题不要急着背接口先问自己一句如果文件传到一半断网了系统怎么证明它知道“哪一片已经传完”把这个问题想明白你就已经赢了很多人。