在 2017 年推出 Cloudflare Workers® 时,我们心怀着一个激进的愿景:在网络边缘运行的代码不仅要能够提高性能,而且比在单个数据中心运行的代码更容易部署,运行成本也要更低。这个愿景意味着,Workers 关乎的不仅是边缘计算,也体现了我们正在重新构想应用程序的构建方式。
使用“无服务器”方法让我们能够极大简化部署,使用隔离技术则使我们能够以更低的成本交付无服务器,而且没有令其他提供商却步的漫长冷启动。通过 Workers KV,我们为平台添加了易于使用并且最终一致的边缘存储。
但直到今天,依然无法彻底在边缘管理具有强一致性的状态,或者在多个客户端之间进行实时协调。因此,应用程序的这些部分仍然必须托管在其他位置。
Durable Object 为存储和状态带来了一种真正的无服务器方法:具有一致、低延迟和分布式的特点,而且维护和扩展也毫不费力。它们还提供了一种在客户端之间进行协调的简便方法,不论是特定聊天室中的用户、特定文档的编辑器,还是特定智能家居中的 IoT 设备。Durable Object 是 Workers 堆栈中缺少的部分,它使整个应用程序可以完全在边缘上运行,彻底摆脱集中式“源站”服务器。
今天,我们将启动 Durable Object 的封闭测试。
Request a beta invite »什么是“Durable Object”?
坦率地说,为此产品取名颇为棘手,因为它与当今广泛使用的其他任何云技术都不一样。这个众所周知的事物披着许多层外衣,但最终我们决定采用“Unique Durable Object”,或简写为“Durable Object”。我来通过分解进行阐释:
Object:Durable Object 是面向对象编程意义上的对象。Durable Object 是类的实例,也就是用 JavaScript(或您选择的语言)编写的类定义。类具有定义其公共接口的方法,而对象是此类的实例,将代码与某些私有状态结合在一起。
Unique:每个对象具有全局唯一标识符。这个对象一个时间上仅存在于整个世界的一个位置。在世界中任何地方运行并知道该对象 ID 的 Worker 都可以向其发送消息。所有这些消息最终都传递到同一个地方。
Durable:与 JavaScript 中的一般对象不同,Durable Object 可以将持久状态存储在磁盘上。每个对象的持久状态都是其私有的,这意味着不仅可以快速访问存储,而且对象甚至可以安全地在内存中维护状态的一致副本,并且毫无延迟地对其进行操作。内存中对象在空闲时关闭,并在以后需要时重新创建。
它们可以做什么?
Durable Object 具有两大能力:
储存:每个对象具有关联的持久存储。这一存储是具体对象所私有的,因此存储始终与对象共存。这意味着,存储速度可以非常快,同时提供强大的事务一致性。Durable Object 将无服务器理念运用于存储,将传统的大型单体数据库拆分成许多个细小逻辑单元。这样,我们便获得了无服务器所带来的优势:无忧扩展,零维护负担。
协调:在过去,使用 Workers 时,每个请求将随机负载平衡到一个 Worker 实例。由于没办法控制哪个实例接收请求,因此无法强制两个客户端与同一个 Worker 进行通信,因而客户端也不能通过 Workers 进行协调。Durable Object 带来改变:与同一主题相关的请求可以转发到同一对象,然后可以在它们之间进行协调,而无需触碰存储。例如,这可用于促进实时聊天、协同编辑、视频会议、发布/订阅消息队列,以及游戏会话等。
敏锐的读者可能会注意到,许多协调用例都需要 WebSocket。实际上与之相反,大多数 WebSocket 用例都需要协调。由于这种互补关系,除了 Durable Object 测试版外,我们还为 Workers 添加了 WebSocket 支持。如需更多信息,请参见以下问答。
区域:地球
使用 Durable Object 时,Cloudflare 会自动确定每个对象将驻留的 Cloudflare 数据中心,并可以根据需要在不同位置之间透明地迁移对象。
传统数据库和有状态基础结构通常需要您考虑地理“区域”,以便您确保将数据存储到使用位置附近。考虑区域通常可能是不尽人意的负担,尤其是对于非固有地理性的应用程序。
使用 Durable Object 后,您可以自己设计存储模型,以匹配应用程序的逻辑数据模型。例如,文档编辑器将为每个文档保留一个对象,而聊天应用程序将为每个聊天保留一个对象。创建成万上亿个对象也不会有问题,因为每个对象的开销都极小。
杀手锏应用:实时协同文档编辑
假设您有一个电子表格编辑器应用程序,或者任何一种可供用户编辑复杂文档的应用程序。它对一个用户运作良好,但您现在希望多个用户能够同时进行编辑。如何实现这个目标?
对于标准 Web 应用程序堆栈,这是一道难题。传统数据库根本不具有实时设计。您希望,当爱丽丝和鲍勃编辑同一电子表格时,爱丽丝的每一次击键都立即显示在鲍勃的屏幕上,反之亦然。不过,如果仅将击键存储到数据库中,并让用户反复轮询数据库以获取新的更新,则最好的情况下您的应用程序将具有欠佳的延迟,而在最坏情况下,您可能会发现数据库事务反复失败,因为不同位置的用户可能会争抢着编辑相同内容。
解决此问题的秘诀在于拥有一个实时协调中心。爱丽丝和鲍勃连接到同一个协调者,这通常使用 WebSockets。然后,协调者将爱丽丝的击键转发给鲍勃,并将鲍勃的击键转发给爱丽丝,无需经过存储层。当爱丽丝和鲍勃同时编辑相同内容时,协调者会即时解决冲突。然后,协调者可以负责更新存储中的文档,但由于协调者将文档的实时副本保留在内存中,因此写回存储可以异步进行。
所有著名的实时协同文档编辑器都以这种方式工作。但对于许多 Web 开发人员来说,尤其是在无服务器基础结构上进行开发的那些,这种解决方案一直都无法触及。标准的无服务器基础结构,甚至是更为广泛的云基础结构,都不能轻易做到分配这些协调点并引导用户与服务器的同一实例进行通信。
Durable Object 能够轻松做到。它不仅使分配协调点变得容易,而且 Cloudflare 会自动在使用它的用户附近创建协调者,并根据需要进行迁移,从而最大程度地减少延迟。提供本地持久存储意味着,即使最终的长期存储速度较慢,也可以立即可靠地保存对文档所做的更改。或者,您甚至可以将整个文档存储在边缘,彻底抛弃数据库。
随着 Durable Object 把门槛降低,我们希望实时协调变成整个网络的标杆。不再有任何理由让用户通过刷新来获得更新了。
示例:原子计数器
export class Counter {
// Constructor called by the system when the object is needed to
// handle requests.
constructor(controller, env) {
// `controller.storage` is an interface to access the object's
// on-disk durable storage.
this.storage = controller.storage
}
// Private helper method called from fetch(), below.
async initialize() {
let stored = await this.storage.get("value");
this.value = stored || 0;
}
// Handle HTTP requests from clients.
//
// The system calls this method when an HTTP request is sent to
// the object. Note that these requests strictly come from other
// parts of your Worker, not from the public internet.
async fetch(request) {
// Make sure we're fully initialized from storage.
if (!this.initializePromise) {
this.initializePromise = this.initialize();
}
await this.initializePromise;
// Apply requested action.
let url = new URL(request.url);
switch (url.pathname) {
case "/increment":
++this.value;
await this.storage.put("value", this.value);
break;
case "/decrement":
--this.value;
await this.storage.put("value", this.value);
break;
case "/":
// Just serve the current value. No storage calls needed!
break;
default:
return new Response("Not found", {status: 404});
}
// Return current value.
return new Response(this.value);
}
}
这是一个 Durable Object 的极简示例,它可以递增、递减,并通过 HTTP 读取。即使同时接收到来自多个客户端的请求,此计数器也具有 一致性,不丢失任何增量或减量。同时,读取完全由内存提供,不需要访问磁盘访问。
// Derive the ID for the counter object named "my-counter".
// This name is associated with exactly one instance in the
// whole world.
let id = COUNTER_NAMESPACE.idFromName("my-counter");
// Send a request to it.
let response = await COUNTER_NAMESPACE.get(id).fetch(request);
类绑定至 Durable Object 命名空间后,即可通过以下代码从世界上任何地方访问 counter
的特定实例:
演示:聊天
聊天大概是最纯粹的实时协作。为此,我们制作了一个演示用的开源聊天应用,该应用使用 Durable Object 完全在边缘运行。
此聊天应用使用一个 Durable Object 来控制每个聊天室。用户使用 WebSockets 连接到这个对象。来自一个用户的消息广播到所有其他用户。聊天记录也存储在持久存储中,但这仅用作历史记录。实时消息直接从一个用户中继到其他用户,不经过存储层。
另外,此演示还使用 Durable Object 实现第二个目的:对来自任何特定 IP 的消息应用速率限制。每个 IP 都分配到一个 Durable Object,跟踪最近的请求频率,因此用户发送过多消息时可能会被暂时屏蔽,即使在多个聊天室发送也一样。有趣的是,这些对象实际上不存储任何持久状态,因为它们只关心最近的历史记录,并且速率限制器偶尔随机重设也无关紧要。因此,这些速率限制器对象是不带存储的纯协调对象的示例。
这个聊天应用只有几百行代码。部署配置也只有几行。但是,它可以无缝扩展到任意数量的聊天室,仅受 Cloudflare 可用资源的限制。当然,由于每个对象都是单线程的,任何个体聊天室的可扩展性存在限制。不过,这个限制远超人类参与者所及的范围。
其他用例
Durable Object 具有无限的用例。除了上述以外,下方仅列出了一些想法:
购物车:在线商店可以在一个对象中跟踪用户的购物车。商店的其余部分可通过完全静态的网站来提供。Cloudflare 会自动将购物车对象托管在最终用户附近,最大程度减少延迟。
游戏服务器:多人游戏可以在一个对象中跟踪比赛状态,该对象托管于靠近玩家的边缘。
IoT 协调:住宅中的设备可以通过一个对象进行协调,而无需与远程服务器通信。
社交摘要:每个用户可以拥有一个汇总其订阅的 Durable Object。
评论/聊天小部件:原本是静态内容的网站可以在个体文章上添加评论小部件,甚至是实时聊天小部件。每篇文章使用单独的 Durable Object 进行协调。这样,源服务器可以仅关注于静态内容。
未来:真正的边缘数据库
我们将 Durable Object 视为构建分布式系统的低级原语。如上文提到的那些,某些应用程序可以直接使用对象来实施协调层,甚至可以用作其唯一存储层。
但是,如今的 Durable Object 还不是完整的数据库解决方案。每个对象只能查看自己的数据。要在多个对象之间执行查询或事务,应用程序需要做一些额外工作。
即便如此,每个大型分布式数据库(无论是关系数据库、文档,还是图形等)在某种程度上都是由存储大量整体数据的“块”或“碎片”组成。分布式数据库的职责是在块之间进行协调。
在我们看来,未来的边缘数据库可将每个“块”存储为一个 Durable Object。如此一来,我们就有可能构建彻底在边缘运行、完全分布式,而且没有区域或原籍的数据库。这样的数据库不必由我们构建;任何人都可以在 Durable Object 基础上构建它们。Durable Object 只是边缘存储之旅的第一步。
参与 Beta 测试
存储数据是一项重要责任,不能掉以轻心。由于正确处理至关重要,我们必须审慎对待。我们会在未来几个月中逐步推出 Durable Object。
Request a beta invite »与所有 Beta 版本一样,这个产品仍在开发中,本文所述的某些功能尚未完全启用。有关 Beta 版限制的完整详情可以从这些文档中找到。
如果您要立即试用 Durable Object,请告诉我们您的用例。我们会选择最有趣的用例来提供早期访问。
问答
Durable Object 是否能服务 WebSocket?
是。
作为 Durable Object 测试的一部分,我们使 Workers 能够充当 WebSocket 端点,包括担当客户端或服务器。在这之前,Workers 可以将 WebSocket 连接代理到后端服务器,但是不能直接表明协议。
尽管在技术上任何 Worker 都可以这种方式表明 WebSocket,但 WebSocket 只有与 Durable Object 结合才最有用处。当客户端使用 WebSocket 连接应用程序时,您需要一种方式,使服务器生成的事件发回到现有的套接字连接。若无 Durable Object,就无法将事件发送到保管 WebSocket 的特定 Worker。使用 Durable Object 后,您便可以将 WebSocket 转发到某个对象。然后,消息可以通过唯一 ID 寻址到这个对象,接着对象可以将这些消息通过 WebSocket 转发到客户端。
上文中的聊天应用演示使用了 WebSocket。您可以查看源代码来了解其工作方式。
这与 Workers KV 相比有何异同?
两年前,我们推出了全局键值数据存储 Workers KV。KV 是一个相当简约的全局数据存储,可以很好地满足某些用户,但并非所有人都适合。KV 具有最终一致性;也就是说,在一个位置进行的写入可能无法立即在其他位置上看到。而且,它实施的是“最后写入为准”语义。这意味着,如果同时从世界上多个位置修改单个键,则这些写入很容易彼此覆盖。KV 采用这种设计方式,以支持对不常更改的数据进行低延迟读取。但是,这些设计决策使 KV 不适合频繁更改的状态,也不适合需要在全世界立即可见的更改。
相比之下,Durable Object 的主要功用不是存储产品,许多用例实际上并未利用持久存储。就它们确实提供存储的程度而言,Durable Object 在存储范围中位于和 KV 相对的另一端,特别适合需要事务保证和即时一致性的工作负载。不过,由于事务天生必须在单一位置上协调,并且由于光速的固有限制,位于和该位置相对的世界另一端的客户端会遭遇适度的延迟。Durable Object 通过自动迁移到靠近使用对象的地方来解决这个问题。
简而言之,Workers KV 依旧是在全世界提供静态内容、配置和其他鲜有更改的数据的最佳方法,而 Durable Object 则更加适合管理动态状态和协调。
未来,我们计划在 Workers KV 本身的实施中利用 Durable Object,以进一步提高性能。
为什么不使用 CRDT?
您可以在 Durable Object 基础上构建基于 CRDT 的存储,但 Durable Object 不要求您使用 CRDT。
无冲突复制数据类型(CRDT)或它们的表亲(操作转换(OT))是一种可以从世界上多个地方同时编辑数据,无需同步且不会丢失数据的技术。例如,这些技术通常用于实时协同文档编辑器的实施中,以便用户的击键可以实时显示在其本地文档副本中,而无需等待获知其他人是否先编辑了文档的另一部分。若不深究细节,这些技术可以比作实时版本的“git fork”和“git merge”,当中的所有合并冲突都以确定性方式自动解决,因此一切都以相同状态告终。
CRDT 是功能强大的技术,但正确运用可能颇具挑战。只有某些类型的数据结构才能以不轻易导致数据丢失的方式自动解决冲突。任何熟悉 git 的开发人员都能发现这个问题:任意冲突解决颇有难度,而且任何自动执行它的算法都有可能在某些时候出错。如果算法必须以任意顺序处理合并,而且仍要获得相同的答案,那就难上加难了。
我们认为,CRDT 对于大多数应用程序而言过于复杂,不值得去做。更糟糕的是,可以表示为 CRDT 的数据结构集对于许多应用程序而言也太过有限。为每个文档分配一个权威协调点通常要容易得多,而这恰恰是 Durable Object 能够实现的。
总之,可以在 Durable Object 基础上使用 CRDT。如果对象的状态适合用 CRDT 来处理,则应用程序可以将该对象复制到为不同区域服务的多个对象中,然后通过 CRDT 同步其状态。如果值得,那么应用程序可以将它作为优化来实施。
最后一点,状态变为“无服务器”意味着什么?
传统上看,无服务器专注于无状态计算。在无服务器架构中,计算的逻辑单元简化为一种细粒度的事务:单个事件,例如 HTTP请求。这特别有效,因为事件恰好是我们在设计服务器应用程序时考虑的逻辑工作单元。没有人以“服务器”、“容器”或“进程”为单位来考虑业务逻辑,我们考虑的是事件。正是由于这种语义上的一致,无服务器成功地将维护服务器的大量后勤负担从开发人员转交给云提供商。
然而,无服务器架构在传统上是无状态的。每个事件都独立执行。如果要存储数据,您必须连接到传统数据库。如果要在请求之间进行协调,您必须连接到提供该功能的其他服务。这些外部服务往往重新带来无服务器原本要避免的运营问题。开发人员和服务运营商不仅要操心扩展数据库以处理不断增加的负载,还要操心如何将其数据库拆分为“区域”以有效处理全局流量。后者可能会特别麻烦。
那么,我们如何将无服务器理念运用到状态呢?正如无服务器计算将计算拆分为细粒度的片段,无服务器状态将状态拆分成细粒度的片段。同样,我们试图寻找一种状态单元来与应用程序中的逻辑单元相对应。应用程序中的逻辑状态单元不是“表”、“集合”或“图形”。相反,它取决于应用程序。聊天应用程序中的逻辑状态单元是聊天室,在线电子表格编辑器中的逻辑状态单元是电子表格,而在线商店中的逻辑状态单元是购物车。通过使存储层提供的物理存储单元与应用程序中固有的逻辑状态单元相匹配,我们可以允许底层存储提供者(Cloudflare)负责处理各种以前属于开发人员职责的诸多后勤问题,包括可扩展性和区域性。
这便是 Durable Object 的功用。