R2 是 Cloudflare 的对象存储,兼容 S3 接口,最大的卖点是没有出网流量费。很多人第一次用它,是因为把图片、视频放在别处被流量费咬了一口,换过来之后发现账单只剩存储和请求两项。
这篇文章按实际操作顺序写:建桶、传文件、绑域名、配缓存、加防盗链,最后是日常运维和排错。文中价格与免费额度以 Cloudflare 官方文档为准,具体以控制台显示为实。
先搞清楚 R2 的计费模型
R2 只收三样东西的钱:存储量、Class A 操作、Class B 操作。出网流量(Egress)对所有存储类型都是零费用,这是它和 S3 最本质的区别。
| 项目 | Standard | Infrequent Access |
|---|---|---|
| 存储 | $0.015 / GB-月 | $0.01 / GB-月 |
| Class A 操作 | $4.50 / 百万次 | $9.00 / 百万次 |
| Class B 操作 | $0.36 / 百万次 | $0.90 / 百万次 |
| 数据取回费 | 无 | $0.01 / GB |
| 最短存储期 | 无 | 30 天 |
| 出网流量 | 免费 | 免费 |
免费额度只对 Standard 生效,每月 10 GB 存储、100 万次 Class A、1000 万次 Class B。个人博客、小站点的图片和附件基本用不到付费区间。
理解 Class A 和 Class B 的区别,是省钱的关键:
- Class A(贵,写类):PutObject、CopyObject、ListObjects、CreateMultipartUpload、UploadPart 等,凡是"改动状态"的算这里。
- Class B(便宜,读类):GetObject、HeadObject 等读取操作。
- 免费:DeleteObject、DeleteBucket、AbortMultipartUpload。
有一个容易被忽略的细节:计费单位向上取整。做了 100 万零 1 次 Class A,按 200 万次计费;存了 1.1 GB-月,按 2 GB-月计费。所以别写出"每秒 List 一次目录"这种代码,List 属于 Class A,会把免费额度吃得很快。
建桶与控制台操作
登录 Cloudflare 控制台,左侧菜单进 R2,首次使用需要绑定支付方式(即使只用免费额度)。
建桶时注意两点:桶名全局唯一、创建后不能改名;存储位置(Location)选自动或就近区域即可,R2 会自动做全球分布,位置主要影响写入延迟和数据驻留合规。
建好之后,桶的 Settings 页面有几个开关值得先说清楚:
- Public access → R2.dev subdomain:打开后会给你一个
pub-xxxxxxxx.r2.dev的地址,能直接公网访问。这个域名只适合测试,Cloudflare 明确说明它有速率限制、不保证性能,不要拿它上生产。 - Public access → Custom Domains:绑定自己的域名,这才是生产用法,下一节细说。
- CORS Policy:前端直传或跨域读取时需要配,否则浏览器会拦。
用 API Token 和命令行传文件
控制台能拖拽上传,但批量操作还是要靠命令行。先在 R2 页面右侧的 “Manage R2 API Tokens” 创建一个 Token,权限选 Object Read & Write,可以限定到某个桶。记下三样东西:Access Key ID、Secret Access Key、以及账户的 S3 端点:
|
|
方式一:rclone(适合同步目录、断点续传)
|
|
|
|
方式二:AWS CLI(R2 兼容 S3,换个 endpoint 就能用)
|
|
上传时顺手把 Cache-Control 写进对象元数据,是后面缓存能生效的前提。带内容哈希的静态资源(app.a1b2c3.js 这种)可以放心用一年;会变的文件用 max-age=86400 更稳。
绑定自定义域名,缓存才有可能生效
这一步是整个教程的分水岭。用 r2.dev 域名访问,请求根本不经过 Cloudflare 的 CDN 缓存,也就谈不上缓存命中、WAF 和速率限制;只有绑定自定义域名并开启橙色云代理,这些能力才会挂上去。
操作路径:桶 → Settings → Public access → Custom Domains → Connect Domain,填一个子域名,例如 cdn.example.com。前提是这个域名已经托管在同一个 Cloudflare 账户下,系统会自动写入一条 CNAME 记录,代理状态必须是橙色云(Proxied),灰色云等于直连,缓存和防护全部失效。
绑定完成后验证两件事:访问 https://cdn.example.com/你的文件名 能拿到内容;响应头里出现 cf-cache-status,初始一般为 MISS 或 DYNAMIC,配好缓存规则后应该变成 HIT。
缓存:两层配置,缺一不可
R2 前面这层缓存由 Cloudflare 负责,效果取决于两处配置:对象自带的 Cache-Control 头,和域名上的 Cache Rules。前者是"源站怎么说",后者是"CDN 怎么执行"。
第一层:对象元数据。 上传时设置 Cache-Control: public, max-age=31536000, immutable,告诉 CDN 和浏览器这个文件可以长期缓存。已经上传的文件可以改元数据,或干脆重传。
第二层:Cache Rules。 路径是域名 → Caching → Cache Rules → Create rule。一条够用的规则长这样:
- 规则名:
r2-cdn-cache - 匹配条件:
Hostname equals cdn.example.com - 缓存资格:Eligible for cache
- Edge TTL:Ignore cache-control header and use this TTL → 30 天(或 7 天)
- Browser TTL:Respect origin(沿用源站)
为什么不直接沿用源站的 Cache-Control?因为 Cloudflare 的默认行为只缓存常见静态扩展名,HTML 之类默认不缓存。用 Cache Rules 显式声明 TTL,能让"边缘缓存多久"脱离源站头的限制,特别是当对象上传时压根没设头的情况下。
不同内容的 TTL 可以参考这个思路:
| 内容 | Edge TTL | 说明 |
|---|---|---|
| 图片、字体 | 30 天 | 基本不改,缓存收益最高 |
| 带哈希的 JS/CSS | 1 年 | 文件名变即内容变,可 immutable |
| 视频、压缩包 | 30 天 | 体积大,命中一次省一次 Class B |
| PDF、文档 | 7 天 | 偶尔更新,留一点回旋余地 |
| JSON、接口响应 | 不缓存 | 动态内容,缓存会读到脏数据 |
缓存配好之后,收益是双重叠加的:命中边缘缓存的请求不会打到 R2,既不消耗 Class B 操作,也不产生任何存储读取。一个日访问几万次的图片站,缓存命中率从 40% 提到 95%,账单差距可能是一个数量级。
改完规则记得刷缓存:Caching → Purge Cache,可以按 URL 刷,也可以整站刷。否则旧的边缘副本会一直按老 TTL 活着。
防盗链:先认清它能防到什么程度
防盗链的本质是"判断请求是不是从我的页面发起的"。绕不过去的前提是:Referer 可以由客户端随意伪造,所以它只能挡住直接引用你链接的一般站点,挡不住有心人。真正要保护付费资源,得用签名 URL 或鉴权代理。
方案一:WAF 自定义规则校验 Referer(最常用)
路径:域名 → Security → WAF → Custom rules → Create rule。
|
|
动作选 Block。这条规则的意思是:访问 CDN 域名、带了 Referer、但 Referer 不是自己站点的,一律拦截。
http.referer ne "" 这个条件很重要——浏览器直接打开图片、或者从微信/QQ 里点开时,Referer 往往是空的。加了这一句表示"放行空 Referer",体验更好;如果资源敏感,就去掉它,让空 Referer 也被挡。
方案二:签名 URL(适合私有资源)
公开资源用 Referer 够了,付费下载、限时链接这种就要签名。R2 支持 S3 预签名,生成的是带时效的临时地址:
|
|
链接一小时后失效,转发出去也没用。缺点是每个文件都要单独生成,适合后端按需签发,不适合海量静态资源。
方案三:Worker 做鉴权代理
把域名指向 Worker,在 Worker 里校验 token、Cookie、UA 或调用频率,通过后再用 env.MY_BUCKET.get(key) 从 R2 取对象返回。灵活度最高,可以加黑白名单、限速、按用户计次,代价是要写代码,且 Worker 请求本身有免费额度限制。
方案四:速率限制兜底
防盗链防的是"别人引用",防不住"有人拿脚本刷你的公开链接"。域名 → Security → WAF → Rate limiting rules,对同一 IP 设阈值,比如 10 秒内超过 100 次请求就 Block 或 Managed Challenge。免费计划也提供少量规则额度,配一条针对大文件路径的规则很划算。
组合起来用:WAF 挡盗链、Rate Limiting 挡刷量、签名 URL 保敏感资源,三层各管一段。
日常运维:迁移、清理与成本
从 S3 搬数据有两种官方工具:Super Slurper 适合一次性批量迁移,会自动分片、重试;Sippy 适合渐进式迁移,请求命中 R2 就直接返回,没有就从源桶拉过来并回填,业务可以先切流量再慢慢搬。两者工具本身免费,只算 R2 的 Class A 操作费。
对象生命周期:桶的 Settings 里可以配生命周期规则,比如把 90 天未访问的对象转到 Infrequent Access,或直接删除临时文件。注意 IA 有 30 天最短存储期,删早了照样按 30 天收钱,别拿它存短期文件。
成本自查清单:
- 缓存命中率是否够高(
cf-cache-status: HIT的占比) - 有没有代码在频繁
ListObjects(这是 Class A,很贵) - 大文件是否走了分片上传并正常完成(残留的分片会占存储)
- 免费额度是否已经贴近上限(10 GB / 100 万 Class A / 1000 万 Class B)
常见问题排查
访问返回 403:先看对象是不是私有、域名有没有绑对;如果是刚配的 WAF 规则,去 Security Events 里看是不是被自己拦了。
cf-cache-status 一直是 DYNAMIC:说明请求没被当作可缓存内容。检查域名是否橙色云、Cache Rule 是否真的命中、文件扩展名是否在缓存范围内。
改了文件但访问还是旧内容:边缘缓存没过期。要么 Purge 缓存,要么用带哈希的文件名做版本控制,后者才是长久做法。
前端直传报 CORS 错误:桶的 Settings 里配 CORS Policy,把允许的 Origin 写清楚,AllowedMethods 至少包含 PUT,需要读取再加 GET。
上传大文件失败:超过 5 GB 的对象必须走分片上传(Multipart Upload),普通 PutObject 单次上限是 5 GB,用 rclone 或 SDK 会自动处理。
怀疑被刷:看 R2 的请求量图表和域名的流量分析,对照 Class A/B 操作数。异常增长通常来自某个公开链接被批量请求,先上 Rate Limiting,再考虑给该路径加签名。