大文件上传:API 中转、预签名直传与分片上传

比较经 API 中转、S3 预签名 PUT 直传与预签名分片上传三种拓扑:文件字节走哪条路、谁签名、S3 验证什么,以及过期签名、header 不匹配和 CORS 这些真实故障怎么恢复。

上传功能最容易被设计成“前端把文件 POST 给后端,后端再存进对象存储”。能用,但文件越大、用户越多,API 就越像一根被堵住的水管。先把问题说清楚:文件字节该走哪条路,谁有权放行这次写入。

有约束的设计问题

一个学习平台允许用户上传作业附件:大多数是 20 MB 以内的图片和 PDF,少数是 2–8 GB 的录屏。高峰时几百人同时上传,部分用户在移动网络下经常断线。存储用 Amazon S3,应用已经有登录和权限体系。上传链路应该怎么设计?

这里只讨论上传写入。下载授权、CDN 分发和病毒扫描产品属于相邻问题。

三种拓扑

架构字节路径谁签名 / 授权完成信号适合
API 中转上传Browser → API → S3API 用自己的 IAM 身份调用 PutObjectAPI 收到 S3 响应入库前必须处理字节的小文件
预签名 PUT 直传Browser → S3API 签发只允许一次 PUT 的 SigV4 URLS3 事件 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。

  1. 浏览器带着登录会话申请上传,只发文件名、类型和大小。
  2. API 鉴权,自己选定对象 key,写一条 pending 记录。
  3. API 用 SigV4 签出一个只允许 PUT 这个 key 的 URL,连同必须携带的 header 一起返回。
  4. 浏览器直接 PUT 到 S3。
  5. 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. 预签名分片上传:大文件和断网的答案

分片上传把一个对象拆成多段,每段独立上传、独立重试:

  1. API 调用 CreateMultipartUpload,拿到 UploadId
  2. API 为每个分片签发一个 UploadPart URL。
  3. 浏览器并行上传分片,每片返回一个 ETag
  4. 浏览器把分片序号和 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 SignatureDoesNotMatchAPI 连同 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、签名和记录,永远不碰文件字节。

面试时这样回答

  1. 先复述约束:文件大小分布、并发量、网络条件。
  2. 说字节路径:“API 只签名,字节从浏览器直达 S3”,并点出 S3 用签名验证请求。
  3. 说代价:签名 header 与 CORS 的约束,大文件为什么要分片,未完成的分片如何清理。
  4. 说一个故障和恢复:例如过期签名返回 Request has expired,重新签发 URL。