对象存储 R2 · 技术讲解

文件放 R2
账留在数据库

对象存储只管字节。谁拥有它、还在不在、能不能删——全靠库里那一行。

// 面向要接 R2 的工程师 · 实测口径 2026-08
// 本文为公开版,具体标识已替换为占位符

CLOUDFLARE R2 S3 兼容 API 零出口流量费 按用途分桶 · 三种公开面 SUPABASE / NEON 索引 私有桶服务端转发
0
条访问路径公开域 / 自定义域 / 私有转发
$0
出口流量费 / GB选它基本就为这一条
0
步落库范式先建行,再发 key
0
天空备份被绿灯掩盖已修 · 见「实测坑」
SCROLL / 向下滚动
01
What Object Storage Is

不是磁盘,也不是数据库

R2 只做一件事:用一个 key 存取一整份字节。没有目录、不能改一半、也不能 WHERE

Object◇ R2 / S3

对象存储

key → 整份字节

写入即整份覆盖,没有随机写。扁平命名空间,「目录」只是 key 前缀。图片、视频、备份、安装包的归宿。

单对象 ≤5TB · 11 个 9 持久性
Block / File◈ 磁盘

块与文件系统

路径 → 可随机读写

能 seek、能改一个字节、有真目录树。代价是要挂载、要扩容、冗余得自己管。

EBS / NFS / 本机盘
Database✦ Postgres

数据库

行 → 索引 + 事务

适合查、改、关联、约束。不适合塞几 MB 的二进制——那会把备份和连接池一起拖垮。

Supabase / Neon / Turso
Design

货放对象存储,目录留数据库。R2 里的每个 key 都必须在库里有一行认领它——没有那一行,这份字节就既查不到、也没人敢删。

02
Why R2 · Cost & Trade-offs

选它基本只有一个理由

出口流量免费。存得多不便宜,发得多才便宜——媒体分发场景差距是数量级的。

月账单对比:存 1TB + 出站 10TB
美元 / 月 · R2 为官网现价 · S3 为参考挂牌价
R2$15
S3 标准(含出站)≈$923
只存不发 · R2$15
只存不发 · S3≈$23

// R2 侧 $0.015 / GB·月 + 出站 $0(官网复核 08-18)· S3 侧为参考挂牌价,未复核

NO QUERY只能按前缀翻页

list 没有 WHERE、没有 JOIN、没有排序。想问「这个客户的文件有几份」,只能问数据库。

NO METRICS用量要另发 token

默认 R2 token 没有 GraphQL Analytics 权限,Class A/B 操作量读不到——实测拿不到桶热度。

LOCATIONAPAC 藏在「自动」下

「指定管辖权地」只有 EU / FedRAMP。要亚太得选自动 → 位置提示 → APAC

LIFECYCLE规则要编辑不要新增

新桶自带一条 7 天 multipart abort。同前缀再加一条同类规则,行为未定义——改那条就好。

03
Buckets · Public Surface

N 个桶,三种公开面

一个账户通常按业务拆成 N 个桶。先问每个桶「谁能从公网直接读到它」——答案只有三种,而最省事的那种最难收回。

R2.DEV公开开发域

裸桶直读、零配置。URL 一旦被写进代码或数据,就等于关不掉——收口即断链。

CUSTOM DOMAIN自定义域

挂进自己的 zone,才有缓存规则、WAF 与鉴权可用。对客分发的规范出口

PRIVATE全私有

没有任何公开面,只经服务端转发访问。备份与内部资料的唯一归宿

Gap

老桶最常见的缺口是没有过期类生命周期规则。停写的桶、预览图、构建残留会一直躺着——没人认领,也没人敢删。建桶当天就把生命周期和「谁认领」一起定下来。

04
Bucket × Database · The Pattern

先建行,再发 key

我们见过最干净的范本是一张资产表:读、写、删全过数据库,R2 只是从属副本。

01 · API先建行写入资产表拿到 storage_key
02 · Server签凭条presign 短有效期,限这一个对象
03 · Client直传 R2字节不经业务服务器
04 · API回写状态大小 / mime 落库,行转 ready
05 · Read只认库里的行删除也先删行,再清对象

// 反过来「先传后记」会在任何失败点留下没人认领的孤儿对象

五个仓的打通程度(已匿名)
A–D 分档 · 2026-08 内部审计
仓 AA
仓 BB
仓 CB−
仓 DC
仓 ED

// B=存了完整 URL 而非 key · C=索引在本机 SQLite · D=域名硬编码进代码

为什么必须是 key 而不是 URL

库里存 https://pub-xxx.r2.dev/a.jpg 的那一刻,换域名就得刷全表;存 storage_key 则只需改一处拼接。这就是 A 档与 B 档的全部差别。

05
Access Paths

同一份字节,三条路

给谁看,决定走哪条路。选错就是数据裸奔,或者国内根本打不开

PUB-HASHr2.dev 公开域

零配置直连公网,适合临时验证。一旦写进代码就再也关不掉——它还会把桶的存在一起暴露出去。

CUSTOM DOMAIN自定义域 media.*

挂进 zone 才有缓存规则与 WAF。缓存规则的主机名必须用 eqwildcard 实测从不命中

PRIVATE + PROXY私有桶服务端转发

浏览器只碰自家域名,服务端取字节直接把 Response.body 接下去。别 arrayBuffer——512MB 机器会 OOM。

Constraint

签名 URL 不是万能:指向 *.supabase.co 的签名地址在国内解析不了——签名有效,只是那台机器到不了。凭据还得放路径段,因为 <img> / <a download> 带不了自定义头。

缓存与回源

现行规则 edge 1 月 / browser 1 年,排除备份前缀。验证命中必须用 GET——HEAD 会误报 DYNAMIC。

上传完必须回读

200 ≠ 内容对,Content-Type 对 ≠ 文件全。HEAD 状态 + 魔数 + 线上引用三步缺一不可。

06
Field Notes · All Reproduced

六个真踩出来的坑

全部是实测记录,不是理论风险。最贵的一个整整绿灯了 15 天

INCIDENT备份连续 15 天是空文件

pg_dump | gzip 没开 pipefail,取的是 gzip 的退出码;客户端 16.14 撞服务端 17.10 直接 abort。产物 20 字节,CI 全绿

EXPOSURE备份别进挂公开域的桶

桶一旦挂了公开域名,里面任何前缀都可能被公网直接读到——敏感产物永远不进带公开域名的桶

SSL ALERT 40握手失败是 accountId 打错

不是沙箱也不是网络:ID 错了 SNI 就非法。另外,模板 env 文件里的 R2 变量常常只是占位符,别信。

BY DESIGN裸域名 404 不是故障

自定义域没有根对象。分发要给完整对象路径,而且必须可点击——截图里的 56 字符 URL 没人能手输对。

CACHEwildcard 规则从未匹配过

主机名用 wildcard 写的缓存规则一直没生效,全量回源还看不出来。改成 eq 后立刻 MISS → HIT

GREP按桶名搜是零命中

代码里写的是 pub-xxxxxxxx 前缀,不是桶名。要搜 hash——按桶名 grep 会得出「没人用,可以关」的假结论。

07
Checklist

收口清单:五件该做的事

上面每一个坑,收口动作都很具体。建桶、写第一行上传代码之前对一遍。

DO该做的

  • 备份单独建零公开域名的私桶,配过期生命周期
  • 备份 pipeline 开 pipefail,产物做 gzip -t 与体积阈值校验
  • 缓存规则主机名用 eq,改完用 GET 验证 MISS → HIT
  • 先建行再发 key,库里存 storage_key 不存 URL
  • 老桶补过期规则,给每个前缀找到认领它的那一行
这一节的通用教训

「退出码 0」不等于「业务有效」。任何写入 R2 的定时任务都要落两个指标:脚本自身心跳,以及产物体积/条数的 delta——只看绿灯,等于没监控。

08
Cheat Sheet

一页看全怎么做

做法口径 / 备注
端点https://<accountId>.r2.cloudflarestorage.comS3 兼容
建桶位置自动 → 位置提示 → 亚太 APAC管辖权地只有 EU
生命周期编辑默认那条 multipart abort别新增同前缀
CORSJSON 压成单行再粘贴CodeMirror 会污染
缓存规则http.host eq "media.example.com"wildcard 不命中
验证缓存用 GET 看 cf-cache-statusHEAD 误报 DYNAMIC
落库范式建行 → presign → 直传 → 回写存 key 不存 URL
私有原件服务端流式转发,凭据放路径段不发签名 URL
失败语义查不到 / 不是你的 / 路径非法 一律 404403 = 探测接口
备份产物pipefail + gzip -t + 体积阈值私桶,零公开域
上传验证HEAD 200 + 魔数 + 线上引用三步缺一不可
一句话总括:R2 只回答「字节放哪、传出去要不要钱」;「这份字节归谁、还在不在、能不能删」全归数据库——桶里所有难题,追到底都是某处少写了那一行账。