李工上个月差点裸辞。
他在一家有200多号人的工程公司当IT负责人,公司用的一套老牌企业云盘号称支持大文件上传,结果每次设计师传3GB的BIM模型,项目部传1.2GB的无人机航拍视频,不是超时就是中断。最离谱的一次,50分钟的渲染文件传到97%崩溃了,设计师当场骂人,李工只能陪着笑脸说”我们再试一次”。
这种场景在工程公司、设计院、影视制作团队里太常见了。企业云盘的大文件上传能力,不是”支持”两个字能说明问题的,背后是一整套分片上传、断点续传、并发控制的工程实现。今天把这里面真正有坑的地方说清楚。
分片上传:把大文件拆成小块
先说分片上传的原理。这个技术听起来不复杂,但参数选错会出大事。
核心逻辑:客户端把文件拆成固定大小的分片(chunk),逐个上传,服务端接收后按顺序合并成完整文件。每个分片独立传输,互不影响——任意一个分片失败了,只需要重传那一个,不用从头再来。
拿巴别鸟的实践来看,分片大小的选择是个经验活儿:
- 单分片太小(比如512KB):分片数量爆炸,合并时的I/O次数暴增,上传一个10GB文件要产生20000多个HTTP请求,握手开销吃光带宽
- 单分片太大(比如100MB):重传代价高,50%的进度重新传100MB,肉疼;而且有些网络中间设备对大请求体有超时限制
- 业界常见值:1MB到10MB之间。巴别鸟默认用2MB单分片,最大支持到10MB
再看几个硬限制,这是很多人踩坑的地方:
| 参数 | 典型限制 | 备注 |
|---|---|---|
| 单分片大小 | 1KB ~ 10MB | 服务端可配置 |
| 最大分片数 | 1000 ~ 10000 | 受限于合并缓冲区和文件签名校验 |
| 最大单文件大小 | 2GB ~ 50GB | 视存储架构和合并策略而定 |
| 最大并发分片数 | 3 ~ 10 | 浏览器端受限于同域名连接数 |
李工后来查日志才发现,他们那套云盘最大分片数设的是500,上传超过1GB的文件时分片数直接超限,返回的错误信息写的是”文件过大请联系管理员”,让人哭笑不得。
分片大小动态调整是个值得做的优化。巴别鸟的实现逻辑是:根据文件大小自动选择分片——小于100MB的文件用2MB分片;100MB到1GB用5MB分片;大于1GB用10MB分片。这样既控制了分片数量,又不会让重传代价过高。
并发上传是另一个关键点。串行上传N个分片,总时间=N×单分片上传时间。但如果你能同时传M个分片,理论时间是(N/M)×单分片上传时间+M×分片启动开销。实际上浏览器端受同域名连接数限制,Chrome下M开到6到8比较合适,再高收益递减。服务端也要做并发控制——如果10个用户同时上传,每个都开8路并发,服务端,瞬间积压的待合并分片能把磁盘I/O打满。
分片上传的真实流程:
- 客户端请求服务端获取上传会话(session),服务端返回一个唯一uploadId和推荐的分片大小
- 客户端计算文件的MD5或SHA1哈希(用于秒传和完整性校验),附加到session里
- 客户端按顺序或乱序上传各个分片,每个请求带uploadId、分片序号、分片内容
- 服务端每收到一个分片就落盘(写临时目录),避免内存缓冲
- 所有分片传完后,客户端发起合并请求,服务端按序号拼装、校验哈希、输出最终文件
断点续传:续的不只是”断点”
很多人以为断点续传就是”断了之后从断点继续”。实际上要实现真正可靠的断点续传,需要解决三个问题:续哪里的传、怎么判断已经传了、怎么保证续上的是对的。
Range请求:HTTP层面的断点支持
HTTP协议本身支持范围请求,用Range和Content-Range两个请求头就能实现基本的续传能力。
客户端发请求时带上:
Range: bytes=2048000-4095999
表示我要这个文件的第2048000字节到第4095999字节(基于0的偏移,按2MB分片算就是第2个分片)。
服务端如果支持,返回:
HTTP/1.1 206 Partial Content
Content-Range: bytes 2048000-4095999/10485760
Content-Length: 2048000
然后只返回这个范围的数据。
但这里有个大坑:Nginx默认不转发Range请求。你的应用服务端写了Range处理逻辑,但前面跑了一层Nginx做反向代理,对不起,Nginx可能直接把它当普通请求处理了,返回完整文件,续传功能直接报废。
解决方案有两个:
1. Nginx配置里加proxy_http_version 1.1;和proxy_request_buffering off;,并且明确设置max_ranges。
2. 不用Nginx的代理,自己在应用层处理Range——这种方式更可控,但开发成本高。
秒传:哈希校验的价值
秒传是断点续传的最佳拍档。原理很简单:上传前先计算文件的MD5/SHA1哈希,发送给服务端查询——如果服务端已经存在这个哈希对应的文件,说明文件内容一致,直接”秒传成功”(服务端做个硬链接或标记,无需真正传数据)。
这个机制的价值在于:团队协作时,一个人传过的文件,其他人传同名文件或同一批文件,可以瞬间完成,不用重复消耗带宽。
哈希算法选型也有讲究:
– MD5:速度快,但有碰撞风险(理论上可构造恶意碰撞文件)。对于普通业务文件够用。
– SHA1:比MD5慢约20%,碰撞风险低很多。Google当年做过SHA1碰撞演示,但需要特定构造,对常规文件威胁不大。
– SHA256:更慢(约为MD5的3-4倍),但安全强度最高。金融、医疗行业建议用这个。
李工他们公司踩过一个坑:服务端存了几千个GB的图纸文件,用的是MD5校验,设计师传来传去经常出现”秒传失败但文件内容一样”的情况——后来排查发现是当年有人用不同的压缩工具处理过同一批图纸,文件大小一样但MD5不同,秒传命中不了。后来换了SHA256,问题消失。
秒传的另一个限制:大文件的哈希计算本身就很慢。一个10GB的文件,MD5计算在SSD上大概需要15-30秒(取决于I/O速度),SHA256需要60-120秒。这段时间用户在等待,服务端要保持session有效。用户感知到的”秒传”其实是”等了几十秒后告诉我不用传了”——体验上需要加个进度提示。
续传状态的持久化
断点续传最难的部分不是协议,是服务端怎么记录”哪些分片已经传完了”。
常见的实现方式:
1. 分片状态表
服务端维护一个数据库表或Redis结构,记录每个uploadId对应的分片接收状态。格式大概是:
{
"uploadId": "u-20240415-abc123",
"fileHash": "e3b0c44298fc1c149afbf4c8996fb924",
"totalChunks": 5120,
"receivedChunks": [0, 1, 2, 3, 5, 6, 9, ...],
"createdAt": "2024-04-15T10:00:00Z",
"expiresAt": "2024-04-16T10:00:00Z"
}
每次收到一个分片,服务端更新这个状态。客户端断线重连后,先查这个状态,知道从第几个分片继续。
2. 临时文件合并法
不维护分片状态表,而是把每个分片直接写到一个统一的临时目录,用序号命名。合并时按序号遍历一遍,缺的序号就是没传完的。这种方式实现简单,但有两个问题:临时文件膨胀(需要定期清理过期session),以及分片乱序到达时合并逻辑要处理好。
3. 合并超时和过期机制
这是很多人忽略的坑:服务端如果不给upload session设置过期时间,磁盘空间会被”僵尸上传”慢慢占满。巴别鸟的策略是:上传会话72小时内有效,超过没合并的分片和状态全部清理。客户端断线重连时如果session已过期,需要从头开始上传。
Tus协议: resumable upload 的开放标准
分片上传各家实现不同,协议不互通,换个云盘就得重写客户端。Tus协议就是为了解决这个问题而诞生的——一个开放的、HTTP-based的、可恢复上传协议。
Tus的核心设计原则是:服务端不保存任何上传状态,状态由客户端主导。这听起来反直觉,但细想很合理——服务端只负责接收分片和最终合并,客户端自己维护”已经传到哪里了”,重连时客户端告诉服务端从哪个字节继续。
Tus v1.0.0的核心请求流程:
- 创建上传会话
POST /files HTTP/1.1
Upload-Length: 10737418240
Upload-Metadata: filename dXNlcm5hbWUucGRm, filetype YXBwbGljYXRpb24vcGRm
服务端返回Location: /files/abc123,后续所有请求都基于这个URL。
- 查询当前进度(可选,客户端用来判断从哪继续)
HEAD /files/abc123 HTTP/1.1
服务端返回Upload-Offset: 5242880,告诉客户端服务端已经收到了多少字节。
- 继续上传(从offset位置续传)
PATCH /files/abc123 HTTP/1.1
Upload-Offset: 5242880
Content-Type: application/offset+octet-stream
Content-Length: 5242880
[BINARY CHUNK DATA]
- 服务端更新offset,直到
Upload-Offset == Upload-Length时文件完成。
Tus协议的优点:
– 开放标准,客户端库有官方实现(Python/JavaScript/Go/Java/iOS/Android)
– 协议简单,容易自己实现服务端
– 支持任意大小的文件(offset是字节级别,不是分片级别)
– 扩展机制丰富(checksum、concatenation等扩展)
Tus协议的坑:
– PATCH方法在某些企业防火墙环境下被禁用(Nginx默认对PATCH支持不友好)
– tusd(官方Go语言服务端实现)在高并发场景下默认配置容易出现goroutine泄露
– tusd默认没有内置的认证鉴权,需要自己包装一层中间件
常见坑:这几个问题能让你调一周
Nginx代理超时
这是大文件上传的头号杀手。Nginx默认的proxy_connect_timeout是60秒,proxy_send_timeout是60秒,proxy_read_timeout是60秒。上传一个1GB文件需要多久?假设带宽是50Mbps,理论时间约163秒——直接超时给你看。
必须调整的参数:
proxy_connect_timeout 600s;
proxy_send_timeout 600s;
proxy_read_timeout 600s;
client_max_body_size 50G; # 允许上传的最大body
proxy_request_buffering off; # 关闭代理层缓冲,否则大文件会先缓冲在Nginx内存里
同时要修改php.ini或对应后端语言的配置文件,把上传超时和body大小限制也调大。
代理层对大请求的内存问题
即使Nginx不超时,还有一个隐藏的内存陷阱:proxy_request_buffering on时,Nginx会先把整个请求体缓存到内存里,然后才往后端传。如果上传2GB文件,Nginx进程会尝试申请2GB内存——32位进程直接崩溃,64位进程也会因为内存碎片导致系统可用内存急剧下降。
解决方案:全局设置proxy_request_buffering off;,并且在location块里也明确写一遍。
合并时的内存峰值
服务端合并分片时,如果一次性把整个文件加载到内存里再写磁盘,内存会爆炸。正确的做法是流式合并:按顺序读每一个分片,用sendfile()或pipe()系统调用直接导向输出文件,跳过用户态内存拷贝。
在Java里是FileChannel.transferTo(),在Python里是shutil.copyfileobj(),在Go里是io.Copy()——都是流式处理,不会把整个文件塞进内存。
断点续传的多端冲突
同一个文件,PC客户端在上传,手机客户端也在上传,都断了,都续传——服务端怎么合并?这种场景需要加文件锁或版本冲突检测:第二个客户端尝试续传时,如果发现服务端记录的offset和自己本地的offset不匹配,说明有冲突,应该报错提示用户,而不是盲目覆盖。
分片合并顺序校验失败
网络不稳定时,分片可能乱序到达。服务端必须严格校验分片序号,如果收到”序号3″后发现序号2还没到,不能直接跳过——应该缓存序号3,等序号2到了再合并。可以用一个bitmap记录哪些序号已到达,满了再触发合并。
上传进度”回退”的用户体验问题
有些实现里,由于并发上传乱序到达,客户端上报的进度会出现”跳变”——比如用户看到进度到了60%,突然跳回55%,然后又跳到70%。这在技术上是正确的(55%的时候序号5到了,但序号4没到,所以55%是假的),但用户体验很差。
解决方案是乐观进度:客户端把已发送的分片就计入进度,不等服务端确认;服务端合并时发现缺分片,再通知客户端补传。这样用户看到的是单调递增的进度条。
实战经验:巴别鸟的大文件上传架构
巴别鸟在2023年重写了大文件上传模块,核心设计是这样的:
客户端:
– 默认用5MB分片,最大支持10MB(通过服务端配置下发)
– 最多6路并发上传同一文件
– 预计算MD5+SHA256双校验(MD5用于秒传预判,SHA256用于最终完整性校验)
– 进度条采用乐观策略,已发送即计入显示
服务端:
– tusd作为可选协议支持(1.0.0版本)
– 自研分片上传作为默认协议(兼容性更好,支持分片状态查询)
– 分片状态存在Redis集群里(12小时过期),最终文件元数据在PostgreSQL
– 合并用流式管道,不落内存
– 上传会话支持客户端主动取消(释放服务端占用的临时空间)
最常踩的坑:
上线第一个月,发现高并发场景下合并速度跟不上接收速度,磁盘I/O队列长度飙到200+,最后加了一层分片接收限流——服务端同时最多合并3个文件,超过的排队——问题解决。
总结
大文件上传不是”把文件POST到服务器”那么简单。分片大小选错了,文件稍微大点就失败;Nginx超时没配,上传过程中莫名中断;续传状态没持久化,重启服务后所有上传清零;哈希校验没做,秒传永远命中不了。
企业云盘选型时,必须把大文件上传能力当作核心指标去测,不要相信官网”支持大文件上传”这种模糊描述。测试方法很简单:找一台10Mbps的上行带宽的服务器(比大多数公司实际带宽还快),传一个5GB的文件,看看到底能不能传完、中间断了怎么办、续传靠不靠谱。
李工后来换了支持分片续传的方案,3GB的BIM模型平均12分钟传完,中断了重连10秒内恢复。他说这是他当IT负责人以来,花最少时间解决的最大用户投诉。