2025 年 Cloudflare 公布了一组数据:R2 存储的对象数量突破 10 万亿个,同比增长超过 300%。这个数字背后是一个简单但致命的优势——零出口流量费用。当 AWS S3 的出口带宽成本成为许多中小项目的心头大患时,R2 用「出口免费 + S3 兼容 API」的组合拳,直接改写了对象存储的游戏规则。如果你的应用还在为图片、视频、静态资源的存储和分发支付高额账单,这篇文章会帮你重新审视整个方案。
💰 一、R2 vs S3:成本对比与选型决策
在讨论技术细节之前,先算一笔账。对象存储的成本由三部分组成:存储容量费、请求次数费、出口流量费。其中出口流量费往往占总成本的 60%-80%,而这恰恰是 R2 的核心优势。
📊 真实成本对比
以一个典型的中型 Web 应用为例:月存储 100GB,月读取请求 500 万次,月出口流量 200GB。
| 费用项目 | AWS S3 (us-east-1) | Cloudflare R2 | 节省比例 |
|---|---|---|---|
| 存储容量 | $2.30/月 | $1.50/月 | 35% |
| 读取请求 (Class A) | $2.00/月 | $0.90/月 | 55% |
| 读取请求 (Class B) | $0.02/月 | $0.0036/月 | 82% |
| 出口流量 | $18.40/月 | $0.00 | 100% |
| 月总计 | $22.72 | $2.40 | 89% |
⚠️ 注意: 以上价格基于 2026 年 6 月的公开定价。S3 出口流量按 $0.09/GB 计算(前 100TB)。R2 的免费出口额度为每月 10GB,超出部分仍免费——这是 R2 与 S3 最本质的区别。
当流量增长到每月 1TB 时,S3 的出口费用飙升到 $92,而 R2 依然是 $0。对于 CDN 场景(大量小文件高频读取),差距更加明显。
🎯 什么场景适合 R2
- ✅ 静态资源托管:图片、CSS、JS、字体文件
- ✅ 用户上传内容:头像、文档、媒体文件
- ✅ CDN 源站:配合 Cloudflare CDN 实现全球加速
- ✅ 备份与归档:数据库备份、日志归档
- ✅ AI 模型与数据集:大文件存储,频繁读取
❌ 什么场景暂时不适合 R2
- ❌ 需要 S3 Select、Athena 等高级分析功能
- ❌ 需要跨区域复制(R2 目前支持自动多区域,但配置灵活性不如 S3)
- ❌ 深度依赖 AWS 生态(Lambda@S3、Glue 等)
🔧 二、R2 核心功能与 API 实战
R2 提供了两种访问方式:S3 兼容 API(适合已有 S3 代码迁移)和 Workers Binding API(性能更好,延迟更低)。我们分别来看。
📦 通过 S3 兼容 API 操作 R2
R2 完全兼容 S3 API,这意味着你可以直接使用 AWS SDK 操作 R2,只需修改 endpoint 配置。
// 使用 AWS SDK v3 操作 Cloudflare R2
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3'
// 配置 R2 客户端 —— 只需修改 endpoint 即可从 S3 迁移
const r2Client = new S3Client({
region: 'auto',
endpoint: 'https://<ACCOUNT_ID>.r2.cloudflarestorage.com',
credentials: {
accessKeyId: '<R2_ACCESS_KEY_ID>',
secretAccessKey: '<R2_SECRET_ACCESS_KEY>',
},
})
// 上传文件
async function uploadFile(key, body, contentType) {
const command = new PutObjectCommand({
Bucket: 'my-app-assets',
Key: key,
Body: body,
ContentType: contentType,
// R2 支持自定义元数据
Metadata: {
'uploaded-by': 'api',
'version': '1.0',
},
})
const result = await r2Client.send(command)
return result
}
// 上传示例:将 Buffer 写入 R2
const imageBuffer = await fs.readFile('./hero-banner.webp')
await uploadFile('images/2026/hero-banner.webp', imageBuffer, 'image/webp')
💡 提示: 从 S3 迁移到 R2 时,90% 的代码无需修改。唯一需要调整的是
endpoint和credentials。Cloudflare 提供了rclone工具可以一键迁移已有 S3 数据。
⚡ 通过 Workers Binding 操作 R2(推荐)
Workers Binding 是 Cloudflare 推荐的方式,它绕过了 S3 API 的 HTTP 开销,直接在边缘节点操作存储,延迟更低。
// Workers Binding 方式操作 R2 —— 延迟更低,代码更简洁
export default {
async fetch(request, env) {
const url = new URL(request.url)
const key = url.pathname.slice(1) // 去掉开头的 /
// GET 请求:从 R2 读取文件
if (request.method === 'GET') {
const object = await env.MY_BUCKET.get(key)
if (!object) {
return new Response('Not Found', { status: 404 })
}
const headers = new Headers()
headers.set('Content-Type', object.httpMetadata?.contentType || 'application/octet-stream')
headers.set('Cache-Control', 'public, max-age=31536000, immutable')
headers.set('ETag', object.httpEtag)
return new Response(object.body, { headers })
}
// PUT 请求:上传文件到 R2
if (request.method === 'PUT') {
const contentType = request.headers.get('Content-Type') || 'application/octet-stream'
await env.MY_BUCKET.put(key, request.body, {
httpMetadata: { contentType },
customMetadata: { uploadedAt: new Date().toISOString() },
})
return new Response(JSON.stringify({ success: true, key }), {
headers: { 'Content-Type': 'application/json' },
})
}
return new Response('Method not allowed', { status: 405 })
},
}
Workers Binding 的性能优势很明显:同一个 Cloudflare 边缘节点内,R2 读取延迟通常在 5-15ms,而通过 S3 API 走公网则需要 50-200ms(取决于区域)。
🔐 安全访问控制
R2 支持两种安全模型:API Token(适合服务端)和 Presigned URL(适合客户端直传)。
// 生成预签名 URL —— 让前端直接上传到 R2,不经过你的服务器
// 这是生产环境最常用的模式,节省服务器带宽
async function generatePresignedUploadUrl(env, key, expiresIn = 3600) {
// 使用 r2-client 库生成 presigned URL
const url = await env.MY_BUCKET.createPresignedUrl({
key,
method: 'PUT',
expiresIn, // 秒
// 可选:限制文件类型
})
return url
}
// 在 Worker 中提供上传接口
// POST /api/upload-url { "filename": "photo.jpg", "type": "image/jpeg" }
async function handleUploadRequest(request, env) {
const { filename, type } = await request.json()
// 生成唯一文件名,避免冲突
const key = `uploads/${Date.now()}-${crypto.randomUUID()}/${filename}`
// 生成 presigned URL
const uploadUrl = await env.MY_BUCKET.createPresignedUrl({
key,
method: 'PUT',
expiresIn: 3600,
})
// 前端拿到这个 URL 后,直接 PUT 上传,不经过你的服务器
return Response.json({
uploadUrl,
key,
// 最终访问地址
publicUrl: `https://cdn.yourdomain.com/${key}`,
})
}
⚠️ 警告: 永远不要在前端代码中硬编码 R2 API Token。Presigned URL 是唯一安全的客户端直传方案。每个 presigned URL 应该设置合理的过期时间(建议 15-60 分钟)。
🚀 三、实战:用 Workers + R2 构建图片处理 CDN
这是最经典的 R2 使用场景:用户上传原始图片,通过 Workers 在边缘节点实时处理(裁剪、压缩、格式转换),然后通过 CDN 分发。整个流程无需额外的图片处理服务。
🏗️ 架构设计
用户上传 → Workers 生成 Presigned URL → 直传 R2
↓
用户请求 → CDN → Workers 检查缓存 → R2 读取原图 → 实时处理 → 返回
↓
命中缓存直接返回
核心思想:图片处理发生在边缘节点,而不是你的服务器。Cloudflare Workers 支持 cf.image 属性,可以在 Workers 层面直接对图片做变换。
// 完整的图片处理 CDN Worker
export default {
async fetch(request, env) {
const url = new URL(request.url)
const key = url.pathname.slice(1)
// 解析图片变换参数
// 示例 URL: /images/photo.jpg?width=800&format=webp&quality=85
const width = parseInt(url.searchParams.get('width')) || 0
const format = url.searchParams.get('format') || 'auto'
const quality = parseInt(url.searchParams.get('quality')) || 85
// Step 1: 检查变体缓存
const cacheKey = buildCacheKey(key, width, format, quality)
const cached = await env.IMAGE_CACHE.get(cacheKey, { type: 'arrayBuffer' })
if (cached) {
return new Response(cached, {
headers: buildImageHeaders(format, cached.byteLength, true),
})
}
// Step 2: 从 R2 读取原始图片
const original = await env.MY_BUCKET.get(key)
if (!original) {
return new Response('Image not found', { status: 404 })
}
// Step 3: 使用 Cloudflare Image Resizing 在边缘处理
// 注意:需要在 Cloudflare Dashboard 中启用 Image Resizing 功能
const imageResponse = new Response(original.body, {
headers: { 'Content-Type': original.httpMetadata?.contentType || 'image/jpeg' },
})
const resizeOptions = { fit: 'scale-down' }
if (width > 0) resizeOptions.width = width
if (format === 'webp') resizeOptions.format = 'image/webp'
else if (format === 'avif') resizeOptions.format = 'image/avif'
// 使用 cf.image 进行边缘图片处理
const resized = await fetch(request.url, {
cf: {
image: {
...resizeOptions,
quality,
},
},
})
// Step 4: 缓存处理后的图片到 R2 KV(或 Cache API)
const processedBuffer = await resized.arrayBuffer()
await env.IMAGE_CACHE.put(cacheKey, processedBuffer, {
expirationTtl: 86400 * 30, // 30 天过期
})
return new Response(processedBuffer, {
headers: buildImageHeaders(format, processedBuffer.byteLength, false),
})
},
}
function buildCacheKey(key, width, format, quality) {
return `${key}:w${width}:f${format}:q${quality}`
}
function buildImageHeaders(format, size, fromCache) {
return {
'Content-Type': format === 'webp' ? 'image/webp' : format === 'avif' ? 'image/avif' : 'image/jpeg',
'Cache-Control': 'public, max-age=31536000, immutable',
'X-Cache': fromCache ? 'HIT' : 'MISS',
'X-Image-Size': size.toString(),
}
}
📌 记住:
cf.image是 Cloudflare 付费功能(Pro 计划 $20/月起)。如果预算有限,可以用纯 Workers 代码实现基础的图片压缩和格式转换,或者使用开源库如sharp的 WASM 版本。
🌐 配置自定义域名与 CDN
R2 Bucket 支持直接绑定自定义域名,无需额外配置 CDN,因为 Cloudflare 本身就是 CDN。
# 在 Cloudflare Dashboard 中配置:
# 1. 进入 R2 → 选择 Bucket → 设置 → 自定义域名
# 2. 添加域名:cdn.yourdomain.com
# 3. Cloudflare 自动配置 DNS 记录和 SSL 证书
# 或者通过 Wrangler CLI 配置
npx wrangler r2 bucket domain add my-app-assets --domain cdn.yourdomain.com
配置完成后,https://cdn.yourdomain.com/images/photo.jpg 会自动通过 Cloudflare 的 300+ 全球节点分发,无需任何额外配置。
📈 性能优化技巧
| 优化策略 | 实现方式 | 效果 |
|---|---|---|
| 多格式适配 | 根据 Accept 头自动返回 WebP/AVIF |
体积减少 30%-50% |
| 响应式图片 | URL 参数控制宽度,前端用 srcset |
移动端流量减少 60% |
| 边缘缓存 | Cache API + R2 双层缓存 | 命中率 >95% |
| 懒加载分发 | 小图直接内联,大图懒加载 | 首屏加载提速 40% |
| 渐进式 JPEG | 上传时转为 Progressive JPEG | 感知加载速度提升 |
💡 四、最佳实践与避坑指南
经过在多个生产项目中的实践,总结以下关键经验:
✅ 推荐做法
- ✅ 使用 Workers Binding 而非 S3 API:同一个请求链路内,Binding 比 S3 API 快 3-5 倍
- ✅ 设置合理的 Cache-Control 头:静态资源用
max-age=31536000, immutable,动态资源用短缓存 - ✅ 使用 Lifecycle Rules 自动清理:设置 30 天后删除临时文件,90 天后转为 Infrequent Access 存储类
- ✅ 启用 S3 API 的 CORS 配置:前端直传场景必须配置,否则浏览器会拦截
- ✅ 为每个环境(dev/staging/prod)使用独立 Bucket:避免测试数据污染生产环境
❌ 避免做法
- ❌ 不要在 Workers 中同步读取大文件:R2 单次读取上限 5GB,大文件应该用流式读取
- ❌ 不要忽略错误处理:R2 偶尔会返回 500 错误,必须有重试机制
- ❌ 不要用 R2 做数据库:R2 是对象存储,不适合高频小文件读写(用 KV 或 D1)
- ❌ 不要在 key 中使用中文或特殊字符:虽然技术上支持,但会导致 CDN 缓存 key 不一致
⚠️ 常见坑点
坑点 1:R2 的 eventual consistency
R2 在对象覆写(PUT 同一个 key)时是最终一致的,不是强一致的。这意味着你上传一个新版本后,立即读取可能返回旧版本。
// ❌ 错误:上传后立即读取,可能拿到旧版本
await bucket.put('config.json', newData)
const result = await bucket.get('config.json') // 可能是旧数据!
// ✅ 正确:使用版本化 key 或在元数据中标记版本
const versionedKey = `config/v${Date.now()}.json`
await bucket.put(versionedKey, newData)
// 更新指针(用 KV 存储当前版本号)
await env.CONFIG_KV.put('current-version', versionedKey)
坑点 2:Workers 的 CPU 时间限制
Workers 免费版有 10ms CPU 时间限制,付费版有 30ms。图片处理等 CPU 密集操作很容易超限。
💡 提示: 对于复杂图片处理,考虑使用 Cloudflare Images 服务($1/月/1000 张),它专门优化了图片处理性能,比在 Workers 中处理更高效。
坑点 3:大文件上传超时
Workers 请求超时为 30 秒(免费版)或 60 秒(付费版)。上传大文件(>100MB)时应该使用 Multipart Upload。
// R2 Multipart Upload —— 上传大文件的正确方式
async function uploadLargeFile(env, key, fileStream, totalSize) {
// Step 1: 创建 multipart upload
const multipartUpload = await env.MY_BUCKET.createMultipartUpload(key)
const partSize = 10 * 1024 * 1024 // 每个分片 10MB
const parts = []
let partNumber = 1
// Step 2: 逐片上传
for (let offset = 0; offset < totalSize; offset += partSize) {
const chunk = fileStream.slice(offset, offset + partSize)
const uploadedPart = await multipartUpload.uploadPart(partNumber, chunk)
parts.push(uploadedPart)
partNumber++
}
// Step 3: 完成上传
const object = await multipartUpload.complete(parts)
return object
}
🎯 总结与选型建议
Cloudflare R2 不是 S3 的简单替代品,而是一种不同的存储哲学。S3 是一个功能极其丰富的平台,适合深度 AWS 生态用户;R2 则专注于「存储 + 分发」这个核心场景,用零出口费用和边缘计算的优势打差异化。
我的建议:
- 如果你的应用 80% 的场景是存储文件并分发给用户,R2 是更好的选择,成本可以降低 80%-90%
- 如果你需要 S3 Select、Athena、Glue 等高级数据分析功能,继续用 S3
- 如果你已经在用 Cloudflare(CDN、DNS、Workers),R2 是天然的补充,几乎没有集成成本
- 迁移策略:新项目直接用 R2,老项目可以先将 CDN 源站切换到 R2,保留 S3 作为备份
相关工具推荐:
- 🔧 Wrangler CLI — Cloudflare 官方命令行工具,管理 R2 Bucket 和 Workers
- 🔧 rclone — 跨云存储同步工具,支持 S3 → R2 一键迁移
- 🔧 Cloudflare Images — 专业图片处理服务,适合高并发图片场景
- 🔧 S3 Browser — Windows 下的 R2 管理工具,S3 兼容