Chapter 04

R2:零出口费的对象存储

R2 对标 S3,API 兼容,但有一条杀手锏——出口流量免费。对图片、视频、ML 数据集这类读多于写的场景,直接砍掉 AWS 账单大头。

为什么零出口费重要

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(它是源站)。要公开访问,两种方式:

  1. 自定义域名:Dashboard 设置 assets.example.com 直接指向 bucket,Cloudflare CDN 自动加速
  2. 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');

避坑清单

本章小结