资讯动态

自托管Durable Objects方案Celld实测:开源替代实现分布式状态管理

发布时间:2026/8/20 13:58:28 来源:尧图企业网站定制
在分布式应用开发中状态持久化与全局一致性一直是核心挑战。Cloudflare Durable Objects 以其独特的“全局单例”模型为无服务器架构提供了强大的状态管理能力但其强绑定于 Cloudflare 平台也限制了部署的灵活性。近期一个名为Celld的开源项目进入了开发者视野它旨在提供一个可自托管的、兼容 Durable Objects API 的替代方案。本文将带你深入实测 Celld从概念解析、环境搭建到核心功能对比为你完整呈现一个可脱离云厂商锁定的状态管理新选择。本文适合正在评估或使用 Cloudflare Durable Objects但希望获得更高部署自主权的后端开发者、架构师以及任何对分布式状态管理感兴趣的工程师。通过阅读和实践你将能够独立部署一个 Celld 服务理解其与官方方案的异同并评估其是否适用于你的项目场景。1. 背景与核心概念为什么需要“自托管 Durable Objects”在深入 Celld 之前我们有必要先厘清 Cloudflare Durable Objects 的核心价值及其带来的“甜蜜的负担”。1.1 Cloudflare Durable Objects 是什么Cloudflare Durable Objects (DO) 是构建在 Cloudflare Workers 无服务器平台之上的一个抽象层。它不是一个数据库而是一个有状态的、全局唯一的 JavaScript 对象。其核心特性包括全局单例每个 Durable Object 由一个唯一 ID 标识。无论请求来自全球哪个边缘节点对同一 ID 的请求都会被路由到同一个对象实例上保证了状态的强一致性。自动持久化对象的状态类属性会自动被持久化到 Cloudflare 的底层存储中开发者无需手动处理数据库读写。基于 WebSocket 的实时通信DO 原生支持 WebSocket可以轻松构建聊天室、协作编辑等实时应用所有连接都汇聚到同一个对象实例。它解决了什么问题传统无服务器函数Serverless Functions是无状态的处理复杂状态如用户会话、游戏房间、购物车需要依赖外部数据库这引入了延迟、复杂性和最终一致性问题。DO 将状态和逻辑紧密绑定简化了有状态服务的开发模型。1.2 “平台绑定”的挑战与“自托管”的需求尽管 DO 能力强大但它与 Cloudflare Workers 生态系统深度绑定。这带来了几个现实挑战供应商锁定你的应用核心状态逻辑完全依赖于 Cloudflare 平台迁移成本极高。开发与测试环境虽然提供了wrangler dev本地模拟但完整测试和离线开发体验仍有局限。成本与合规对于数据敏感或需要特定合规要求的业务将核心状态完全托管于第三方云服务可能不适用。技术栈限制虽然 Workers 支持多种语言但 DO 的核心开发体验仍围绕 JavaScript/TypeScript。Celld 的定位正是为了解决这些痛点。它试图实现一个与 Durable Objects API 高度兼容的运行时让你可以在自己的服务器、私有云甚至本地开发机上运行相同的业务逻辑代码从而实现“一次编写多处部署”的灵活性。2. 环境准备与版本说明在开始实测 Celld 前我们需要搭建一个基础的开发环境。Celld 本身使用 Deno 编写因此我们的环境将围绕 Deno 展开。核心环境要求操作系统macOS, Linux, 或 Windows (WSL 2 推荐)。本文示例基于 Ubuntu 22.04 LTS。运行时Deno 1.40.0 或更高版本。Celld 构建于 Deno 之上利用了其安全的运行时和现代模块系统。包管理/工具deno 核心运行时。docker与docker-compose(可选) 用于快速部署 Celld 的持久化存储后端如 Redis。代码编辑器 VS Code 或其他支持 TypeScript 的 IDE建议安装 Deno 插件以获得更好的开发体验。版本说明本文撰写时Celld 仍处于早期活跃开发阶段版本号常为0.x.xAPI 和功能可能发生变动。以下版本信息供参考实际操作时请以项目官方仓库的最新文档为准。deno:1.40.3celld: 我们将直接从其 GitHub 仓库的主分支克隆并使用。wrangler(用于对比):3.0.0以上版本。项目结构预览我们将创建一个简单的项目来对比两种实现。celld-vs-do-demo/ ├── src/ │ ├── counter-do/ # Cloudflare Durable Objects 实现 │ │ ├── Counter.ts # Durable Object 类定义 │ │ └── index.ts # Worker 入口文件 │ └── counter-celld/ # Celld 实现 │ ├── Counter.ts # 几乎相同的Durable Object 类定义 │ └── server.ts # Celld 服务入口文件 ├── wrangler.toml # Cloudflare Workers 配置 ├── deno.json # Deno 项目配置 └── README.md3. Celld 核心原理与架构拆解Celld 并非简单重写它需要在自托管环境中模拟出 DO 的核心行为。理解其架构有助于我们判断其适用边界。3.1 核心组件与工作流程Celld 的架构可以简化为以下几个部分HTTP/WebSocket 网关接收外部请求根据请求路径和 Durable Object ID 进行路由。对象实例管理器负责 Durable Object 实例的生命周期管理创建、唤醒、休眠、销毁。存储抽象层定义如何持久化对象状态。Celld 默认提供了内存存储并通过插件形式支持 Redis、PostgreSQL 等外部存储这是实现“持久化”的关键。ID 解析与路由解析请求中的 Durable Object ID并确保相同 ID 的请求被路由到同一个服务器进程/实例中的同一个对象。其工作流程大致如下客户端请求 (GET /api/counter/123) - Celld 网关 - 解析 ID “123” - 查询“对象实例管理器”ID “123” 的 Counter 对象是否存在 - 如果存在将请求转发给该对象实例的 fetch() 方法。 - 如果不存在实例化 Counter 类调用其 constructor()然后调用 fetch()。 - 对象处理请求可能更新其内部状态。 - 响应返回给客户端。对象状态的变化会被存储抽象层捕获并异步持久化到配置的后端如 Redis。3.2 与 Cloudflare Durable Objects 的关键差异尽管 API 兼容但在实现层面存在根本差异这直接影响其特性和适用场景。特性维度Cloudflare Durable ObjectsCelld (自托管)全局一致性保证强保证。利用 Cloudflare 全球网络任何地点的请求都路由到唯一实例。依赖部署。单机部署可保证多机部署需借助外部协调如 Redis 存储层负载均衡粘性会话否则无法保证真正的“全局”单例。持久化存储由 Cloudflare 内部实现透明、自动、高可用。需自行配置和管理。如使用 Redis需负责 Redis 的部署、备份、扩缩容。弹性与扩缩容自动处理开发者无需关心对象在哪个物理位置。手动或通过 K8s 等编排工具。需要自行规划服务器的扩缩容策略。开发体验紧密集成wrangler一键发布、测试、监控。基于deno运行部署流程需自行搭建Docker、Systemd 等。成本模型按请求次数、Durable Object 唤醒次数和持续时间计费。基础设施成本。你需要支付托管 Celld 服务及其存储后端如 Redis、数据库的服务器费用。核心结论Celld 提供了 API 兼容性和部署自由但将分布式系统中最复杂的部分——全局状态协调与高可用保障——的交还给了开发者。它更适合用于开发测试、对“全局唯一性”要求不极端严格的内部应用或作为向多云/混合云架构过渡的兼容层。4. 完整实战构建并对比一个计数器应用让我们通过一个经典的“分布式计数器”示例来亲手实现并感受两者的异同。这个计数器需要支持递增、获取当前值并且值需要持久化。4.1 创建项目结构与 Cloudflare Durable Objects 实现首先创建项目并初始化 Cloudflare Workers 项目。# 创建项目目录 mkdir celld-vs-do-demo cd celld-vs-do-demo # 初始化 Cloudflare Workers 项目 (选择 “Hello World” 模板即可) npm create cloudflarelatest ./src/counter-do # 根据提示项目名可以输入 counter-do选择 “Hello World” script。 cd src/counter-do接下来我们编写 Durable Object 类和 Worker 逻辑。1. 定义 Durable Object 类 (src/Counter.ts):// 文件路径src/counter-do/src/Counter.ts export class Counter { // state 由 Durable Object 存储提供 state: DurableObjectState; // 计数器的值我们会将其持久化 value: number 0; constructor(state: DurableObjectState) { this.state state; // 在初始化时尝试从持久化存储中加载之前的值 this.state.blockConcurrencyWhile(async () { const stored await this.state.storage.getnumber(value); this.value stored || 0; }); } // 处理 HTTP 请求 async fetch(request: Request): PromiseResponse { const url new URL(request.url); if (url.pathname.endsWith(/increment)) { // 递增计数器 this.value; // 将新值异步持久化 await this.state.storage.put(value, this.value); return new Response(JSON.stringify({ value: this.value }), { headers: { Content-Type: application/json } }); } else if (url.pathname.endsWith(/get)) { // 获取当前值 return new Response(JSON.stringify({ value: this.value }), { headers: { Content-Type: application/json } }); } else { return new Response(Not Found, { status: 404 }); } } }2. 编写 Worker 入口文件 (src/index.ts):// 文件路径src/counter-do/src/index.ts // 从 Wrangler 配置中获取绑定名 export interface Env { COUNTER: DurableObjectNamespace; } export default { async fetch(request: Request, env: Env): PromiseResponse { const url new URL(request.url); // 从路径中提取计数器 ID例如 /api/counter/123/increment - id123 const pathSegments url.pathname.split(/); const idIndex pathSegments.indexOf(counter) 1; if (idIndex 1 || idIndex pathSegments.length) { return new Response(Invalid path. Use /api/counter/id/increment or /get, { status: 400 }); } const counterId pathSegments[idIndex]; const action pathSegments[idIndex 1]; // increment or get // 获取或创建对应 ID 的 Durable Object 实例 const id env.COUNTER.idFromName(counterId); const stub env.COUNTER.get(id); // 将请求转发给 Durable Object 实例处理 // 注意我们修改了请求的路径使其匹配 Durable Object 内部的路由逻辑 const newUrl new URL(request.url); newUrl.pathname /${action}; const newRequest new Request(newUrl, request); return stub.fetch(newRequest); }, };3. 配置wrangler.toml:# 文件路径src/counter-do/wrangler.toml name counter-do compatibility_date 2024-03-04 [[durable_objects.bindings]] name COUNTER class_name Counter # 对应 export 的类名 [[migrations]] tag v1 new_classes [Counter] # 声明要创建的 Durable Object 类4. 本地测试# 在 src/counter-do 目录下 npx wrangler dev启动后你可以用 curl 或浏览器测试# 递增 ID 为 test-1 的计数器 curl http://localhost:8787/api/counter/test-1/increment # 输出: {value:1} # 再次递增 curl http://localhost:8787/api/counter/test-1/increment # 输出: {value:2} # 获取当前值 curl http://localhost:8787/api/counter/test-1/get # 输出: {value:2} # 测试另一个 ID状态是独立的 curl http://localhost:8787/api/counter/test-2/get # 输出: {value:0}4.2 Celld 实现自托管相同的逻辑现在我们在同一项目的另一个目录中用 Celld 实现几乎相同的功能。1. 初始化 Celld 项目# 回到项目根目录 cd ../.. mkdir -p src/counter-celld cd src/counter-celld2. 创建 Deno 配置文件 (deno.json):{ tasks: { dev: deno run --allow-net --allow-read --allow-env server.ts }, imports: { celld: jsr:celld/celld } }3. 复用并稍作修改的 Durable Object 类 (Counter.ts):Celld 的目标是 API 兼容因此我们的Counter类几乎不需要改动。主要区别在于导入和类型。// 文件路径src/counter-celld/Counter.ts // 注意Celld 提供了自己的类型模拟了 Cloudflare 的环境 import { DurableObject } from celld; // 使用 Celld 提供的 DurableObject 基类和类型 export class Counter extends DurableObject { value: number 0; async initialize(state: DurableObjectState): Promisevoid { // Celld 可能使用略有不同的生命周期钩子这里我们用 initialize 来模拟 constructor 中的加载逻辑 const stored await state.storage.getnumber(value); this.value stored || 0; } async fetch(request: Request): PromiseResponse { const url new URL(request.url); if (url.pathname.endsWith(/increment)) { this.value; await this.state.storage.put(value, this.value); return new Response(JSON.stringify({ value: this.value }), { headers: { Content-Type: application/json } }); } else if (url.pathname.endsWith(/get)) { return new Response(JSON.stringify({ value: this.value }), { headers: { Content-Type: application/json } }); } else { return new Response(Not Found, { status: 404 }); } } }4. 创建 Celld 服务入口文件 (server.ts):这是 Celld 项目的核心我们需要创建并配置 Celld 服务器。// 文件路径src/counter-celld/server.ts import { Application, Router } from jsr:oak/oakv16; import { Celld, MemoryStorage } from celld; // 使用内存存储仅用于演示 import { Counter } from ./Counter.ts; // 1. 创建 Celld 实例并配置存储这里使用内存存储重启数据会丢失 const celld new Celld({ storage: new MemoryStorage(), // 生产环境应使用 RedisStorage 等 }); // 2. 将我们的 Durable Object 类注册到 Celld celld.defineDO(Counter, Counter); const app new Application(); const router new Router(); // 3. 定义路由将请求转发给 Celld 处理 router.all(/api/counter/:id/:action, async (ctx) { const { id, action } ctx.params; // Celld 通过 getDurableObject 获取 stub const stub celld.getDurableObject(Counter, id); // 构造一个新的请求路径匹配 Durable Object 内部的路由 const newUrl new URL(ctx.request.url); newUrl.pathname /${action}; const newRequest new Request(newUrl, ctx.request); // 将请求交给 Durable Object 处理 const response await stub.fetch(newRequest); ctx.response response; }); app.use(router.routes()); app.use(router.allowedMethods()); console.log(Celld counter server running on http://localhost:8000); await app.listen({ port: 8000 });5. 运行 Celld 服务# 在 src/counter-celld 目录下 deno task dev # 或直接运行: deno run --allow-net --allow-read --allow-env server.ts6. 测试 Celld 服务使用相同的 curl 命令仅更改端口。# 递增 curl http://localhost:8000/api/counter/test-1/increment # 获取 curl http://localhost:8000/api/counter/test-1/get # 测试另一个 ID curl http://localhost:8000/api/counter/test-2/get你会发现行为与 Cloudflare Workers 本地开发模式下的表现一致。4.3 配置持久化存储Redis内存存储 (MemoryStorage) 仅用于演示重启服务数据即丢失。Celld 支持可插拔的存储引擎。以下是如何配置 Redis 作为持久化后端。1. 使用 Docker 启动 Redisdocker run -d -p 6379:6379 --name celld-redis redis:7-alpine2. 修改server.ts使用 RedisStorage首先确保你安装了 Celld 的 Redis 适配器具体包名需查看 Celld 文档这里为示例。// 文件路径src/counter-celld/server.ts (修改后) import { Application, Router } from jsr:oak/oakv16; import { Celld } from celld; import { RedisStorage } from celld/storage/redis; // 假设的导入路径 import { Counter } from ./Counter.ts; import { connect } from jsr:redis/client; // 创建 Redis 客户端 const redisClient await connect({ hostname: localhost, port: 6379, }); const celld new Celld({ // 使用 Redis 存储 storage: new RedisStorage(redisClient), }); celld.defineDO(Counter”, Counter); // ... 其余 Oak 服务器代码保持不变重要你需要根据 Celld 项目实际提供的存储模块来调整导入语句。这展示了 Celld 将状态持久化责任移交给了开发者。5. 常见问题与排查思路在自托管 Celld 的过程中你可能会遇到以下典型问题。问题现象可能原因排查思路与解决方案启动 Celld 服务时报错Module not found1. Deno 导入路径错误。2.celld包未发布到 JSR 或版本不对。1. 检查deno.json中的imports是否正确。2. 直接通过 GitHub 仓库 URL 导入import { Celld } from “https://raw.githubusercontent.com/celldev/celld/main/mod.ts”;(需查看项目实际地址)。3. 运行deno cache --reload server.ts刷新缓存。Durable Object 状态未持久化重启后丢失使用了MemoryStorage或者配置的持久化存储如 Redis连接失败。1. 确认Celld构造函数中配置了正确的持久化storage选项。2. 检查 Redis 等服务是否正常运行网络是否通畅。3. 在initialize或fetch方法中添加日志确认storage.get/put是否被调用。多实例部署时同一 ID 的请求被不同实例处理Celld 单实例是单例但多实例间无协调。负载均衡器将请求随机分发到不同后端实例。1.方案一推荐使用支持**粘性会话Session Affinity**的负载均衡器将同一 ID 的请求固定转发到同一个 Celld 实例。2.方案二所有 Celld 实例共享同一个中心化存储如 Redis并在存储层实现锁机制但这会引入复杂性和延迟。这本质上是 Celld 与 Cloudflare DO 的核心差异。性能不及预期延迟高1. 持久化存储如 Redis网络延迟高或配置不当。2. 对象初始化从存储加载状态耗时过长。1. 将 Celld 实例与其存储后端如 Redis部署在同一可用区或同一台机器上减少网络延迟。2. 考虑对状态进行分片避免单个对象状态过大。3. 评估是否所有状态都需要强持久化部分场景可用内存缓存异步持久化。TypeScript 类型报错Celld 提供的类型定义与 Cloudflare Workers 类型不完全一致。1. 创建本地的类型声明文件.d.ts对 Celld 的 API 进行适配和扩展。2. 在可能产生差异的地方使用// ts-ignore暂时忽略不推荐。3. 关注 Celld 项目的更新其类型定义会逐步完善。6. 最佳实践与工程建议将 Celld 用于生产环境或严肃项目时需要遵循以下工程实践以保障稳定性和可维护性。6.1 存储后端的选择与配置生产环境务必使用外部存储绝对不要使用MemoryStorage。根据数据量和访问模式选择Redis 低延迟、高性能适合状态较小、访问频繁的场景。需配置持久化AOF/RDB以防数据丢失。PostgreSQL/MySQL 适合状态结构复杂、需要 SQL 查询或关联关系的场景。利用其事务特性保证一致性。其他 KV 存储 如 etcd、TiKV适合需要强一致性和高可用的分布式场景。连接池与健康检查为存储客户端配置连接池和健康检查机制避免因存储服务抖动导致 Celld 服务不可用。备份与监控像管理任何有状态服务一样为你的存储后端制定备份策略并监控其性能指标内存使用、连接数、慢查询。6.2 部署与高可用架构单点故障单个 Celld 进程是单点。必须部署多个实例 behind a load balancer。状态路由一致性如前所述使用粘性会话是保证“每个 ID 对应唯一实例”最简单有效的方式。在 Nginx 或云负载均衡器中配置基于 Cookie 或特定 HTTP 头如X-Durable-Object-Id的哈希路由。滚动更新与状态迁移更新 Celld 服务版本时需要优雅处理。建议流程将负载均衡器流量从待更新实例上引流。等待该实例上所有 Durable Object 处理完当前请求并休眠Celld 需支持优雅关闭。更新该实例。将其重新加入负载均衡池。循环处理下一个实例。考虑使用容器化使用 Docker 容器化 Celld 应用并通过 Kubernetes 或 Nomad 进行编排可以简化部署、扩缩容和健康管理。6.3 应用开发注意事项ID 设计Durable Object ID 的设计直接影响性能和数据分布。避免使用单调递增的 ID这可能导致热点。使用随机、散列的 ID如 UUID、雪花算法 ID有助于负载均衡。对象生命周期明确对象的生命周期。不活跃的对象应被设计为自动休眠以释放资源。避免在单个对象中存储无限增长的数据。错误处理与重试网络调用、存储操作都可能失败。在fetch方法内部实现健壮的错误处理和幂等性逻辑。对于因实例重启导致的临时失败客户端应具备重试机制。监控与日志为 Celld 服务添加详细的日志记录对象的创建、唤醒、请求处理耗时和错误。集成监控系统如 Prometheus暴露关键指标请求速率、对象活跃数、存储操作延迟等。6.4 何时选择 Celld何时坚持 Cloudflare选择 Celld 的场景开发与测试需要一个完全本地的、可离线工作的 Durable Objects 模拟环境。混合云/多云策略核心业务逻辑需要能在不同云环境或私有数据中心运行。数据驻留与合规状态数据必须存储在特定地理区域或自有基础设施中。技术栈探索希望深入理解 Durable Objects 模型而不受制于特定云厂商。坚持 Cloudflare Durable Objects 的场景追求极致的全球低延迟与强一致性你的应用用户遍布全球且状态一致性要求极高。希望完全托管免运维不想管理服务器、存储、网络和扩缩容。深度集成 Cloudflare 生态同时使用了 Workers、R2、Pages 等其他 Cloudflare 服务希望获得无缝体验。项目处于早期或快速原型阶段希望以最小运维开销快速验证想法。通过本文的实测与对比我们可以看到 Celld 作为一个开源项目成功地捕捉到了 Durable Objects 编程模型的核心精髓并为开发者提供了宝贵的部署灵活性。然而这种灵活性是以承担更多分布式系统复杂性为代价的。在决定采用 Celld 之前请务必根据你的团队运维能力、业务对一致性的要求以及长期架构规划来做出权衡。对于大多数场景Cloudflare Durable Objects 仍然是更简单、更可靠的选择但对于那些需要突破平台限制的特定需求Celld 无疑打开了一扇新的大门。建议在实际项目中可以先在非核心业务或测试环境中尝试 Celld积累经验后再做更大范围的评估。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价