Cloudflare R2 对象存储实战:零出口费的 S3 替代方案

Cloudflare R2 是兼容 S3 API 的对象存储服务,最大亮点是彻底取消出口流量费,配合 Workers 可构建零服务器的文件分发架构。本文从成本测算、SDK 接入、Workers 绑定到 S3 迁移,完整讲解 R2 实战用法。

Cloudflare R2 是 Cloudflare 推出的 S3 兼容对象存储服务:它提供与 AWS S3 几乎一致的 API,可以直接用 AWS SDK 操作,但与 S3 最大的区别在于——R2 不收取任何出口流量费(egress fee),数据从 R2 分发到全球用户完全免费。这直接命中了 S3 最被诟病的成本黑洞:当你的文件被大量下载时,S3 的出口费往往是存储费的数倍甚至数十倍。

R2 典型适用场景:

  • 静态资源图床与文件托管
  • SaaS 产品的用户上传文件存储
  • 备份与归档
  • 配合 Cloudflare CDN 的全球内容分发
  • 从 S3 迁移以降低带宽成本

对 Cloudflare 平台整体还不熟悉的读者,可以先阅读 Cloudflare 详解:从 CDN 到全球边缘计算平台

为什么 R2 便宜:成本模型对比

对象存储的账单由三部分构成:存储费、请求费、出口流量费。三者的定价策略决定了总成本。

项目AWS S3(us-east-1)Cloudflare R2
存储费$0.023 / GB / 月$0.015 / GB / 月
Class A 请求(写/列)$5.00 / 百万次$4.50 / 百万次
Class B 请求(读)$0.40 / 百万次$0.36 / 百万次
出口流量费约 $0.09 / GB(前 10TB)$0
免费额度5GB 存储(仅首年)10GB 存储 + 每月 100 万次 Class A + 1000 万次 Class B(长期有效)

做一个典型场景测算:一个图床类 SaaS,存储 500GB 图片,每月被下载 5TB,读请求 2000 万次。

  • S3 账单:存储 500 × $0.023 ≈ $11.5 + 请求约 $8 + 出口 5000GB × $0.09 ≈ $450,合计约 $470/月,其中 95% 是出口费。
  • R2 账单:存储 500 × $0.015 = $7.5 + 请求约 $7.2 + 出口 $0,合计约 $15/月

流量越大,差距越夸张。这正是"零出口费"三个字的含金量:它把对象存储从"按流量计费"变成了"按容量计费",成本曲线变得可预测。

快速上手:开通 R2 并获取 API Token

  1. 登录 Cloudflare Dashboard,左侧菜单选择 R2 Object Storage,首次使用需开通(免费额度内不扣费,但需要绑定支付方式)。
  2. 点击 Create bucket,输入 bucket 名称(全局唯一,小写字母与连字符),可选择 location hint(如 apac 亚太、eeur 东欧),创建完成。
  3. 获取 API 凭证:进入 R2 概览页,点击右侧 Manage R2 API TokensCreate API Token,选择权限范围(建议按需只授予目标 bucket 的读写权限),创建后会得到三样东西:
    • Access Key ID
    • Secret Access Key(只显示一次,务必保存)
    • 账号 ID(Account ID),用于拼接 endpoint

R2 的 S3 兼容 endpoint 格式固定为:

https://<account_id>.r2.cloudflarestorage.com

有了 endpoint + Access Key + Secret,任何 S3 工具链都能直接操作 R2。

S3 兼容实战:用 AWS SDK 连接 R2

JavaScript(AWS SDK v3)

import { S3Client, PutObjectCommand, GetObjectCommand,
         ListObjectsV2Command, DeleteObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const r2 = new S3Client({
  region: 'auto', // R2 必须填 auto
  endpoint: 'https://<account_id>.r2.cloudflarestorage.com',
  credentials: {
    accessKeyId: process.env.R2_ACCESS_KEY_ID,
    secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
  },
});

// 上传
await r2.send(new PutObjectCommand({
  Bucket: 'my-bucket',
  Key: 'images/avatar.png',
  Body: fileBuffer,
  ContentType: 'image/png',
}));

// 列举
const list = await r2.send(new ListObjectsV2Command({ Bucket: 'my-bucket', Prefix: 'images/' }));

// 删除
await r2.send(new DeleteObjectCommand({ Bucket: 'my-bucket', Key: 'images/avatar.png' }));

// 生成预签名 URL(有效期 1 小时,常用于让前端直传)
const uploadUrl = await getSignedUrl(
  r2,
  new PutObjectCommand({ Bucket: 'my-bucket', Key: 'uploads/' + crypto.randomUUID() }),
  { expiresIn: 3600 }
);

预签名 URL 是生产环境的标配做法:服务器只负责签发短时效 URL,文件流量不经过你的应用服务器,前端直接 PUT 到 R2,既省带宽又安全。

Python(boto3)

import boto3

s3 = boto3.client(
    's3',
    endpoint_url='https://<account_id>.r2.cloudflarestorage.com',
    aws_access_key_id='<access_key_id>',
    aws_secret_access_key='<secret_access_key>',
    region_name='auto',
)

s3.upload_file('local.jpg', 'my-bucket', 'images/local.jpg')
obj = s3.get_object(Bucket='my-bucket', Key='images/local.jpg')
data = obj['Body'].read()

url = s3.generate_presigned_url(
    'get_object',
    Params={'Bucket': 'my-bucket', 'Key': 'images/local.jpg'},
    ExpiresIn=3600,
)

要点只有一个:region 必须设为 auto,endpoint 指向你的账号专属地址,其余代码与操作 S3 完全相同。

Workers 绑定实战:免密钥访问 R2

如果你的应用跑在 Cloudflare Workers 上,不需要任何 Access Key——通过 binding 绑定后,Worker 可以直接以原生 API 操作 bucket,延迟极低。(Workers 本身的入门指南见 Cloudflare Workers 入门实战

wrangler.toml 配置:

name = "file-service"
main = "src/index.js"
compatibility_date = "2026-07-01"

[[r2_buckets]]
binding = "MY_BUCKET"      # 代码中通过 env.MY_BUCKET 访问
bucket_name = "my-bucket"

Worker 代码:

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    const key = url.pathname.slice(1);

    if (request.method === 'PUT') {
      // request.body 是 ReadableStream,直接流式写入 R2,不占内存
      await env.MY_BUCKET.put(key, request.body, {
        httpMetadata: { contentType: request.headers.get('content-type') },
        customMetadata: { uploader: 'user-123' },
      });
      return new Response('Uploaded', { status: 201 });
    }

    if (request.method === 'GET') {
      const object = await env.MY_BUCKET.get(key);
      if (object === null) return new Response('Not Found', { status: 404 });

      const headers = new Headers();
      object.writeHttpMetadata(headers);
      headers.set('etag', object.httpEtag);
      // object.body 是流,大文件不经过 Worker 内存直接转发给客户端
      return new Response(object.body, { headers });
    }

    if (request.method === 'DELETE') {
      await env.MY_BUCKET.delete(key);
      return new Response(null, { status: 204 });
    }
  },
};

R2Object 上的常用属性:body(ReadableStream)、httpEtagsizeuploadedcustomMetadatahttpMetadataget 还支持 range 参数做分段读取,配合流式响应可以轻松实现视频拖拽播放。list() 方法支持分页与前缀过滤,适合做文件管理后台。

这套"Workers + R2"组合就是 JAMstack 架构深度解析 中"边缘函数 + 边缘存储"理念的落地形态:没有源站、没有服务器,文件在 Cloudflare 网络内闭环。

典型应用场景

静态资源图床

创建 bucket 后,在 bucket 设置中开启 Public Access:可以直接启用 Cloudflare 分配的 r2.dev 子域名,更推荐绑定自定义域名(如 cdn.example.com,要求域名 DNS 托管在 Cloudflare)。绑定后文件即可通过 https://cdn.example.com/images/a.png 公开访问,且自动享有 CDN 缓存与 HTTPS。前端上传走预签名 URL 或 Workers 中转,读取走公开域名,读写分离。

备份与归档

数据库每日导出后推送到 R2,10GB 免费额度足够大多数个人项目。R2 支持生命周期规则(Lifecycle Rules),可自动删除超过 N 天的备份文件。

用户上传文件

SaaS 产品的头像、附件、导出文件等,用 Workers 做鉴权网关:验证登录态后从 R2 取文件返回,私有内容也能享受零出口费。

与 CDN 配合分发

R2 绑定自定义域名后天然跑在 Cloudflare CDN 上,可在 Cache Rules 中调整缓存策略,热门文件直接命中边缘缓存,回源 R2 的请求(Class B)费用也极低。与 Cloudflare Pages 完全指南 中托管的前端站点配合,就是一套完整的零出口费静态架构。

从 S3 迁移到 R2

rclone 全量迁移

rclone 原生支持 R2,配置一个 remote 即可:

rclone config
# 新建 remote:类型选 s3,provider 选 Cloudflare
# endpoint 填 https://<account_id>.r2.cloudflarestorage.com

# 迁移命令(先 dry-run 验证)
rclone sync s3src:source-bucket r2dst:my-bucket --progress --dry-run
rclone sync s3src:source-bucket r2dst:my-bucket --progress --transfers 16

sync 会按 key 和 checksum 增量同步,中断后重跑即可续传。

Sippy 增量迁移

对于不能停机的生产系统,Cloudflare 提供 Sippy:在 bucket 设置中配置源 S3 的凭证后,R2 会在每次对象被请求时自动从 S3 拉取并存入 R2(copy-on-read),未迁移的冷数据继续回源。流量逐步切换,最终实现无感迁移,迁完后关闭 Sippy 即可。

注意事项与 S3 差异

  • API 兼容性:R2 实现了 S3 的核心 API(PUT/GET/DELETE/列举/分片上传/预签名),但不支持 bucket ACL、对象级 ACL 等边缘特性,依赖这些功能的程序需要改造。
  • 一致性模型:R2 是强一致的——写入成功后立即可读,不需要像早期 S3 那样处理最终一致性问题。
  • 区域模型:R2 没有传统意义的 region,数据按 location hint(apaceeurwnam 等)就近存储,由 Cloudflare 网络负责全球分发。创建后 location 不可修改,只能在创建时通过 wrangler.tomllocation 字段或 Dashboard 指定。
  • r2.dev 限速:公开的 r2.dev 子域名有速率限制,生产环境务必绑定自定义域名。
  • 幂等与重试:SDK 默认重试策略即可,大文件务必使用分片上传(multipart),R2 完全兼容。

常见问题(FAQ)

Cloudflare R2 真的不收流量费吗?

真的。无论数据从 R2 流向互联网、流向 Workers 还是通过自定义域名经 CDN 分发,出口流量一律 $0。账单只包含存储费($0.015/GB/月)和请求费(Class A/B)。这也是 R2 与 S3、GCS、Azure Blob 最本质的商业模式差异。

R2 和 S3 的性能差距大吗?

写入和单次读取的绝对延迟 S3 略占优(同区域场景),但 R2 绑定自定义域名后读取走 Cloudflare 全球 CDN 缓存,终端用户的实际访问延迟通常比直连 S3 更低。对图床、静态资源、下载分发这类读多写少的场景,R2 体验往往更好;超高频小对象随机写场景建议实测后再定。

如何公开访问 R2 bucket 里的文件?

两种方式:一是在 bucket 设置中开启 r2.dev 公共开发 URL(有限速,仅适合测试);二是绑定自定义域名,要求该域名 DNS 托管在 Cloudflare,绑定后自动签发 HTTPS 证书并接入 CDN,这是生产推荐做法。私有文件则通过 Workers 鉴权读取或预签名 URL 访问。

R2 的免费额度是多少?

长期免费额度为:10GB 存储、每月 100 万次 Class A 操作(写、列)、每月 1000 万次 Class B 操作(读),出口流量永久免费。个人图床、博客附件、小型备份基本零成本。超出部分按量计费,单价也低于 S3。

R2 能直接当图床用吗?

完全可以,而且是目前性价比最高的图床方案之一:上传走预签名 URL 或 Workers 中转,访问走绑定的自定义域名,配合 CDN 缓存后绝大多数请求不产生 R2 操作费。配合自定义域名与缓存规则,10GB 免费额度能支撑一个中型内容站点的全部图片流量。

相关阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「saas」更多文章