AI 参与说明(Agent:Codex):本文由 Codex 根据作者提供的主题与 Cloudflare 官方文档调研、整理和校验。本文按 2026-08-23 可访问的资料编写;Dynamic Workers、Durable Objects 的 Beta 状态、限额与计费均可能变化,部署前请复核文末一手资料。
结论:Durable Object Facets 不是 Durable Objects 的替代品,也不是另一种独立数据库产品。它是一个让父 Durable Object(supervisor)托管子 Durable Object class 的组合能力:子对象可以是运行时通过 Dynamic Workers 加载的代码,并拥有与父对象彼此隔离的 SQLite 数据库。它解决的核心问题是:既要让 AI 或用户生成的代码保留持久状态,又不把一个可自由创建的 Durable Object namespace、宿主数据或平台权限直接交给这段代码。
如果代码由自己部署、业务实体需要独立寻址和分片,例如聊天室、库存项、限流桶或协作房间,通常继续选 普通 Durable Objects。只有在“动态/可扩展代码 + 隔离状态 + 父级治理”同时成立时,Facets 才是更贴切的模型。
Cloudflare 在 2026-04-13 的发布公告中把 Facets 称为 open beta;当前 Dynamic Workers 价格页说明 Dynamic Workers 仅向 Workers Paid 提供。因此不应把它当作已经承诺长期稳定 API 的 GA 基础设施;上线前需再次核对状态与价格。发布公告 当前价格页
先建立正确的心智模型#
Facets 的典型架构有三层:
- Supervisor:由平台开发者编写、部署并拥有 namespace 的普通 SQLite-backed Durable Object。它保存授权、代码版本、配额或审计元数据。
- Dynamic Worker code:通过 Worker Loader API 在运行时加载,导出一个继承
DurableObject的 class。 - Facet:由 supervisor 内的
this.ctx.facets.get()创建或恢复的子对象。每个 facet name 都对应一个独立 SQLite 数据库。
flowchart TD C["客户端 / 入口 Worker"] --> S["Supervisor Durable Object<br/>认证、配额、代码版本与审计"] S --> PS["Supervisor SQLite<br/>元数据、ACL、账单计数"] S --> F["ctx.facets.get(name, factory)"] F --> DW["Dynamic Worker 中导出的<br/>DurableObject class"] DW --> FS["Facet SQLite<br/>业务状态"] S --> R["facet.fetch() 或 RPC 转发"] R --> F
父数据库与每个 facet 数据库属于同一个 overall Durable Object,但它们是相互隔离的:动态代码不能读取或修改 supervisor 数据库,supervisor 也不能通过 storage API 直接读取 facet 数据库。两侧若需要交换信息,应经过显式的 fetch()、RPC 或宿主授予的 capability,而不是共用一张表。官方概念与 API
官方文档把 Dynamic Workers 作为主要入口,但 ctx.facets 是 Durable Object 的 child/composition API,而不是仅限“AI 代码”的营销术语。例如 Cloudflare Agents SDK 的 Sub-agents 也使用 facets:只有顶层 parent 需要 namespace,child 可以作为 facet-only class 并拥有独立 SQLite。Cloudflare Agents SDK:Sub-agents
与普通 Durable Objects 的区别#
| 维度 | 普通 Durable Object | Durable Object Facet | 工程含义 |
|---|---|---|---|
| 产品定位 | 部署的 class + top-level Durable Object namespace。 | 由父 DO 托管的 child class;官方主场景是 Dynamic Workers。 | Facet 是组合层,不是 DO v2。 |
| 代码来源 | 随 Worker 部署,通常由平台开发者完全信任。 | 可在运行时从 Dynamic Worker 取得 class。 | 特别适合 AI 生成、用户上传或按租户变化的代码。 |
| 寻址入口 | 调用方通过 namespace、ID 与 stub 找到一个业务对象。 | 由父对象调用 ctx.facets.get(name, factory);没有单独的 top-level namespace。 | 外部请求先经过 supervisor,便于认证、审计和限额。 |
| 存储边界 | 每个顶层 DO 有自己的私有 storage。 | 每个 facet name 有独立 SQLite;与父数据库共同存放在一个 overall DO 内。 | 状态隔离很强,但不要把 facet 当成独立地域放置或独立扩缩容单元。后半句是由“同一 overall DO”推导出的工程约束,应压测验证。 |
| 身份 | 自己的 ctx.id 来自 namespace ID。 | 未提供 id 时,facet 继承 parent 的 ctx.id;可在 startup options 中显式提供 ID。 | 依赖对象 name/ID 的框架需显式设计子身份,不能默认它天然独立。 |
| 安全模型 | class 通常与 Worker 的 bindings、代码信任边界一致。 | SQLite 隔离;但网络与 bindings 是否可用仍取决于 Dynamic Worker 的 capability 配置。 | 数据库隔离不等于执行权限隔离。对不可信代码应默认 globalOutbound: null。 |
| 生命周期 | 按普通 DO 的部署、唤醒与 storage 生命周期管理。 | get() 创建/恢复;abort() 停止运行但保留数据库;delete() 停止并永久删除数据库。 | 可以热切换 child class,但 schema/data 兼容性仍由应用负责。 |
| Alarm | 每个顶层 DO 有一个原生 Alarm。 | Facet 没有独立 physical Alarm slot;Cloudflare Agents SDK 由 root parent 持有 Alarm 并转发子任务。 | 需要独立原生 Alarm 的业务实体,应优先建成顶层 DO 或让父对象统一调度。 |
| 成本与成熟度 | 按 Durable Objects 的用量模型评估。 | 采用 Dynamic Workers 的 Facet 模式还要纳入唯一代码、请求与 CPU 维度。 | 不要假定一个 facet 等于一份独立 DO 配额或固定成本;需要用真实流量压测和监控。 |
前七行均来自 Facets 官方文档;Alarm 的实现说明来自 Agents SDK Sub-agents 文档。Dynamic Worker 的出站与 capability 安全边界见 Bindings 和 Egress control。
最小工作路径#
以下示例使用 JavaScript,结构与 Cloudflare 当前 Facets 指南一致,但没有在本仓库部署到 Cloudflare account。它展示的是 API 关系;生产代码还必须完成鉴权、代码来源校验、限额、日志和 schema migration。
先配置一个 SQLite-backed supervisor 与 Worker Loader。这个配置沿用 Facets 指南中的 migrations 写法;新项目也可以按 Durable Objects class lifecycle 文档 使用 exports,但两种生命周期配置不能混用。
{
"$schema": "./node_modules/wrangler/config-schema.json",
"compatibility_date": "2026-08-23",
"main": "src/index.js",
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["AppRunner"]
}
],
"worker_loaders": [{ "binding": "LOADER" }]
}随后由 AppRunner 加载 dynamic class,并将请求转发给名为 app 的 facet:
import { DurableObject } from "cloudflare:workers";
const APP_SOURCE = [
'import { DurableObject } from "cloudflare:workers";',
"export class App extends DurableObject {",
" fetch() {",
' const count = (this.ctx.storage.kv.get("count") ?? 0) + 1;',
' this.ctx.storage.kv.put("count", count);',
" return Response.json({ count });",
" }",
"}",
].join("\n");
export class AppRunner extends DurableObject {
async fetch(request) {
const facet = this.ctx.facets.get("app", async () => {
const version = this.ctx.storage.kv.get("codeVersion") ?? "v1";
const worker = this.env.LOADER.get(
"app:" + this.ctx.id.toString() + ":" + version,
async () => ({
compatibilityDate: "2026-08-23",
mainModule: "app.js",
modules: { "app.js": APP_SOURCE },
globalOutbound: null,
}),
);
return { class: worker.getDurableObjectClass("App") };
});
return facet.fetch(request);
}
}
export default {
async fetch(request, _env, ctx) {
return ctx.exports.AppRunner.getByName("demo").fetch(request);
},
};这里有四个容易遗漏的事实:
get(“app”, factory)首次启动或 facet 从 hibernation 恢复时才调用factory;如果 facet 仍在运行,会复用现有实例而不再次运行 factory。- 返回值像普通 DO stub 一样,可调用
fetch(),也可调用 RPC 方法。 “app”是 facet 的存储名称;同一父对象里不同 name 会得到不同的 SQLite 数据库。globalOutbound: null只切断默认全局出站网络,不会替代对自定义 binding、参数、存储配额和业务授权的审查。
更新、停止与删除不是同一件事#
Facet API 将三种操作分得很清楚:
// 停止旧代码;旧 stub 会失效,但 SQLite 数据仍保留。
this.ctx.facets.abort("app", new Error("switching to code v2"));
// 下次 get() 会按 factory 重新启动,可返回新的 class。
const next = this.ctx.facets.get("app", factoryForV2);
// 停止并永久清空该 facet 的 SQLite 数据库。
this.ctx.facets.delete("app");因此,安全的更新流程通常是:在 supervisor 数据库保存不可变的 code version 与 source/bundle → 让新的 Dynamic Worker ID 同时包含版本、内容 hash 和权限版本 → 做好数据库兼容迁移 → abort() 旧 facet → 由下一次 get() 启动新版本。abort() 不会做数据迁移,delete() 则不可逆;这两点应成为运维 API 的明确语义。
Dynamic Workers 的价格页明确指出:同一个 ID 配上不同代码版本会按不同 Dynamic Worker 计数,而 load() 每次调用都会创建一个新的 Dynamic Worker。对可复用版本使用稳定且不可变的 get(id),既使恢复语义可预测,也避免不必要的创建计数。Dynamic Workers pricing
Facets 真正适合哪些场景#
1. AI 生成的小应用或个人工具#
这是官方的典型案例。平台为每个应用创建一个 AppRunner,把代码版本、权限、用量、审计日志留在父数据库;AI 生成的 App 则只能读写自己的 facet SQLite。平台既能让应用有计数器、草稿、会话等持久状态,又不用把一整个 DO namespace 交给模型。
2. 多租户插件、自动化与低代码扩展#
若租户能上传脚本或配置化业务逻辑,可以把一个 tenant/application 作为 parent DO 的路由粒度,把插件的状态放在对应 facet。supervisor 在进入 child 前执行租户鉴权、配额、审计、版本选择,并只传入最小 capability。这比让插件直接拿到 D1、R2、KV 或原始 DO binding 更容易实现最小权限。
3. 有私有状态的子 Agent#
Cloudflare Agents SDK 用 facets 为 Sub-agents 提供独立 SQLite,让 parent agent 保留编排、路由与访问控制。这适用于一个顶层会话下有多个子任务/角色、且它们不应直接共享短期记忆或业务状态的设计。注意 child 没有独立 physical Alarm slot;需要定时唤醒时由根 parent 统一承接和路由。Sub-agents 的存储、路由与调度说明
4. 可持久化的代码预览与应用构建平台#
预览系统通常需要快速加载某个不可变版本的用户代码,又要保留该预览的局部状态。Facet 让平台能把“代码版本 + 数据库 + 访问策略”绑定在同一个 supervisor 下。若预览只需要短暂运行、完全不需要可靠状态,单纯 Dynamic Worker 反而更简单。
何时不要选 Facets#
- 代码是静态、可信且由团队部署的。聊天室、协作房间、游戏状态、锁、库存扣减、限流等仍是普通 DO 的直接用例;额外引入 Loader、supervisor 和代码版本治理只会提高复杂度。
- 子实体必须成为独立的路由、地域或吞吐单元。Facet 的数据库与父对象共处一个 overall DO,官方没有承诺每个 facet 具有独立 placement、吞吐或配额。需要横向分片时,应先按 tenant、document、room 等键创建多个顶层 DO,再在每个顶层对象内按需使用 facets。
- 需要跨大量实体查询或分析。Facet SQLite 仍是局部对象存储;全局索引、报表或关系查询更适合 D1、R2 或 Analytics Engine。
- 工作本质是可恢复的多步骤流程。使用 Workflows;Facet 解决的是子代码与局部状态隔离,不是通用流程编排。参见 Workflows 与 Dynamic Workflows。
- 需要完整 Linux、原生依赖或 shell。选择 Containers 或 Sandbox SDK,而不是把 Dynamic Worker/Facet 当作通用容器。
上线前检查单#
- 顶层 supervisor 的分片键明确,例如 tenant、应用或 workspace;不能把所有用户塞进一个 parent DO。
- 动态代码的 ID 同时包含代码内容、依赖、权限与 compatibility 配置的不可变版本。
- 默认禁用全局出站网络,并通过受控 capability 提供真正需要的业务能力。
- 父库只保存控制面数据,facet 库只保存 child 业务状态;不依赖跨库直接读取。
- 已设计
abort()后的 code/schema 兼容、回滚和 stale stub 错误处理;delete()设为显式且可审计的破坏性操作。 - 已在 hibernation、代码更新、多个 facet name、配额耗尽和异常退出下测试 factory 是否能稳定恢复正确版本。
- 已同时观察 Durable Objects 与 Dynamic Workers 的用量、错误和延迟;不要按照“facet 数量”猜测容量或成本。