Cloudflare R2 + Workers 实战:构建低成本全球对象存储与 CDN 全指南

深度解析 Cloudflare R2 零出口费用对象存储,对比 AWS S3 成本,实战 Workers 集成图片处理 CDN,含完整代码与架构方案

DevOps 与部署 2026-06-04 15 分钟

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% 的代码无需修改。唯一需要调整的是 endpointcredentials。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 兼容

📚 相关文章