Cloudflare Durable Objects 实战:在边缘构建有状态应用

深入解析 Cloudflare Durable Objects 的 Actor 模型、单实例保证、存储 API 和 WebSocket Hibernation,用完整代码示例教你构建实时协作应用、分布式限流器和任务队列。

DevOps 与部署 2026-05-30 15 分钟

Cloudflare Workers 让我们能在全球 300+ 个边缘节点运行无状态代码,但当你需要状态——比如实时协作编辑器的文档状态、聊天室的在线用户列表、或者分布式限流器的计数器——纯无状态的 Worker 就力不从心了。Durable Objects(持久对象)正是为解决这个问题而生的边缘原语,它提供了一个全球唯一、单实例运行、自带持久化存储的 Actor,让你在边缘优雅地管理有状态逻辑。

截至目前,Durable Objects 已经从一个实验性功能演进为 Cloudflare 平台上的一等公民,支撑了大量生产级应用。但很多开发者对它的理解还停留在「边缘数据库」的层面——实际上,它的设计哲学和使用模式远比这深刻。

🏗️ 一、Durable Objects 核心概念

1.1 Actor 模型:为什么需要单实例?

传统的无服务器架构中,每个请求可能被调度到不同的实例,多个实例并发处理同一个资源时,你必须依赖外部存储(Redis、数据库)来做状态同步和并发控制。这不仅增加了延迟,还引入了分布式一致性难题。

Durable Objects 采用经典的 Actor 模型:每个 Object 实例在全球范围内只有一个活跃副本,所有对该 Object 的请求都会被路由到同一个实例。这意味着:

  • 无需锁机制 — 单线程执行,天然无竞态条件
  • 内存状态即真实状态 — 实例内存中的数据就是最新的
  • 自动持久化 — 结合内置 Storage API,状态不会丢失
  • 不适合高吞吐写入 — 单实例是瓶颈,超过 ~1000 req/s 需要考虑分片
// ❌ 传统方式:无状态 Worker + Redis,需要处理并发
export default {
  async fetch(request, env) {
    const count = await env.REDIS.incr('rate:counter');
    if (count > 100) return new Response('Rate limited', { status: 429 });
    // 但这里的 incr 和后续操作不是原子的!
    return new Response('OK');
  }
};

// ✅ Durable Objects 方式:单实例保证,天然原子
export class RateLimiter {
  constructor(state, env) {
    this.state = state;
    this.count = 0;
    this.windowStart = Date.now();
  }

  async fetch(request) {
    const now = Date.now();
    if (now - this.windowStart > 60000) {
      this.count = 0;
      this.windowStart = now;
    }
    this.count++;
    if (this.count > 100) {
      return new Response(JSON.stringify({ allowed: false, remaining: 0 }), {
        status: 429,
        headers: { 'Content-Type': 'application/json' }
      });
    }
    return new Response(JSON.stringify({
      allowed: true,
      remaining: 100 - this.count
    }), { headers: { 'Content-Type': 'application/json' } });
  }
}

1.2 生命周期:实例何时创建、何时销毁?

理解 Durable Objects 的生命周期是避免踩坑的关键:

阶段 触发条件 行为
创建 首次通过 idFromName()newUniqueId() 访问 分配全球唯一 ID,在最近的边缘数据中心启动实例
活跃 有请求到达 实例保持在内存中,处理请求
休眠 ~10 秒无请求 实例被卸载,内存状态丢失(持久化数据保留)
唤醒 新请求到达 从持久化存储恢复状态,重新启动实例
销毁 通过 deleteAll() 或过期策略 清除所有持久化数据,不可恢复

⚠️ **警告:**实例休眠时内存中的临时状态会丢失。不要把重要数据只放在内存中——始终配合 Storage API 使用。

1.3 ID 策略:命名 ID vs 唯一 ID

// 入口 Worker 中创建 Durable Object 引用
export default {
  async fetch(request, env) {
    const url = new URL(request.url);

    // 命名 ID:相同名称总是路由到同一个实例
    // 适用于:用户级对象、文档级对象、房间级对象
    const userId = request.headers.get('X-User-Id');
    const userObjId = env.USER_STATE.idFromName(`user:${userId}`);
    const userObj = env.USER_STATE.get(userObjId);

    // 唯一 ID:每次调用生成新的唯一实例
    // 适用于:临时会话、一次性任务
    const sessionObjId = env.SESSION.newUniqueId();
    const sessionObj = env.SESSION.get(sessionObjId);

    return userObj.fetch(request);
  }
};

💡 **提示:**命名 ID 是确定性的——idFromName("user:123") 在任何数据中心都会得到相同的 ID。利用这个特性可以实现全球一致的用户状态。

🔥 二、三大实战场景

2.1 场景一:实时协作编辑器

这是 Durable Objects 最经典的用例。每个文档对应一个 Durable Object 实例,管理文档内容和连接到该文档的所有客户端的 WebSocket。

// document-do.js
export class DocumentEditor {
  constructor(state, env) {
    this.state = state;
    this.env = env;
    this.sessions = []; // WebSocket 连接
    this.document = '';  // 文档内容

    // 从持久化存储恢复文档
    this.state.blockConcurrencyWhile(async () => {
      const saved = await this.state.storage.get('document');
      if (saved) this.document = saved;
    });
  }

  async fetch(request) {
    const url = new URL(request.url);

    if (url.pathname === '/websocket') {
      // 升级为 WebSocket
      const pair = new WebSocketPair();
      const [client, server] = [pair[0], pair[1]];

      this.state.acceptWebSocket(server);
      this.sessions.push(server);

      // 发送当前文档内容给新连接的客户端
      server.send(JSON.stringify({
        type: 'init',
        content: this.document,
        users: this.sessions.length
      }));

      // 广播用户数变化
      this.broadcast({ type: 'users', count: this.sessions.length });

      return new Response(null, { status: 101, webSocket: client });
    }

    if (url.pathname === '/content') {
      return new Response(this.document, {
        headers: { 'Content-Type': 'text/plain' }
      });
    }

    return new Response('Not found', { status: 404 });
  }

  // WebSocket 消息处理
  async webSocketMessage(ws, message) {
    try {
      const data = JSON.parse(message);

      if (data.type === 'edit') {
        // 应用编辑操作(这里简化为全量替换)
        this.document = data.content;

        // 持久化到存储
        await this.state.storage.put('document', this.document);

        // 广播给其他客户端(排除发送者)
        this.broadcast({ type: 'update', content: this.document }, ws);
      }
    } catch (e) {
      ws.send(JSON.stringify({ type: 'error', message: e.message }));
    }
  }

  // WebSocket 关闭处理
  async webSocketClose(ws, code, reason) {
    this.sessions = this.sessions.filter(s => s !== ws);
    this.broadcast({ type: 'users', count: this.sessions.length });
  }

  broadcast(message, exclude) {
    const payload = JSON.stringify(message);
    for (const ws of this.sessions) {
      if (ws !== exclude) {
        try { ws.send(payload); } catch (e) { /* 忽略已断开的连接 */ }
      }
    }
  }
}

📌 **记住:**Durable Objects 的 WebSocket 支持 Hibernation 模式——当没有消息时,实例可以休眠,不消耗 CPU 时间。这对长连接场景(聊天室、协作编辑器)的成本优化至关重要。

2.2 场景二:分布式限流器(滑动窗口)

传统限流器要么用 Redis(增加延迟),要么用本地内存(不准确)。Durable Objects 提供了一个既准确又低延迟的方案。

// rate-limiter-do.js
export class SlidingWindowRateLimiter {
  constructor(state, env) {
    this.state = state;
    this.windowMs = 60000; // 1 分钟窗口
    this.maxRequests = 100;

    this.state.blockConcurrencyWhile(async () => {
      const saved = await this.state.storage.get('requests');
      this.requests = saved || [];
    });
  }

  async fetch(request) {
    const now = Date.now();
    const windowStart = now - this.windowMs;

    // 清理过期请求记录
    this.requests = this.requests.filter(ts => ts > windowStart);

    const currentCount = this.requests.length;
    const allowed = currentCount < this.maxRequests;

    if (allowed) {
      this.requests.push(now);
      // 持久化(异步,不阻塞响应)
      this.state.storage.put('requests', this.requests);
    }

    // 计算窗口重置时间
    const resetTime = this.requests.length > 0
      ? this.requests[0] + this.windowMs
      : now + this.windowMs;

    const headers = {
      'Content-Type': 'application/json',
      'X-RateLimit-Limit': String(this.maxRequests),
      'X-RateLimit-Remaining': String(Math.max(0, this.maxRequests - this.requests.length)),
      'X-RateLimit-Reset': String(Math.ceil(resetTime / 1000)),
    };

    if (!allowed) {
      const retryAfter = Math.ceil((resetTime - now) / 1000);
      headers['Retry-After'] = String(retryAfter);
      return new Response(JSON.stringify({
        error: 'Too many requests',
        retryAfter
      }), { status: 429, headers });
    }

    return new Response(JSON.stringify({
      allowed: true,
      remaining: this.maxRequests - this.requests.length
    }), { headers });
  }
}

性能对比:

方案 延迟 准确性 全球一致性 成本
Redis + Lua 脚本 1-5ms(同区域) 需要 Redis 集群
本地内存 <0.1ms 低(多实例不一致)
Durable Objects <1ms(边缘最近) ✅ 全球唯一实例 按请求计费
Cloudflare Rate Limiting <0.5ms 免费额度有限

2.3 场景三:轻量级任务队列

Durable Objects 可以作为边缘任务队列,处理不需要持久化到传统消息队列的轻量异步任务。

// task-queue-do.js
export class TaskQueue {
  constructor(state, env) {
    this.state = state;
    this.tasks = [];
    this.processing = false;

    this.state.blockConcurrencyWhile(async () => {
      this.tasks = (await this.state.storage.get('tasks')) || [];
    });
  }

  async fetch(request) {
    const url = new URL(request.url);

    if (request.method === 'POST' && url.pathname === '/enqueue') {
      const task = await request.json();
      task.id = crypto.randomUUID();
      task.status = 'pending';
      task.createdAt = Date.now();
      this.tasks.push(task);

      await this.state.storage.put('tasks', this.tasks);

      // 如果没有在处理,启动处理循环
      if (!this.processing) {
        this.processQueue();
      }

      return new Response(JSON.stringify({ queued: true, taskId: task.id }), {
        headers: { 'Content-Type': 'application/json' }
      });
    }

    if (url.pathname === '/status') {
      return new Response(JSON.stringify({
        pending: this.tasks.filter(t => t.status === 'pending').length,
        total: this.tasks.length
      }), { headers: { 'Content-Type': 'application/json' } });
    }

    return new Response('Not found', { status: 404 });
  }

  async processQueue() {
    this.processing = true;

    while (this.tasks.some(t => t.status === 'pending')) {
      const task = this.tasks.find(t => t.status === 'pending');
      if (!task) break;

      task.status = 'processing';
      await this.state.storage.put('tasks', this.tasks);

      try {
        // 执行任务(示例:发送 webhook)
        await this.executeTask(task);
        task.status = 'completed';
      } catch (e) {
        task.status = 'failed';
        task.error = e.message;
        task.retries = (task.retries || 0) + 1;

        // 重试逻辑
        if (task.retries < 3) {
          task.status = 'pending';
        }
      }

      await this.state.storage.put('tasks', this.tasks);
    }

    this.processing = false;
  }

  async executeTask(task) {
    // 根据任务类型执行不同逻辑
    switch (task.type) {
      case 'webhook':
        await fetch(task.url, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(task.payload)
        });
        break;
      case 'cleanup':
        // 清理过期数据
        const cutoff = Date.now() - (task.maxAge || 86400000);
        this.tasks = this.tasks.filter(t =>
          t.status === 'completed' && t.createdAt > cutoff
        );
        break;
      default:
        throw new Error(`Unknown task type: ${task.type}`);
    }
  }
}

⚙️ 三、生产环境最佳实践

3.1 wrangler.toml 配置

# wrangler.toml
name = "my-app"
main = "src/worker.js"
compatibility_date = "2026-01-01"

# Durable Objects 绑定
[[durable_objects.bindings]]
name = "DOCUMENT"
class_name = "DocumentEditor"

[[durable_objects.bindings]]
name = "RATE_LIMITER"
class_name = "SlidingWindowRateLimiter"

[[durable_objects.bindings]]
name = "TASK_QUEUE"
class_name = "TaskQueue"

# Durable Objects 迁移(版本管理)
[[migrations]]
tag = "v1"
new_classes = ["DocumentEditor", "SlidingWindowRateLimiter", "TaskQueue"]

# 存储配置
[vars]
ENVIRONMENT = "production"

3.2 成本模型与容量规划

Durable Objects 的计费包含三部分:

计费项 免费额度 超出价格 说明
请求次数 100 万次/月 $0.15 / 百万次 每次 fetch() 调用计一次
持久化存储写入 1 GB·小时 $0.20 / GB·小时 存储在 Object 内部的数据
持久化存储读取 1 GB·小时 $0.20 / GB·小时 读取存储的数据
WebSocket 消息 100 万条/月 $0.08 / 百万条 通过 WebSocket 发送的消息

⚠️ **警告:**Durable Objects 的请求费用比普通 Workers 高约 10 倍。不要把所有逻辑都放到 DO 中——只把需要状态管理的部分放在 DO 里,无状态逻辑仍然用普通 Worker 处理。

3.3 Alarm API:定时任务

Durable Objects 支持 Alarm API,可以在指定时间唤醒实例执行任务,无需外部 Cron 服务。

// alarm-do.js
export class ScheduledTask {
  constructor(state, env) {
    this.state = state;
  }

  async fetch(request) {
    const url = new URL(request.url);

    if (url.pathname === '/schedule') {
      const { delayMs, taskData } = await request.json();
      const alarmTime = Date.now() + delayMs;

      // 设置 alarm(每个 DO 只能有一个活跃 alarm)
      await this.state.storage.put('taskData', taskData);
      await this.state.storage.setAlarm(alarmTime);

      return new Response(JSON.stringify({
        scheduled: true,
        executeAt: new Date(alarmTime).toISOString()
      }));
    }

    return new Response('OK');
  }

  // alarm 触发时调用
  async alarm() {
    const taskData = await this.state.storage.get('taskData');
    if (!taskData) return;

    try {
      // 执行定时任务
      await fetch(taskData.callbackUrl, {
        method: 'POST',
        body: JSON.stringify(taskData)
      });

      // 如果需要重复执行,设置下一次 alarm
      if (taskData.repeat) {
        await this.state.storage.setAlarm(Date.now() + taskData.intervalMs);
      } else {
        await this.state.storage.delete('taskData');
      }
    } catch (e) {
      // alarm 失败时重试(指数退避)
      const retryCount = (taskData.retryCount || 0) + 1;
      if (retryCount <= 5) {
        taskData.retryCount = retryCount;
        await this.state.storage.put('taskData', taskData);
        await this.state.storage.setAlarm(Date.now() + 1000 * Math.pow(2, retryCount));
      }
    }
  }
}

3.4 常见坑点与避坑指南

坑点 1:并发请求的隐式排队

Durable Objects 是单线程的,但 Cloudflare 会将并发请求排队。如果你的 fetch handler 中有 await,后续请求会等待前一个请求完成。

// ❌ 错误:长时间操作阻塞后续请求
async fetch(request) {
  const result = await this.longRunningOperation(); // 耗时 5 秒
  return new Response(result);
}

// ✅ 正确:使用 waitUntil() 将长时间操作放到后台
async fetch(request) {
  const promise = this.longRunningOperation();
  this.state.waitUntil(promise); // 不阻塞响应
  return new Response('Processing started');
}

坑点 2:存储 API 的配额限制

每个 Durable Object 的存储上限是 50 GB(可申请提升),但更关键的限制是:单个 put() 调用的值不能超过 128 KB,单个 get() 最多返回 1 MB。对于大对象,需要分片存储。

坑点 3:跨数据中心迁移

当一个 Durable Object 被频繁访问时,Cloudflare 会将它迁移到离请求源最近的数据中心。但这个迁移不是即时的——在迁移过程中,请求会被路由到旧位置。如果你的应用对延迟敏感,需要考虑这个行为。

💡 四、Durable Objects vs 替代方案

特性 Durable Objects Redis + Workers Cloudflare KV Cloudflare D1
一致性模型 强一致(单实例) 强一致(同区域) 最终一致 强一致
读延迟 <1ms 1-5ms <1ms 1-10ms
写延迟 <1ms 1-5ms <1ms(传播需 60s) 5-50ms
WebSocket 支持 ✅ 原生
适用场景 有状态实时应用 缓存、计数器 配置、静态数据 关系型查询
全球分布 ✅ 自动 需要集群 单区域

关键结论:如果你的应用需要强一致性 + 实时通信 + 边缘低延迟,Durable Objects 是目前唯一的选择。如果只需要简单的键值存储,用 KV 或 D1 更经济。

🎯 总结

Durable Objects 不是银弹,但它填补了边缘计算中有状态处理的空白。在以下场景中,它是最佳选择:

  • ✅ 实时协作应用(文档编辑、白板、代码协作)
  • ✅ 分布式限流和计数器
  • ✅ 聊天室和在线状态管理
  • ✅ 边缘任务调度和 Alarm
  • ✅ 需要全局唯一性的分布式锁

避免在以下场景使用:

  • ❌ 高吞吐写入(>1000 req/s 单实例瓶颈)
  • ❌ 大量数据存储(50 GB 上限,且成本高于 R2)
  • ❌ 纯读取场景(KV 更经济)

相关资源:

📚 相关文章