为什么零出口费重要
AWS S3 按出口流量收费:美东到互联网 $0.09/GB。一个月 10TB 出口就是 $900。R2 把这部分直接砍到 0——存储费几乎一样,出口完全不花钱。
| 项目 | AWS S3(us-east-1) | Cloudflare R2 |
|---|---|---|
| 存储 | $0.023/GB·月 | $0.015/GB·月 |
| 出口流量 | $0.09/GB | $0 (全免) |
| Class A 操作 | $5/百万 | $4.50/百万 |
| Class B 操作 | $0.4/百万 | $0.36/百万 |
唯一注意:R2 不在自己家的"区域"概念上,而是全球自动多副本。你不用选 region。
创建 Bucket 并绑定
wrangler r2 bucket create my-assets # wrangler.toml 里加 # [[r2_buckets]] # binding = "ASSETS" # bucket_name = "my-assets"
Workers 原生 API
type Bindings = { ASSETS: R2Bucket }; // 上传 app.put('/upload/:key', async (c) => { const key = c.req.param('key'); const body = c.req.raw.body; // ReadableStream 流式上传 await c.env.ASSETS.put(key, body, { httpMetadata: { contentType: c.req.header('content-type') ?? 'application/octet-stream', cacheControl: 'public, max-age=31536000', }, customMetadata: { uploader: c.req.header('x-user') ?? 'anon' }, }); return c.json({ ok: true, key }); }); // 下载 app.get('/file/:key', async (c) => { const obj = await c.env.ASSETS.get(c.req.param('key')); if (!obj) return c.text('not found', 404); const headers = new Headers(); obj.writeHttpMetadata(headers); headers.set('etag', obj.httpEtag); return new Response(obj.body, { headers }); }); // 列表 app.get('/list', async (c) => { const r = await c.env.ASSETS.list({ prefix: 'photos/', limit: 100 }); return c.json(r.objects.map(o => ({ key: o.key, size: o.size }))); });
分块上传(大文件)
超过 100MB 的文件建议用 Multipart Upload:
// 初始化 const mp = await c.env.ASSETS.createMultipartUpload('videos/big.mp4'); // 每 10MB 一个 part const part1 = await mp.uploadPart(1, chunk1); const part2 = await mp.uploadPart(2, chunk2); // 合并 await mp.complete([part1, part2]);
Presigned URL:让浏览器直传 R2
想让前端不经 Worker 直接上传大文件,传统思路是 Presigned URL。R2 开了 S3 Access Key 以后可以复用 AWS SDK:
import { AwsClient } from 'aws4fetch'; const r2 = new AwsClient({ accessKeyId: c.env.R2_ACCESS_KEY, secretAccessKey: c.env.R2_SECRET_KEY, }); app.post('/presign', async (c) => { const { key, contentType } = await c.req.json(); const url = new URL(`https://<account>.r2.cloudflarestorage.com/my-assets/${key}`); url.searchParams.set('X-Amz-Expires', '300'); const signed = await r2.sign(new Request(url, { method: 'PUT' }), { aws: { signQuery: true } }); return c.json({ uploadUrl: signed.url }); });
前端拿到 uploadUrl,直接 PUT,就绕过了你的 Worker。
与 CDN 结合:公开读
R2 自己不做 CDN(它是源站)。要公开访问,两种方式:
- 自定义域名:Dashboard 设置
assets.example.com直接指向 bucket,Cloudflare CDN 自动加速 - Worker 中转:Worker 做鉴权 + 缓存,用户通过 Worker URL 访问(能加逻辑)
// Worker 中转 + Cache API app.get('/img/:key', async (c) => { const cache = caches.default; let res = await cache.match(c.req.raw); if (res) return res; const obj = await c.env.ASSETS.get(c.req.param('key')); if (!obj) return c.text('nope', 404); res = new Response(obj.body, { headers: { 'cache-control': 'public, max-age=86400', 'content-type': obj.httpMetadata?.contentType ?? '' }, }); c.executionCtx().waitUntil(cache.put(c.req.raw, res.clone())); return res; });
事件通知:写入触发 Queue
R2 支持把 PUT/DELETE 事件发到 Queue,典型用于"上传图片后自动生成缩略图":
wrangler r2 bucket notification create my-assets \
--event-type object-create \
--queue thumb-gen-queue
Queue 消费者(下章详讲)收到后调 Images API 生成缩略图,再 put 回 bucket。
从 S3 迁移:Super Slurper
Cloudflare 提供 Super Slurper——Dashboard 里点按钮,输入 S3 凭据,R2 自动把整个 bucket 拷贝过来。TB 级数据一夜迁完。
Data Catalog(Iceberg)
2025 年 R2 新增 Data Catalog,把 R2 包成 Apache Iceberg 的表格存储。能被 DuckDB / Spark / Trino 查询,把 R2 变成"零出口费的数据湖"。
-- DuckDB 直接查 R2 Iceberg 表 INSTALL iceberg; CREATE SECRET r2_cat (TYPE ICEBERG, TOKEN '...'); SELECT COUNT(*) FROM iceberg_scan('r2://my-lake/events');
避坑清单
- 🙅 用 Worker 中转但不加 cache——每次都 R2 回源,白白增加 Class B 操作费
- 🙅 小对象狂写(< 1KB / 秒级)——Class A 操作费会超存储费,合并写
- 🙅 不设 content-type——浏览器不知道怎么渲染,下载对象而不是预览
- 🙅 依赖 S3 区域 API——R2 没 region,某些 AWS SDK 需要指定
auto
本章小结
- R2 = S3 兼容对象存储,零出口费是杀手锏
- Workers 原生 API 简洁:put / get / list / delete,流式上下行
- 大文件用 Multipart;前端直传用 Presigned URL
- 公开访问通过自定义域名或 Worker + Cache API
- Event Notification 连 Queue,做异步处理流水线
- Super Slurper 从 S3 迁移,Data Catalog 把 R2 变成 Iceberg 数据湖