【免费下载链接】autoskills
One command. Your entire AI skill stack. Installed.
本指南是 Cloudflare Durable Objects(DO)生产环境排错的实战手册,聚焦
packages/autoskills/skills-registry/cloudflare-deploy/references/durable-objects/gotchas.md中收录的 15 类高频故障,覆盖内存态丢失、定时任务失效、构造器重复执行、503 过载、存储配额、CPU 超时、WebSocket 断连、迁移失败、RPC 兼容性与并发竞态等核心场景。读完本文,你将掌握每类故障的根因分析、正确修复代码与可落地的规避策略,并理解 DO 完整限制表与 Hibernation 机制的全部注意点。
一、先建立正确的认知:DO 的生命周期决定了故障根源
要理解 Durable Objects 的大部分"诡异"故障,必须先理解它的生命周期状态机。根据 durable-objects/README.md 中的定义,DO 实例会经历以下状态流转:
[Not Created] → [Active] ⇄ [Hibernated] → [Evicted] ↓ [Destroyed]- Not Created:DO ID 已存在但实例从未被唤醒;
- Active:正在处理请求,内存态有效,按 GB-小时计费;
- Hibernated:WebSocket 连接保持打开但零计算、零成本(内存已被清理);
- Evicted:实例被移出内存,下次请求触发冷启动;
- Destroyed:通过迁移或手动删除销毁数据。
绝大多数 gotchas 都源于两个关键事实:DO 空闲时会自动 Hibernate(休眠),甚至可能直接 Evict(驱逐);每次唤醒(无论是冷启动还是从 Hibernation 唤醒)构造函数都会重新执行。下面进入逐个故障的排错详解。
二、Common Errors:15 类高频故障逐项拆解
1. "Hibernation Cleared My In-Memory State"——休眠清空了内存状态
- 问题:DO 休眠后,类成员变量全部丢失;
- 根因:DO 空闲时自动 Hibernation,纯内存数据不持久化;
- 修复:关键数据写入
ctx.storage,每连接元数据用ws.serializeAttachment()持久化。
// ❌ 错误 - 休眠后丢失 private userCount = 0; async webSocketMessage(ws: WebSocket, msg: string) { this.userCount++; // Lost! } // ✅ 正确 - 持久化到存储 async webSocketMessage(ws: WebSocket, msg: string) { const count = this.ctx.storage.kv.get("userCount") || 0; this.ctx.storage.kv.put("userCount", count + 1); }注意:这里用到的是ctx.storage.kv(SQLite DO 上的同步 KV API)。如果使用传统 KV-only DO,则应使用异步 APIawait this.ctx.storage.get("key")/put。关于三种存储 API 的取舍,详见 durable-objects/api.md 与 do-storage/README.md。
2. "setTimeout Didn't Fire After Restart"——重启后定时任务没触发
- 问题:基于
setTimeout的调度在实例被驱逐后消失; - 根因:
setTimeout只存在于内存中,Eviction 会清除所有定时器; - 修复:改用
ctx.storage.setAlarm()实现可靠的定时调度。
// ❌ 错误 - 驱逐即丢失 setTimeout(() => this.cleanup(), 3600000); // ✅ 正确 - 跨驱逐存活 await this.ctx.storage.setAlarm(Date.now() + 3600000); async alarm() { await this.cleanup(); }Alarm 的可靠性语义(来自 api.md):Alarm 会跨 DO 驱逐/重启存活;Cloudflare 会自动重试失败的 Alarm;但不保证 exactly-once,业务处理需要幂等设计。同时要注意setAlarm()会覆盖已有 Alarm,且每个 DO 最多只有一个 Alarm。
3. "Constructor Runs on Every Wake"——构造函数每次唤醒都执行
- 问题:昂贵的初始化逻辑拖慢所有请求;
- 根因:构造函数在两种场景都会执行——冷启动(DO 被驱逐后首次请求创建新实例)和从 Hibernation 唤醒(带 WebSocket 休眠的 DO 收到消息或 Alarm 时被唤醒);
- 修复:改用懒加载,或把初始化结果缓存到 storage。
// ❌ 错误 - 每次唤醒都做昂贵加载 constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); this.heavyData = this.loadExpensiveData(); // Slow! } // ✅ 正确 - 懒加载 private heavyData?: HeavyData; private getHeavyData() { if (!this.heavyData) this.heavyData = this.loadExpensiveData(); return this.heavyData; }这是 DO 设计与普通类最大的心智差异:构造函数不是"只跑一次",而是"每次唤醒都跑"。与此配套的还有ctx.blockConcurrencyWhile()——需要一次性初始化(如建表、schema 迁移)时,用它包住初始化代码可保证首次请求期间没有其他请求并发进入。
4. "Durable Object Overloaded (503 errors)"——单 DO 过载报 503
- 问题:高负载下出现 503 错误;
- 根因:单个 DO 实例吞吐量超过约 1K req/s 的软上限;
- 修复:跨多个 DO 分片(Sharding)。
分片思路见 patterns.md:用idFromName("shard:" + hash % N)将负载均匀分散到 N 个分片,典型分片数 10~1000,建议从 100 开始测量后调整。同时客户端应实现 503 重试 + 退避。
5. "Storage Quota Exceeded (Write failures)"——存储配额超限写入失败
- 问题:写入操作持续失败;
- 根因:DO 存储超过单实例 10GB 上限,或超出账号级配额(Free 计划 SQLite 总存储 5GB);
- 修复:用 Alarm 定时清理旧数据、对过期数据执行
deleteAll()、或升级套餐。
async alarm() { // 清理过期数据(SQLite) this.ctx.storage.sql.exec("DELETE FROM old_data WHERE timestamp < ?", cutoff); }6. "CPU Time Exceeded (Terminated)"——CPU 时间超限被终止
- 问题:请求执行中途被终止;
- 根因:单请求处理超过默认 30s CPU 时间上限;
- 修复:在 wrangler.jsonc 中调大
limits.cpu_ms(最大 300s),或将工作切分为小块。
{ "limits": { "cpu_ms": 300000 // Max CPU time: 30s default, 300s max } }配置位置与更多选项见 configuration.md。
7. "WebSockets Disconnect on Eviction"——WebSocket 意外断连
- 问题:连接无故断开;
- 根因:未使用 Hibernation API 的 DO 在实例被驱逐时,其上的 WebSocket 连接随之断开;
- 修复:使用 WebSocket Hibernation 处理器(
ctx.acceptWebSocket()+webSocketMessage/webSocketClose/webSocketError),同时客户端实现指数退避重连。
// 服务端:启用 hibernation 并持久化连接元数据 async fetch(req: Request): Promise<Response> { const [client, server] = Object.values(new WebSocketPair()); this.ctx.acceptWebSocket(server, ["room:123"]); server.serializeAttachment({ userId: "abc" }); return new Response(null, { status: 101, webSocket: client }); }客户端指数退避重连与断线清理示例均收录于 patterns.md 的 "WebSocket Reconnection" 小节。
8. "Migration Failed (Deploy error)"——部署时迁移失败
- 根因:迁移标签(tag)不唯一、标签不按顺序递增,或 class 名无效;
- 修复:检查标签唯一性与顺序(v1、v2、v3...),并核对 class 名与导出的类一致。
迁移规则(来自 configuration.md):标签必须唯一且顺序递增;不支持回滚;部署时自动应用;new_sqlite_classes优先于new_classes(SQLite vs KV);deleted_classes会立即销毁全部数据(不可逆)。部署前务必用npx wrangler deploy --dry-run校验。
9. "RPC Method Not Found"——RPC 方法找不到
- 根因:
compatibility_date早于 2024-04-03,导致无法使用 RPC 调用; - 修复:将
compatibility_date更新到 ≥ 2024-04-03,或改用fetch()调用。
const count = await stub.increment(); // RPC(需 compat ≥ 2024-04-03) const count = await (await stub.fetch(req)).json(); // fetch()(兼容旧版)RPC 与 fetch() 的选择树同样见 durable-objects/README.md:新项目 + compat ≥ 2024-04-03 用 RPC(类型安全、更简单);需要 HTTP 语义(header、status)、代理请求到 DO、或兼容旧项目时用 fetch()。
10. "Only One Alarm Allowed"——每个 DO 只有一个 Alarm
- 根因:需要调度多个定时任务,但每个 DO 仅支持一个 Alarm;
- 修复:采用事件队列模式(Event Queue Pattern),用单个 Alarm 驱动多个任务。
队列模式的完整实现(来自 patterns.md):
async scheduleEvent(id: string, runAt: number) { await this.ctx.storage.put(`event:${id}`, { id, runAt }); const curr = await this.ctx.storage.getAlarm(); if (!curr || runAt < curr) await this.ctx.storage.setAlarm(runAt); } async alarm() { const events = await this.ctx.storage.list({ prefix: "event:" }), now = Date.now(); let next = null; for (const [key, ev] of events) { if (ev.runAt <= now) { await this.processEvent(ev); await this.ctx.storage.delete(key); } else if (!next || ev.runAt < next) next = ev.runAt; } if (next) await this.ctx.storage.setAlarm(next); }核心思想:新事件先入库,若其执行时间早于当前 Alarm 则重置 Alarm;Alarm 触发时扫描队列,处理到期事件并把 Alarm 续到下一个最近事件。
11. "Race Condition Despite Single-Threading"——单线程下仍有竞态
- 问题:并发请求观察到不一致的状态;
- 根因:DO 虽是单线程串行处理请求,但await 是让出点,异步操作期间其他请求可以交错执行;
- 修复:关键区用
blockConcurrencyWhile()加锁,或直接用原子存储操作。
// ❌ 错误 - 存在竞态 async incrementCounter() { const count = await this.ctx.storage.get("count") || 0; // ⚠️ await 期间另一个请求可能执行到这里 await this.ctx.storage.put("count", count + 1); } // ✅ 正确 - 原子 SQL 操作(推荐) async incrementCounter() { return this.ctx.storage.sql.exec( "INSERT INTO counters (id, value) VALUES (1, 1) ON CONFLICT(id) DO UPDATE SET value = value + 1 RETURNING value" ).one().value; } // ✅ 正确 - 显式临界区加锁 async criticalOperation() { await this.ctx.blockConcurrencyWhile(async () => { const count = await this.ctx.storage.get("count") || 0; await this.ctx.storage.put("count", count + 1); }); }关于 SQLite 与ctx.storage.sql的原子语义、事务支持,可进一步阅读 do-storage/README.md(其中也包含并发门控、INTEGER 精度、SQL 限制等 storage 专属 gotchas)。
12. "Migration Rollback Not Supported"——迁移不支持回滚
- 根因:尝试在部署后回滚迁移;
- 修复:部署前必须用
--dry-run测试;迁移一旦部署不可回滚。
13. "deleted_classes Destroys Data"——迁移误删全部数据
- 问题:一次迁移操作导致所有数据丢失;
- 根因:
deleted_classes迁移会立即销毁所有 DO 实例及其全部数据; - 修复:先用
--dry-run验证;迁移移动数据时使用transferred_classes以保留数据。
{ "migrations": [ { "tag": "v1", "new_sqlite_classes": ["MyDO"] }, // 创建 SQLite(推荐) // { "tag": "v1", "new_classes": ["MyDO"] }, // 创建 KV(仅付费) { "tag": "v2", "renamed_classes": [{ "from": "Old", "to": "New" }] }, { "tag": "v3", "transferred_classes": [{ "from": "Src", "from_script": "old", "to": "Dest" }] }, { "tag": "v4", "deleted_classes": ["Obsolete"] } // 销毁全部数据! ] }14. "Cold Starts Are Slow"——冷启动缓慢
- 问题:驱逐后的首次请求耗时更长;
- 根因:冷启动时构造函数 + 首次存储访问的开销;
- 修复:这是预期行为——优化构造函数、客户端使用连接池、对关键 DO 采用预热(Warming)策略。
// 预热策略:定期 ping 关键 DO export default { async scheduled(event: ScheduledEvent, env: Env) { const criticalIds = ["auth", "sessions", "locks"]; await Promise.all(criticalIds.map(name => { const id = env.MY_DO.idFromName(name); const stub = env.MY_DO.get(id); return stub.ping(); // Keep warm })); } };15. 其余条目速览
- "Migration Failed (Deploy error)":检查标签唯一/顺序、class 名正确性(见上文第 8 条详细展开);
- 完整的 RPC/迁移/配额/并发条目均已在上文逐条展开,此处不再重复。
三、Limits:Durable Objects 完整限制表
| 限制项 | Free | Paid | 说明 |
|---|---|---|---|
| 每个 DO 的 SQLite 存储 | 10 GB | 10 GB | 按 Durable Object 实例计 |
| SQLite 总存储 | 5 GB | Unlimited | 账号级配额 |
| 单 KV 键值大小 | 2 MB | 2 MB | SQLite/async KV 单个 KV 对 |
| CPU 时间默认 | 30s | 30s | 每请求;可配置 |
| CPU 时间最大 | 300s | 300s | 通过limits.cpu_ms设置 |
| DO 类数量 | 100 | 500 | 不同的 DO 类定义数 |
| SQL 列数 | 100 | 100 | 每表 |
| SQL 语句大小 | 100 KB | 100 KB | 最大 SQL 查询尺寸 |
| WebSocket 消息大小 | 32 MiB | 32 MiB | 每条消息 |
| 请求吞吐 | ~1K req/s | ~1K req/s | 每 DO(软限制,分片可提升) |
| 每 DO Alarm 数 | 1 | 1 | 多事件需队列模式 |
| 总 DO 数量 | Unlimited | Unlimited | 可按需创建任意多实例 |
| WebSockets | Unlimited | Unlimited | 受每 DO 128MB 内存限制约束 |
| 每 DO 内存 | 128 MB | 128 MB | 内存态 + WebSocket 缓冲区 |
这张表是排查第 4、5、6、10 条故障时的第一参照物:例如单 DO 吞吐软上限 ~1K req/s 直接对应 503 过载问题;每 DO 128MB 内存约束意味着 WebSocket 连接过多同样会挤压内存态空间。
四、Hibernation Caveats:休眠机制的 6 条关键注意点
- 内存被清空——所有内存变量丢失,需从 storage 重建或用
deserializeAttachment()恢复; - 构造函数重跑——唤醒时再次执行,避免昂贵操作,使用懒初始化;
- 无保证——DO 可能被 Evict 而非 Hibernate,设计时两者都要兼容;
- 附件大小限制——
serializeAttachment()数据必须是 JSON 可序列化的,且保持小体积; - Alarm 唤醒 DO——Alarm 在执行期间会阻止 DO 进入休眠,直到 handler 完成;
- WebSocket 状态不会自动持久化——必须显式用
serializeAttachment()或 storage 保存。
第 4 点补充说明:attachment 是每连接的元数据(如{ userId: "abc", room: "lobby" }),与全局 storage 是两套机制——前者跟着 WebSocket 连接走,后者是 DO 实例级持久化。二者配合使用才能构建"休眠后仍可正确路由消息"的实时应用。
五、从 Gotchas 反推的最佳实践清单
综合以上 15 类故障与限制表,可以从 patterns.md 的 Best Practices 中提炼出与 gotchas 一一对应的防御性设计:
- 设计:协调类场景用
idFromName()(限流、锁、会话),高吞吐用newUniqueId()分片;构造函数保持轻量; - 存储:优先 SQLite,用事务批量写入;Alarm 负责定时清理;高风险操作前使用 Point-in-Time Recovery(PITR)备份;
- 性能:单 DO ~1K req/s 上限——需要更高吞吐就分片;内存态做缓存;延迟工作交给 Alarm;
- 可靠性:503 用重试 + 退避;为冷启动做设计;迁移一律先
--dry-run; - 安全:在 Worker 层校验输入、对 DO 创建做限流、用 jurisdiction 满足数据合规。
六、排错入口与延伸阅读
本指南对应的完整参考体系位于仓库的packages/autoskills/skills-registry/cloudflare-deploy/references/durable-objects/目录,建议按以下顺序深入:
- durable-objects/README.md —— DO 总览、生命周期状态机、RPC vs fetch 决策树、快速上手;
- durable-objects/configuration.md —— wrangler.jsonc 绑定、迁移配置、环境隔离、
limits.cpu_ms; - durable-objects/api.md —— 类结构、ctx 方法(
waitUntil/blockConcurrencyWhile)、Alarm API、WebSocket Hibernation 处理器; - durable-objects/patterns.md —— 分片、限流、分布式锁、会话管理、队列模式、重连策略;
- do-storage/README.md —— SQLite/KV 存储 API、事务、PITR 与 storage 专属 gotchas;
- 本指南主体(本文所依据的 durable-objects/gotchas.md)——常见错误速查、完整限制表、Hibernation 注意点。
此外,cloudflare-deploy/SKILL.md 给出了平台级决策树:需要"有状态协调 / 实时能力"时选 Durable Objects,需要"强一致的按实体状态"时选 DO Storage。日常开发命令(npx wrangler dev/wrangler dev --remote/wrangler deploy/wrangler deploy --dry-run/wrangler durable-objects list|info|delete)与迁移验证方式详见 configuration.md,这里不再赘述。
一句话总结:Durable Objects 的绝大多数坑都源于"内存不持久 + 每次唤醒重跑构造函数 + 单 Alarm + 单实例吞吐软上限"这四个底层事实。写代码时把关键状态放进 storage、用 Alarm 替代 setTimeout、用原子 SQL 替代读-改-写、迁移前先 dry-run,就能绕开 80% 的生产事故。
【免费下载链接】autoskills
One command. Your entire AI skill stack. Installed.
相关推荐
Cloudflare Agents SDK 实战指南:在 Durable Objects 上构建有状态、实时、全局分布式 AI Agent
Cloudflare Agents SDK 实战指南:在 Durable Objects 上构建有状态、实时、全局分布式 AI Agent Cloudflare
人工智能AI 技能AI 插件Cloudflare Agents SDK 完全指南:用 Durable Objects 构建有状态 AI Agent 的 API 详解
Cloudflare Agents SDK 完全指南:用 Durable Objects 构建有状态 AI Agent 的 API 详解 导读 本文基于 aut
编译Elixir源码时踩过的坑:从报错到解决的实战指南
编译Elixir源码时踩过的坑:从报错到解决的实战指南 你是否在编译Elixir源码时遇到过"make: No targets specified and no
编程语言编译器标准库语言运行时并发编程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考