Cloudflare
Durable Objects
Arquitetura
Trade-offs
Decisão Técnica

当耐用物品是错误的答案时

对何时不使用耐用对象进行诚实分析,为每个用例提供正确的替代方案以及简化决策的诊断问题。

持久对象之所以受到关注,是因为它们解决了一些困难——与边缘的序列化访问保持一致的状态——这产生了一种偏见:刚刚学习该工具的工程师倾向于将其应用于不需要解决的问题。结果是代码不错。它是正确的、有效的代码,但比它需要的更昂贵、更复杂。对于在 D1 就足够时构建在 DO 上的简单 CRUD,账单可能会大十到五十倍,并且没有用例所需的一致性优势。

决定是否需要 DO 的问题

在决定使用持久对象之前,有一个具体问题:您需要管理的状态是否需要来自多个并发客户端的序列化访问?

序列化意味着操作的顺序很重要,并且对同一数据的两个同时操作必须是互斥的。多个竞争客户端意味着多个代理可以同时尝试修改相同的数据,并且两个代理都需要看到一致的结果。

如果答案是否定的 - 如果数据被许多人读取但很少写入,或者如果不太可能并发写入同一记录或由另一层处理 - DO 会增加复杂性和成本,但不会增加有用的保证。

其中 D1 是正确答案

对于关系数据,D1 是 Cloudflare 堆栈中的自然位置。它具有完整的 SQL,支持 JOIN、索引、事务、即席查询 — D1 拥有而 DO 没有的一切。 DO 是具有过程 API 的线性化键值存储。如果您没有明确预先计算并存储此数据,它无法回答“列出过去 7 天内登录且仍具有正余额的所有用户”。

D1 的读取负载成本要低得多:读取每百万行 0.001 美元,写入每百万行 1.00 美元。主存储体区域的额外写入延迟(这是 D1 最常被提及的缺点)仅影响写入操作。对于读取占主导地位的应用程序,这种权衡很大程度上有利于 D1。

令人困惑的情况:“但我需要写入是原子的”。 D1有交易。 0保证原子性。不同之处在于,D1 使用[数据库2]锁——在高并发情况下可能会发生争用——而 DO 在设计上使用序列化。对于绝大多数具有正常写入负载的应用程序来说,带有事务的 D1 就足够了,而且便宜得多。

其中 KV 是正确答案

KV 针对主导大多数边缘用例的模式进行了优化:大量读取、偶尔写入、不同 PoP 之间不需要强一致性。配置缓存、预先计算的计算结果、最终一致性可接受的用户会话——KV 以亚毫秒级延迟响应 PoP 中的本地缓存读取,并采用有利于大量读取的定价模型。

错误在于假设因为 KV 没有原子读取-修改-写入操作,所以您需要 DO 来处理任何更改。 Web 应用程序中的绝大多数数据的变化不需要严格的原子性:当用户编辑其首选项时更新的用户配置文件不需要与另一个竞争脚本相互排斥 - 因为没有其他人同时编辑相同的配置文件。

当并发写入相同数据的频率足够高以至于 KV 的最终一致性产生用户可见的不正确结果时,DO 就开始有意义。

速率限制不是 DO 用例

速率限制是“看起来需要 DO 但实际上不需要”的最常见示例。直觉是正确的:速率限制需要每个用户有一个计数器,该计数器随着每个请求而自动递增。如果两个 Worker 同时增加同一个计数器,则可能会出现竞争条件,导致用户发出的请求数量超过限制。

Cloudflare Rate Limiting API 原生解决了这个问题,无需代码,无需 DO,也无需超出计划的额外成本。它适用于所有付费计划,支持每个 IP、每个经过身份验证的用户、每个路由、每个标头的限制,并且具有可配置的周期。对于边缘的速率限制,使用本机 API 比实现计数器 DO 更简单、更便宜且更可靠。

DO 进行速率限制的合理情况是,当您有本机 API 未涵盖的要求时:具有精确精度的自定义滑动窗口逻辑、依赖于不在标头中的用户会话数据的限制,或者需要与应用程序 DO 中已存在的其他数据保持一致的速率限制。

用于异步处理的工作队列

有时作为 DO 候选者出现的另一种模式:工作队列,其中多个生产者对任务进行排队,多个消费者进行处理。 DO 可以将其建模为具有任务列表和处理循环的对象。

工作队列本身就解决了这个问题:生产者生产 1×,消费者通过处理程序接收批次,具有自动重试、死信队列以及每百万条消息 40 美元的单独费用。对于具有重试和退避的异步处理,队列更适合、更便宜,并且没有 DO 的串行吞吐量上限。

当消费需要按身份序列化(按顺序处理用户的所有操作,而不需要并行)以及当该处理需要访问 DO 本地的状态时,用于异步处理的 DO 就有意义。

文件存储是R2,不是DO存储

DO 存储是针对小对象(设置、计数器、会话状态、消息)进行优化的键值存储。它没有记录的值限制,但专为可以合理地满足请求的数据而设计。对于文件、图像、视频和任何较大的 blob,R2 是正确的选择:每月 0.015 美元/GB 的存储(不到 DO 存储成本的十分之一),Workers 没有出口成本,具有 S3 兼容的 API。

DO 存储并不是对象存储的替代品——它是 DO 本身的事务状态存储。

选择前诊断

DO 解决了替代方案无法以相同保证解决的问题的四个用例:与多个客户端同时修改同一文档的协作编辑、实时状态协调(其中在线列表需要一致)、带超时的分布式锁(即使持有者崩溃也需要保证过期)、以及按到达排序的事件日志(其中插入顺序在语义上很重要)。

对于不属于此列表的所有内容,平台内有一个更便宜、更简单或两者兼而有之的替代方案。使用 DO 的决定始于有关序列化的问题。如果您无法阐明为什么数据访问序列化对于用例来说是必要的,那么 DO 可能不是一个合适的工具。

在不必要的地方使用 DO 的风险并不在于它破坏了任何东西——代码仍然可以工作。代价是带来不必要的复杂性:一种强加串行吞吐量限制的抽象,要求将工人付费计划作为先决条件,具有明显的学习曲线,并且对于替代方案能够很好解决的情况,其成本高于替代方案。对于已经在系统其他地方使用 DO 的团队来说,多一个 DO 的边际成本会更低。对于从 Cloudflare 开始使用简单 CRUD 的团队来说,从 D1 和 KV 开始,然后在真正需要序列化的地方添加 DO 是正确的复杂性顺序。

另请阅读

  • [Cloudflare Durable Objects:边缘的一致状态 - 真正改变的是什么33
  • [生产中的耐用物品:法案将是什么样子以及令人惊讶的限制4
  • [持久对象编程模型:与您曾经使用过的任何东西不同的地方5
  • [持久对象和 WebSockets:无需专用服务器的多人游戏6
  • [从 Pages 迁移到 Workers:何时有意义以及变革的实际成本7
  • [面向领导者的 WebAssembly:当架构决策值得时8