Cloudflare R2 高级实战:多区域复制、生命周期策略与 Workers 流式处理

深入 Cloudflare R2 生产级能力:跨区域复制架构、S3 事件通知替代方案、对象生命周期自动归档、Workers 流式上传与大文件分片、图片/视频优化管道(Image Resizing + Stream),含成本模型对比与多云容灾设计。

前置阅读:建议先阅读 Cloudflare R2 对象存储实战

关键概念:R2 的核心差异化优势是零出口费(Zero Egress),这意味着将数据从 R2 分发到全球的成本远低于 AWS S3。对于高流量分发场景,成本节省可达 80%+。

  1. ² 跨区域复制与一致性

    R2 的多区域复制不是原生功能,需通过 Workers + Rules 实现:

    // workers/r2-replication.ts
    export interface Env {
      R2_PRIMARY: R2Bucket;   // 主区域 bucket
      R2_BACKUP: R2Bucket;    // 备份区域 bucket
      REPLICATION_QUEUE: Queue;
    }
    
    export default {
      // 监听 PUT 事件(通过 R2 Event Notifications 或 Object Lifecycle Rules)
      async fetch(request: Request, env: Env) {
        const url = new URL(request.url);
    
        if (url.pathname === "/replicate") {
          const { bucket, key } = await request.json();
    
          // 跨区域复制
          const source = await env.R2_PRIMARY.get(key);
          if (!source) return new Response("Not found", { status: 404 });
    
          await env.R2_BACKUP.put(key, source.body, {
            httpMetadata: source.httpMetadata,
            customMetadata: {
              ...source.customMetadata,
              replicated_at: new Date().toISOString(),
              source_bucket: bucket,
            },
          });
    
          return Response.json({ replicated: true, key });
        }
    
        return new Response("OK");
      },
    
      // Queue Consumer:异步批量复制
      async queue(batch: MessageBatch<any>, env: Env) {
        for (const message of batch.messages) {
          const { key } = message.body;
          const source = await env.R2_PRIMARY.get(key);
          if (source) {
            await env.R2_BACKUP.put(key, source.body, {
              httpMetadata: source.httpMetadata,
            });
          }
          message.ack();
        }
      },
    };
    

    复制一致性模型

    策略延迟一致性适用场景
    同步复制+200-500ms强一致金融交易记录
    异步 Queue~1-5s最终一致常规文件、日志
    定时批处理~1h最终一致冷备份、归档
  2. ³ 对象生命周期管理

    R2 原生支持生命周期规则(S3 兼容 API):

    # 通过 wrangler 配置生命周期(当前需通过 S3 API 或 Dashboard)
    # 规则示例:30天后转低频,90天后删除
    
    // workers/lifecycle-manager.ts
    // 自定义生命周期:基于 Workers 定时触发(Cron Triggers)
    
    export default {
      async scheduled(event: ScheduledEvent, env: Env) {
        // 每天执行一次清理
        const { objects } = await env.R2_BUCKET.list({
          prefix: "uploads/temp/",
        });
    
        const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000;  // 7 天前
        const toDelete = [];
    
        for (const obj of objects) {
          const uploaded = new Date(obj.uploaded).getTime();
          if (uploaded < cutoff) {
            toDelete.push(obj.key);
          }
        }
    
        // 批量删除(R2 支持一次最多 1000 个)
        if (toDelete.length > 0) {
          // 由于 R2 目前不原生支持批量删除 API,循环删除:
          await Promise.all(
            toDelete.map(key => env.R2_BUCKET.delete(key))
          );
          console.log(`Deleted ${toDelete.length} expired objects`);
        }
      },
    };
    
    # wrangler.toml
    [triggers]
    crons = ["0 2 * * *"]  # 每天凌晨 2 点
    
  3. ⁴ Workers 流式上传与大文件处理

    // workers/stream-upload.ts
    export default {
      async fetch(request: Request, env: Env) {
        if (request.method !== "POST") {
          return new Response("Method not allowed", { status: 405 });
        }
    
        const url = new URL(request.url);
        const key = url.searchParams.get("key");
        if (!key) return new Response("Missing key", { status: 400 });
    
        // 直接流式转发(不加载到内存)
        // 适合大文件:视频、备份、日志
        await env.R2_BUCKET.put(key, request.body, {
          httpMetadata: {
            contentType: request.headers.get("content-type") || "application/octet-stream",
          },
        });
    
        return Response.json({ uploaded: true, key });
      },
    };
    

    分片上传(Multipart Upload)

    // workers/multipart-upload.ts
    // R2 支持 S3 兼容的分片上传
    import { S3Client, CreateMultipartUploadCommand,
             UploadPartCommand, CompleteMultipartUploadCommand } from "@aws-sdk/client-s3";
    import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
    
    const s3 = new S3Client({
      region: "auto",
      endpoint: `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`,
      credentials: {
        accessKeyId: env.R2_ACCESS_KEY_ID,
        secretAccessKey: env.R2_SECRET_ACCESS_KEY,
      },
    });
    
    // 步骤 1: 初始化分片上传
    export async function initiateMultipart(key: string) {
      const command = new CreateMultipartUploadCommand({
        Bucket: "my-bucket",
        Key: key,
      });
      const response = await s3.send(command);
      return response.UploadId;
    }
    
    // 步骤 2: 为每个分片生成预签名 URL(客户端直传)
    export async function getPresignedPartUrls(
      key: string,
      uploadId: string,
      totalParts: number
    ) {
      const urls = [];
      for (let i = 1; i <= totalParts; i++) {
        const command = new UploadPartCommand({
          Bucket: "my-bucket",
          Key: key,
          UploadId: uploadId,
          PartNumber: i,
        });
        const url = await getSignedUrl(s3, command, { expiresIn: 3600 });
        urls.push({ partNumber: i, url });
      }
      return urls;
    }
    
    // 步骤 3: 完成上传
    export async function completeMultipart(
      key: string,
      uploadId: string,
      parts: { PartNumber: number; ETag: string }[]
    ) {
      const command = new CompleteMultipartUploadCommand({
        Bucket: "my-bucket",
        Key: key,
        UploadId: uploadId,
        MultipartUpload: { Parts: parts },
      });
      return s3.send(command);
    }
    
  4. ⁵ 图片/视频动态优化管道

    使用 Cloudflare Image Resizing + Workers 构建动态图片服务:

    // workers/image-optimization.ts
    export default {
      async fetch(request: Request, env: Env) {
        const url = new URL(request.url);
        const key = url.pathname.slice(1);  // 去掉前导 /
        const width = parseInt(url.searchParams.get("w") || "0");
        const format = url.searchParams.get("f") || "auto";
        const quality = parseInt(url.searchParams.get("q") || "85");
    
        // 检查缓存(KV 中存储已处理的变体)
        const cacheKey = `img:${key}:w=${width}:f=${format}:q=${quality}`;
        const cached = await env.CACHE.get(cacheKey, { type: "stream" });
        if (cached) {
          return new Response(cached, {
            headers: { "Content-Type": `image/${format === "auto" ? "webp" : format}` },
          });
        }
    
        // 获取原始图片
        const original = await env.R2_BUCKET.get(key);
        if (!original) return new Response("Not found", { status: 404 });
    
        // 使用 Cloudflare Image Resizing(需开通 Image Resizing 服务)
        // 或者使用 Workers 集成 sharp/wasm(实验性)
        const imageUrl = new URL(
          `https://imagedelivery.net/${env.IMAGE_ACCOUNT_HASH}/${key}/public`
        );
    
        if (width) imageUrl.searchParams.set("width", width.toString());
        if (format !== "auto") imageUrl.searchParams.set("format", format);
        imageUrl.searchParams.set("quality", quality.toString());
    
        const optimized = await fetch(imageUrl);
    
        // 缓存处理结果(24小时)
        const body = await optimized.arrayBuffer();
        await env.CACHE.put(cacheKey, body, { expirationTtl: 86400 });
    
        return new Response(body, {
          headers: {
            "Content-Type": optimized.headers.get("Content-Type") || "image/webp",
            "Cache-Control": "public, max-age=86400",
          },
        });
      },
    };
    

    图片处理参数速查

    参数说明示例
    w / width最大宽度?w=800
    h / height最大高度?h=600
    f / format输出格式?f=webp / ?f=avif
    q / quality质量(1-100)?q=85
    fit适应模式?fit=crop / ?fit=contain
  5. ⁶ 成本模型对比

    月度假设:存储 5TB,月出口流量 20TB,1000 万次请求

    服务商存储费出口费请求费月总计vs R2
    Cloudflare R2$0$0~$4~$4基准
    AWS S3 Standard$115$1,800$50$1,965490x
    AWS S3 + CloudFront$115$1,200$85$1,400350x
    Google Cloud Storage$104$1,600$40$1,744435x
    MinIO (自托管)$200 (VPS)含在带宽-$20050x

    注意:R2 的 “免费” 有使用上限。Pro 以上套餐含更大额度,超额后请求费 $0.36/百万。

  6. ⁷ 多云容灾架构

    ┌─────────────┐     写     ┌─────────────┐
    │  应用服务器  │──────────→│   R2 (主)    │
    │  (Workers)  │            │  零出口费分发  │
    └─────────────┘            └──────┬──────┘
           │                          │
           │  异步复制 (Queue)         │ 读取
           ▼                          ▼
    ┌─────────────┐            ┌─────────────┐
    │  S3 (备份)   │            │   全球 CDN   │
    │  合规/冷备   │            │  <50ms 延迟  │
    └─────────────┘            └─────────────┘
    

    容灾切换逻辑

    class MultiCloudStorage {
      constructor(
        private primary: R2Bucket,
        private backup: S3Client,
        private healthCheck: () => Promise<boolean>
      ) {}
    
      async get(key: string) {
        try {
          if (await this.healthCheck()) {
            return await this.primary.get(key);
          }
        } catch (e) {
          console.warn("Primary failed, falling back to S3");
        }
        // 灾备读取
        return this.backup.send(new GetObjectCommand({ Bucket: "backup", Key: key }));
      }
    }
    

延伸阅读

继续阅读

探索更多技术文章

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

全部文章 返回首页

「工具与平台」更多文章