先确认是不是 R2 本身的问题

我会先拿同一个对象做三次测试:本地 CLI 上传、服务端 SDK 上传、浏览器 presigned URL 直传。三条链路只有一条失败,就不要急着改存储架构。

现象可能原因第一刀
小文件也失败endpoint 或凭证错重新比对 account id 和 token
大文件中途断没有分片或超时太短multipart upload
上传前就失败presigned URL 过期上传前即时签名
偶发失败网络或上游抖动指数退避重试

最短排查路径

第一步,确认 SDK endpoint 使用 R2 的 S3 兼容地址,不要继续指向 AWS 区域 endpoint。region 按文档示例写固定值或客户端要求的占位值。

第二步,给上传设置合理 timeout。服务端请求不要无限挂起,前端也要显示进度。用户上传 200MB 视频时,如果页面没有进度条,他会反复点击,反而制造更多失败。

第三步,大文件改 multipart upload。独立产品里,头像、PDF、小图可以简单直传;录屏、数据集、AI 生成压缩包必须分片。

第四步,presigned URL 不要提前批量生成太久。用户打开页面、离开、回来再上传,链接可能已经失效。我的做法是点击上传按钮后再向后端要 URL。

第五步,记录 R2 返回的 status、request id、对象 key 和文件大小。没有这些字段,客服只能听用户描述「一直转」。

为什么会这样?

R2 提供 S3 兼容 API,但「兼容」不代表你可以忽略限制。S3 SDK 的默认超时、重试、region、签名方式,很多都是为 AWS 环境设计的。换到 R2 后,必须把 endpoint、凭证权限、对象 key 规则重新检查一次。

另一个坑是把上传当成普通表单。文件上传是长连接、可中断、可重试的任务。你越早把它设计成状态机,后面越少半夜修工单。

继续排查清单

  • token 是否只有目标 bucket 权限
  • key 是否包含用户输入的奇怪字符
  • Content-Type 是否和后续读取逻辑一致
  • CORS 是否允许前端域名
  • presigned URL 有效期是否覆盖上传时间
  • 前端是否支持取消和重试
  • 后端是否把失败对象清理掉

我会怎么设计上传状态?

上传不要只存一个 url。我会给每个文件建一条 uploads 记录,状态从 createdsigneduploadingcompletedfailed。前端每次拿到签名 URL 后,把状态更新为 signed;上传完成后再让后端 HEAD 一次对象,确认大小和类型,再把它标为 completed

这样做的好处是排查很快。用户说「文件没传上去」,你能看到他到底没拿到签名、上传中断,还是对象已经在 R2 但业务表没关联。很多所谓 R2 超时,其实是业务状态没同步,文件已经存在,只是前端没拿到最终确认。

清理任务也要提前写。created 超过 30 分钟还没上传的记录可以标记过期;failed 状态保留错误码;已经上传但没有业务归属的对象,隔天批量清理。存储成本不只是每 GB 价格,还有你能不能解释每一个对象为什么还在那里。

我还会给上传入口加灰度限制。新用户先限制文件大小,老用户或付费用户再开放批量上传。这样既保护账单,也避免还没验证好的上传链路被突然打满。

还没恢复时

把上传链路降级:限制单文件大小、改成后台上传、暂停批量导入。团队排查 Cloudflare、GitHub Actions、存储和域名时,最好保持固定登录环境;我会给发布和存储后台准备海外服务跑 GitHub Actions / Cloudflare 的稳定线路,这样出问题时变量少一点。

相关阅读