兰 亭 墨 苑
期货 · 量化 · AI · 终身学习
首页
归档
编辑文章
标题 *
URL 别名 *
内容 *
(支持 Markdown 格式)
## 底层原理 我们来用一个生动的比喻来解释这项技术的底层原理。 想象一下传统的“无状态”Serverless(比如普通的 Cloudflare Worker)和“有状态”的 Durable Object 分别是什么。 --- ### 场景一:普通的“无状态”Serverless(比如常规 Worker) 这就像一个庞大的**“呼叫中心”**,里面有成千上万个接线员。 1. **接线员没有固定身份**:当你打电话(发起一个请求)进去,系统会随便找一个空闲的接线员为你服务。你这次打和下次打,接电话的几乎不可能是同一个人。 2. **接线员没有记忆**:每个接线员都是“健忘”的。他帮你处理完当前这件事(比如查询天气)就挂了。他不会留下任何记录。你下次再打电话进去,哪怕是问同一个城市的天气,另一个接线员也要从头再来一遍。 3. **优点**:非常高效,能同时处理海量电话。因为每个接线员都不需要记东西,所以可以随时增减人手,非常灵活。 这就是**“无状态”(Stateless)**。每个请求都是独立的、一次性的,服务器不保留任何关于你之前操作的记忆。这对于简单的、一次性的任务非常完美。 --- ### 场景二:Durable Objects(“有状态”的 Serverless) 现在,想象你在这个“呼叫中心”里,可以申请一个**“专属管家”**,这就是 Durable Object。 1. **专属管家有唯一身份(Globally-unique name)**: * **原理**:当你创建一个 Durable Object 时,系统会给它一个全球唯一的“手机号码”(ID)。无论你从世界哪个角落拨打这个号码(发起请求),接电话的永远是**同一个**管家。这个管家就是你的计算实例。 2. **专属管家有随身笔记本(Durable storage attached)**: * **原理**:这个管家最厉害的地方在于,他随身带着一个笔记本(持久化存储)。每次你和他交流,他都会把重要的信息记下来。比如,你让他帮你管理一个购物车,他会在本子上记下“用户A加入了2件商品”。 * **“计算与存储相结合”**的真正含义是:管家(计算单元)和他的笔记本(存储)是**绑定在一起、形影不离的**。这带来了两个巨大好处: * **强一致性 (Strongly consistent)**:因为只有这一个管家能在这本笔记上写东西,所以信息绝对不会混乱或过时。 * **访问飞快 (Fast to access)**:因为笔记本就在管家手上,他不需要跑去别处(比如一个独立的数据库)翻查资料,所以响应速度极快。 3. **专属管家可以协调工作(Coordinate between multiple clients)**: * **原理**:如果你和你的朋友想一起编辑一份文档,你们俩都可以拨打同一个“专属管家”的手机号。这个管家会看着他的笔记本,确保你们俩的操作不会互相冲突,他能有序地处理你们的请求,实现协同工作。 4. **按需服务,地理就近(Like a Worker...)**: * **原理**:和你第一次呼叫他时一样,这个管家会被安排在你地理位置附近的分部待命。他不忙的时候会去休息(关闭),一旦你呼叫他,他会立刻出现(快速启动),非常节省资源。 --- ### 总结 | 特性 | 普通 Worker (无状态) | Durable Object (有状态) | | :--- | :--- | :--- | | **身份** | 匿名、随机 | 唯一、固定(像手机号) | | **记忆** | 没有记忆,用完即忘 | 有持久记忆(像随身笔记本) | | **核心** | 纯粹的计算 | **计算 + 存储** 的结合体 | | **适用场景** | 查询天气、转换格式等一次性任务 | 聊天室、在线协作文档、游戏状态同步、购物车等需要记住上下文的任务 | 所以,Durable Objects 的底层原理,就是通过**给一个计算单元(Worker)分配一个永不改变的全球唯一ID,并把一块专属于它的、与它物理位置靠近的存储和它绑定**,从而创造出了一个既有 Serverless 弹性、又能记住状态的“有状态”服务单元。它完美解决了传统 Serverless 无法处理连续、有状态任务的痛点。 ## 计费 好的,Cloudflare Durable Object 的计费模式非常精细,它遵循“按用量付费”的原则,但和你之前可能接触过的 Serverless 模型略有不同。因为它引入了“状态”和“持续活动时间”的概念。 总的来说,它的费用由 **四个核心部分** 组成: 1. **活跃持续时间 (Duration)** 2. **请求数量 (Requests)** 3. **存储用量 (Storage)** 4. **数据传出 (Egress - 适用于所有 Cloudflare 产品)** 下面我们用之前的“专属管家”比喻来详细解释每一项是如何计费的。 --- ### 1. 活跃持续时间 (Duration) - 管家的“工时费” 这是 Durable Object 最独特的计费项。 * **计费内容**:你不是为 CPU 的实际运算时间付费,而是为这个“管家”(Durable Object 实例)**保持活跃的“墙上时间”(Wall-clock time)** 付费。只要它在内存中处于活动状态,哪怕只是在等待你的下一个指令,计时器都在走。 * **如何计算**:它的单位是 **GB-秒 (GB-seconds)**。Cloudflare 会把分配给对象的内存(固定为 128 MB,即 0.125 GB)乘以它保持活跃的秒数。 * `费用 = 0.125 GB * 活跃秒数` * **何时开始/结束**: * **开始**:当一个请求到达,并且该对象实例当前未在内存中时,系统会唤醒它,计时开始。 * **结束**:在处理完最后一个请求,并且没有任何挂起的异步任务(如 `waitUntil`)后,对象会保持一小段空闲时间然后被系统自动关闭,计时结束。 * **比喻**:就像你雇佣了管家,只要他在“在线状态”为你待命,你就需要按小时支付他**工时费**,这与他是不是一直在忙着处理你的请求无关。 ### 2. 请求数量 (Requests) - 给管家的“通话费” 这项费用非常直观。 * **计费内容**:每次你的代码(比如一个普通的 Worker)向某个 Durable Object 发起请求(比如调用 `DurableObjectStub.fetch()`),就算一次请求。 * **如何计算**:按请求的总次数计费,通常是每百万次请求一个价格。 * **注意**:这笔费用是**额外**的。也就是说,你不仅要为发起调用的那个 Worker 的请求付费,还要为被调用的 Durable Object 接收这个请求付费。 * **比喻**:每次你打电话(发起请求)给你的专属管家,都需要支付一笔**通话费**。 ### 3. 存储用量 (Storage) - 管家笔记本的“材料费” Durable Object 的状态需要地方存放。 * **计费内容**:你在对象内部通过 `this.state.storage` API 存储的所有数据的总量。 * **如何计算**:按存储数据的大小和时长计费,单位通常是 **GB-月 (GB-months)**。这和大多数云存储(如 S3)的计费方式类似。 * **注意**:你不需要为读写操作的次数(IOPS)付费,费用只跟你存储的数据“有多大”有关。 * **比喻**:管家用来记事的那个笔记本,如果内容写得越来越多、越来越厚(存储数据量变大),你就需要为这本笔记本支付**材料费**。 ### 4. 数据传出 (Egress) 这部分不是 Durable Object 特有的,而是 Cloudflare 的通用计费项,但仍需考虑。当数据从 Cloudflare 网络传输到外部互联网时,可能会产生费用。 --- ### 免费额度 Cloudflare 为 Workers 和 Durable Objects 提供了非常慷慨的免费套餐,以上提到的计费项大部分都有免费额度(每月刷新): * **活跃持续时间**:每月有 **400,000 GB-秒** 的免费额度。 * **请求数量**:每月有 **100 万次** Durable Object 请求的免费额度。 * **存储用量**:每月有 **1 GB** 的存储免费额度。 这些额度对于小型项目和开发测试来说绰绰有余。只有超出免费额度的部分才会开始计费。 ### 总结 | 计费项 | 解释 | 比喻 | | :--- | :--- | :--- | | **Duration** | 对象在内存中保持活跃的时间 | 管家的**工时费** | | **Requests** | 调用对象的次数 | 给管家的**通话费** | | **Storage** | 持久化存储的数据总量 | 笔记本的**材料费** | Durable Object 的计费模型旨在精确反映一个“有状态”服务所消耗的真实资源:既要为它“活着”的时间付费,也要为与它“沟通”的频率和它“记住”东西的多少付费。 ## 使用场景 Durable Objects 技术的核心是**为需要“记忆”和“协调”的场景提供一个简单、高效的解决方案**。 下面是几个非常适合使用 Durable Objects 的典型场景,以及为什么它能发挥巨大优势的解释。 --- ### 场景一:实时聊天室或直播评论区 **这是什么?** 一个允许多个用户实时加入、发送和接收消息的房间。 **为什么适合 Durable Objects?** * **唯一身份 (Unique Identity):** 每个聊天室都可以被创建为一个 Durable Object,并拥有一个独一无二的 ID(例如 `roomId: "general-chat"`)。所有想加入这个聊天室的用户,都会被定向到这**同一个**对象实例。 * **持久化状态 (Persistent State):** 这个对象可以在其内部存储(`this.state.storage`)这个聊天室的消息历史记录。新用户加入时,可以从这个存储中拉取最近的几十条消息。 * **协调能力 (Coordination):** 当一个用户发送消息时,请求被发送到这个唯一的对象。对象接收到消息后,将其存入状态,然后通过 WebSocket 连接将这条新消息**广播**给所有连接到该对象的其他用户。它成为了所有用户的**单一协调中心**,确保了消息的顺序和分发。 **为什么传统 Serverless 不适合?** 用普通的无状态 Worker,一个用户的消息进来,Worker A 处理了。另一个用户的消息进来,可能是 Worker B 处理。这两个 Worker 互不相识,没有共享的内存。为了同步消息,它们必须依赖一个外部的数据库或缓存(如 Redis)。这会引入额外的网络延迟、增加系统复杂性,并可能导致消息顺序错乱的“竞态条件”(Race Condition)。Durable Object 将这个协调中心内置了。 --- ### 场景二:在线协作文档(如 Google Docs 的简化版) **这是什么?** 多个用户可以同时打开并编辑同一份文档,并能看到其他人的光标位置和实时修改。 **为什么适合 Durable Objects?** * **唯一身份:** 每份文档对应一个 Durable Object (`documentId: "project-spec-v1"`)。所有编辑这份文档的用户都会连接到这个对象。 * **持久化状态:** 对象内部存储着文档的**完整内容**。这是“唯一事实来源”(Single Source of Truth)。 * **协调能力:** 这是最关键的一点。当用户A输入一个字符,这个操作被发送到对象。对象负责将这个修改应用到文档内容上,然后通知所有其他协作者这个变更。如果用户A和用户B几乎同时修改了同一句话,Durable Object 作为**唯一的仲裁者**,可以按顺序处理这些操作,防止数据冲突和丢失。 **为什么传统 Serverless 不适合?** 这会是一场灾难。如果两个用户的修改请求被两个不同的无状态 Worker 同时处理,它们会各自从数据库读取旧的文档版本,进行修改,然后写回数据库。后写入的那个会**覆盖**先写入的,导致其中一个用户的修改**完全丢失**。要解决这个问题需要复杂的数据库事务和锁机制,而 Durable Object 从架构上就避免了这个问题。 --- ### 场景三:用户购物车 **这是什么?** 为每个在线购物的用户维护一个独立的购物车,记录他们想要购买的商品。 **为什么适合 Durable Objects?** * **唯一身份:** 每个用户的购物车都可以是一个 Durable Object,ID 可以是基于用户ID或会话ID生成的(例如 `cartId: "user-12345"`)。 * **持久化状态:** 对象内部存储着该用户购物车中的商品列表、数量和价格。这个状态会一直保留,即使用户关闭了浏览器再回来,只要能定位到同一个对象ID,购物车里的东西就还在。 * **原子操作 (Atomicity):** “添加商品”、“更新数量”、“清空购物车”等操作都在同一个对象内完成,确保了数据的一致性。 **为什么传统 Serverless 不适合?** 每个操作(增、删、查)都需要一个无状态 Worker 先去连接外部数据库(如 DynamoDB),根据用户ID查询到购物车数据,在内存中修改,再写回数据库。这个过程重复且低效。Durable Object 把数据和操作它的逻辑放在了一起,省去了大量的外部数据库往返通信。 --- ### 场景四:API 请求频率限制器 (Rate Limiter) **这是什么?** 限制某个用户或IP地址在单位时间内可以调用API的次数,以防止滥用。 **为什么适合 Durable Objects?** * **唯一身份:** 为每个需要被限制的用户或IP创建一个 Durable Object (`limiterId: "ip-192.168.1.1"`)。 * **持久化状态:** 对象内部存储一个时间戳列表或一个计数器,记录该用户在当前时间窗口内的请求次数。 * **协调能力:** 所有来自该IP的请求在到达真正的业务逻辑之前,先经过这个对象。对象检查其内部状态,如果请求次数未超限,就增加计数器并放行请求;如果超限,则直接拒绝。这是一个**原子性的检查和更新**操作。 **为什么传统 Serverless 不适合?** 要在全球分布的无状态 Worker 之间精确地共享一个计数器非常困难,通常需要一个延迟极低的集中式缓存(如 Redis),但这会成为性能瓶颈和单点故障。Durable Object 自然地解决了这个问题,每个用户的计数器都封装在自己的对象里。 ### 总结 | 适用场景 | 主要利用的 Durable Object 特性 | | :--- | :--- | | **实时聊天室** | **协调能力**(广播消息)和 **唯一身份**(所有用户连同一个房间) | | **协作文档** | **协调能力**(解决编辑冲突)和 **持久化状态**(作为文档的唯一来源) | | **购物车** | **持久化状态**(跨会话保存商品)和 **唯一身份**(每个用户有专属购物车) | | **频率限制器** | **持久化状态**(存储请求计数)和 **原子操作**(检查并更新计数的原子性) | ## 解释 好的,我们来详细解释这段代码。这是一个设计得非常精妙的 Durable Object 示例,它完美地展示了如何利用内存缓存来最大化性能。 ### 总体目标 这段代码定义了一个Durable Object计数器 (`Counter`)。它的核心设计思想是:**在对象初始化时,从持久化存储(硬盘)中读取一次计数值,并将其缓存在内存中。之后的所有请求都直接从内存中读取这个值,从而实现极速响应,避免了每次请求都去访问较慢的存储。** --- ### 分步详解 我们来逐行分析这段代码: ```javascript export class Counter { // 构造函数,在对象实例第一次被创建到内存中时执行 constructor(state, env) { this.state = state; ``` * `constructor(state, env)`: 这是类的构造函数。对于 Durable Object 来说,它有一个特殊的生命周期:**当一个对象实例因为接收到请求而需要被唤醒(从“冷启动”到“热状态”)时,这个构造函数会且仅会执行一次。** 之后只要对象还“活”在内存里,再来多少请求都不会再次执行它。 * `this.state = state;`: `state` 是一个由 Cloudflare 运行时自动注入的对象,它提供了访问持久化存储 (`state.storage`) 和生命周期控制方法(如 `blockConcurrencyWhile`)的能力。这里把它存为 `this.state` 方便后续使用。 ```javascript // `blockConcurrencyWhile()` 确保在初始化完成之前, // 不会处理任何请求。 this.state.blockConcurrencyWhile(async () => { let stored = await this.state.storage.get("value"); // 初始化之后,未来的读取操作就不再需要访问存储了。 this.value = stored || 0; }); } ``` 这是这段代码最核心、最关键的部分。 * **`this.state.blockConcurrencyWhile(async () => { ... })`**: * **作用**:这是一个“并发锁”。它告诉 Durable Object 运行时:“请暂停所有即将到来的 `fetch` 请求,不要处理它们。直到我括号里的这个异步函数 (`async`) 执行完毕,再放行这些请求。” * **为什么必须用它?**:想象一下,一个“冷”的(不在内存中)Durable Object 同时收到了两个请求。运行时会为它创建一个实例,并调用构造函数。如果没有 `blockConcurrencyWhile`,这两个请求可能会并发执行,导致它们都去读取存储 (`storage.get`),可能会引发竞态条件或不必要的重复工作。这个“锁”保证了**初始化逻辑是原子的、安全的,并且在处理任何业务逻辑之前就已完成**。 * **`let stored = await this.state.storage.get("value");`**: * **作用**:这是真正的 I/O 操作。它异步地从与此 Durable Object 实例绑定的**持久化存储**中,读取键名为 `"value"` 的值。这个操作相对较慢,因为它涉及磁盘或网络。 * **`this.value = stored || 0;`**: * **作用**:这是**将状态加载到内存**的关键步骤。 * `stored || 0`:这个逻辑的意思是,如果从存储中读取到的 `stored` 是一个有效值(不是 `undefined` 或 `null`),那就用它。如果 `stored` 是 `undefined`(比如这个对象是第一次被创建,存储里什么都没有),那就使用 `0`作为初始值。 * `this.value = ...`:将这个值赋给类的**实例属性 `this.value`**。现在,这个计数值就被**缓存**在了对象的内存里。 ```javascript // 处理来自客户端的 HTTP 请求 async fetch(request) { // 直接使用 this.value,而不是访问 storage // ... 在这里可以实现增加、减少或读取 this.value 的逻辑 ... } } ``` * `async fetch(request)`: 这是处理外部请求的入口点。每次有请求发给这个 Durable Object,这个方法就会被调用。 * **注释的含义**: 注释 `// use this.value rather than storage` 清楚地表明了设计意图。在 `fetch` 方法内部,当需要获取当前计数值时,代码应该**直接读取 `this.value`**。 * 读取 `this.value` 是一个**同步的、极快的内存访问**。 * 而 `await this.state.storage.get("value")` 是一个**异步的、相对慢的存储访问**。 ### 执行流程模拟 为了更清晰地理解,我们模拟一下请求流程: **场景一:第一个请求到达一个“冷”的 Counter 对象** 1. 请求到达 Cloudflare 边缘节点。 2. 运行时发现 `Counter` 对象不在内存中,于是创建一个新的实例。 3. **`constructor` 被调用**。 4. `blockConcurrencyWhile` **立即“上锁”**,所有外部请求(包括触发这次唤醒的请求)都被暂停在门外。 5. `await this.state.storage.get("value")` 执行,从磁盘读取数据。假设是第一次,返回 `undefined`。 6. `this.value` 被赋值为 `0`。 7. `blockConcurrencyWhile` 内部的函数执行完毕,**“锁”被释放**。 8. 现在,被暂停的那个 `fetch` 请求被放行,`fetch()` 方法开始执行。它会直接从内存中读到 `this.value` 是 `0`。 **场景二:第二个请求紧接着到达一个“热”的 Counter 对象** 1. 请求到达。 2. 运行时发现 `Counter` 对象**已在内存中**。 3. **`constructor` 不会再次执行**。 4. **`fetch()` 方法被直接调用**。它直接从内存中读取 `this.value`,响应速度极快。 ### 总结 这种模式被称为 **“初始化时加载到内存”(Load-on-init)**,是使用 Durable Objects 的最佳实践之一。 * **优点**:极大地提升了对象“热”状态下的性能,因为后续所有读取操作都避免了昂贵的存储 I/O。 * **关键**:利用 `constructor` 和 `blockConcurrencyWhile` 来安全、原子地完成仅有一次的初始化加载过程。 当然,如果要在 `fetch` 方法中修改计数值(比如实现 `increment`),你需要同时更新内存中的值和持久化存储中的值,以确保状态被保存: ```javascript // 在 fetch 方法内 async increment() { this.value++; await this.state.storage.put("value", this.value); // 更新持久化存储 return this.value; } ```
配图 (可多选)
选择新图片文件或拖拽到此处
标签
更新文章
删除文章