大文件上传:API 中转、预签名直传与分片上传
比较经 API 中转、S3 预签名 PUT 直传与预签名分片上传三种拓扑:文件字节走哪条路、谁签名、S3 验证什么,以及过期签名、header 不匹配和 CORS 这些真实故障怎么恢复。
上传功能最容易被设计成“前端把文件 POST 给后端,后端再存进对象存储”。能用,但文件越大、用户越多,API 就越像一根被堵住的水管。先把问题说清楚:文件字节该走哪条路,谁有权放行这次写入。
有约束的设计问题
一个学习平台允许用户上传作业附件:大多数是 20 MB 以内的图片和 PDF,少数是 2–8 GB 的录屏。高峰时几百人同时上传,部分用户在移动网络下经常断线。存储用 Amazon S3,应用已经有登录和权限体系。上传链路应该怎么设计?
这里只讨论上传写入。下载授权、CDN 分发和病毒扫描产品属于相邻问题。
三种拓扑
| 架构 | 字节路径 | 谁签名 / 授权 | 完成信号 | 适合 |
|---|---|---|---|---|
| API 中转上传 | Browser → API → S3 | API 用自己的 IAM 身份调用 PutObject | API 收到 S3 响应 | 入库前必须处理字节的小文件 |
| 预签名 PUT 直传 | Browser → S3 | API 签发只允许一次 PUT 的 SigV4 URL | S3 事件 s3:ObjectCreated:Put | 单次 PUT 装得下的文件、大量并发用户 |
| 预签名分片上传 | Browser → S3,每片一个请求 | API 发起上传、为每片签名、最后完成上传 | CompleteMultipartUpload 之后 | 大文件、不稳定网络、需要续传 |
三者的区别不在“用不用 S3”,而在字节是否经过应用,以及对象在什么时候才真正存在。
1. API 中转:简单,但带宽全压在 API 上
浏览器用 multipart/form-data 把整个文件发给 API,API 再调用 PutObject 写进 S3。好处是应用能在写入前处理每个字节;代价也很直接:
- 一个大上传从头到尾占住一个 API 连接。几百个用户同时传录屏,登录、查询这些无关请求会一起变慢。
- 单次
PutObject最大 5 GB。8 GB 的录屏根本写不进去,而用户可能已经把整个文件传给了 API。 - 中途超时只能整段重传。如果每次请求都生成新的对象 key,重试还会留下多余对象。
2. 预签名 PUT:API 只签名,字节直达 S3
流程分成两段:先向 API 要“许可”,再直接把字节交给 S3。
- 浏览器带着登录会话申请上传,只发文件名、类型和大小。
- API 鉴权,自己选定对象 key,写一条
pending记录。 - API 用 SigV4 签出一个只允许 PUT 这个 key 的 URL,连同必须携带的 header 一起返回。
- 浏览器直接
PUT到 S3。 - S3 写入成功后发出
ObjectCreated:Put事件,确认服务核对大小和类型后,把记录改为uploaded。
预签名 URL 的查询参数就是授权本身:
| 参数 | 含义 |
|---|---|
X-Amz-Algorithm | 签名算法,固定为 AWS4-HMAC-SHA256 |
X-Amz-Credential | 凭证范围:access_key/YYYYMMDD/region/service/aws4_request |
X-Amz-Date | 签名时间 |
X-Amz-Expires | 有效秒数 |
X-Amz-SignedHeaders | 参与签名的 header |
X-Amz-Signature | 签名值 |
用临时凭证签名时还会多一个 X-Amz-Security-Token。
S3 验证什么? S3 用它收到的请求重新计算签名,再和 URL 里的签名比对。HTTP 方法、bucket、对象 key、Region 以及所有参与签名的 header 都必须一致。签名时带了 Content-Type,上传时就必须原样带上:
curl -X PUT -T "report.pdf" -H "Content-Type: application/pdf" "<presigned-url>"
有效期的规则:
- 用 AWS CLI 或 SDK 签名,最长 7 天;S3 控制台里只能设 1 分钟到 12 小时。
- 用临时凭证签名时,凭证过期 URL 就失效;签名用的凭证被撤销、删除或停用,URL 也会提前失效。
- 有效期内任何拿到 URL 的人都能用它。它本质上是一张一次性、短时效的通行证,所以要在用户点击上传前一刻才签,过期时间只覆盖“开始上传”的窗口。
浏览器直传是跨域请求,bucket 必须配置 CORS:
[
{
"AllowedOrigins": ["https://www.example.com"],
"AllowedMethods": ["PUT"],
"AllowedHeaders": ["Content-Type"],
"ExposeHeaders": ["ETag"]
}
]
3. 预签名分片上传:大文件和断网的答案
分片上传把一个对象拆成多段,每段独立上传、独立重试:
- API 调用
CreateMultipartUpload,拿到UploadId。 - API 为每个分片签发一个
UploadPartURL。 - 浏览器并行上传分片,每片返回一个
ETag。 - 浏览器把分片序号和
ETag交给 API,API 调用CompleteMultipartUpload,S3 这时才把分片组装成对象。
几个必须记住的数字:分片 5 MiB 到 5 GiB(最后一片没有下限),最多 10,000 片,对象最大 48.8 TiB。AWS 建议对象达到约 100 MB 时就考虑分片上传。
两个容易漏掉的细节:
- 浏览器要读取每片响应里的
ETag,所以 CORS 规则必须在ExposeHeaders里暴露它,否则分片全部传完也无法完成上传。 CompleteMultipartUpload成功之前对象并不存在,而已上传的分片会一直留在 bucket 里计费,直到上传被完成或中止。不配置规则的话,未完成的上传永远不会被自动中止:
{
"Rules": [
{
"ID": "abort-incomplete-uploads",
"Status": "Enabled",
"Filter": { "Prefix": "uploads/" },
"AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 7 }
}
]
}
故障与恢复
| 故障 | S3 / 浏览器看到什么 | 恢复 |
|---|---|---|
| 预签名 URL 过期后才开始上传 | 403 AccessDenied,消息 Request has expired | 重新申请上传并签发新 URL;给 pending 记录设超时 |
上传时的 Content-Type 与签名时不同 | 403 SignatureDoesNotMatch | API 连同 URL 返回必须携带的 header,浏览器原样发送 |
Bucket CORS 没有放行页面 origin 或 PUT | 浏览器预检失败,请求根本发不出去 | 在 CORS 规则里加入 origin、PUT 和会发送的 header |
| 中转方案遇到超过 5 GB 的文件 | 单次 PutObject 写不进去 | 接收字节前先检查大小,大文件改走分片上传 |
| 分片上传被中途放弃 | 对象不出现,分片持续计费 | AbortIncompleteMultipartUpload 生命周期规则;保存 UploadId 以便续传 |
还有一条原则贯穿三种方案:以 S3 为准确认上传,而不是以前端说“传完了”为准。 预签名直传用 S3 事件通知确认,事件可以发到 SQS、SNS、Lambda 或 EventBridge;标记可用之前,再回读一次对象的大小和类型。
怎么选
- 入库前必须处理字节、文件又很小:API 中转。
- 文件装得进一次 PUT、应用只需要授权和记录:预签名直传。
- 文件大、网络不稳、需要续传:预签名分片上传。
回到开头的学习平台:附件走预签名直传,录屏走预签名分片上传,API 只做鉴权、选 key、签名和记录,永远不碰文件字节。
面试时这样回答
- 先复述约束:文件大小分布、并发量、网络条件。
- 说字节路径:“API 只签名,字节从浏览器直达 S3”,并点出 S3 用签名验证请求。
- 说代价:签名 header 与 CORS 的约束,大文件为什么要分片,未完成的分片如何清理。
- 说一个故障和恢复:例如过期签名返回
Request has expired,重新签发 URL。