工作者持久对象Beta:一种实现有状态无服务器的新方法

2020-09-28 21:36:33

我们在2017年发布了Cloudflare Workers®,抱有一个激进的愿景:运行在网络边缘的代码不仅可以提高性能,而且比运行在单个数据中心的代码更容易部署,运行成本更低。这一愿景意味着员工不仅仅是边缘计算--我们正在重新考虑如何构建应用程序。

使用无服务器方法使我们的部署变得非常简单,使用隔离技术使我们能够以更低的成本交付无服务器,而且不会像其他提供商那样经历漫长的冷启动。我们使用Worker KV向平台添加了易于使用、最终一致的边缘存储。

但直到今天,它还不可能以强大的一致性管理状态,或在多个客户端之间进行实时协调,完全处于边缘状态。因此,您的应用程序的这些部分仍然必须驻留在其他地方。

持久对象提供了一种真正的无服务器存储和状态方法:一致、低延迟、分布式,而且维护和扩展都很轻松。它们还提供了一种在客户端之间进行协调的简单方法,无论是特定聊天室中的用户、特定文档的编辑者,还是特定智能家居中的物联网设备。持久对象是Worker堆栈中缺少的一部分,它使整个应用程序完全可以在边缘运行,根本不需要集中的源服务器。

请求测试版邀请»我要老实说:给这个产品命名很难,因为它和今天广泛使用的任何其他云技术都不太一样。这个众所周知的自行车棚有很多层油漆,但最终我们选择了独特的耐用物品,或简称耐用物品。让我通过分析来解释它们是什么:

对象:持久对象是面向对象编程意义上的对象。持久对象是类的实例--字面意思是用JavaScript(或您选择的语言)编写的类定义。该类具有定义其公共接口的方法。对象是该类的实例,它将代码与某种私有状态组合在一起。

唯一:每个对象都有一个全局唯一的标识符。那个物体一次只存在于整个世界的一个位置。在世界任何地方运行的任何知道该对象ID的工作人员都可以向其发送消息。所有这些信息最终都被送到了同一个地方。

持久性:与JavaScript中的普通对象不同,持久性对象可以将持久性状态存储在磁盘上。每个对象的持久状态都是它的私有状态,这意味着不仅对存储的访问速度很快,而且对象甚至可以安全地在内存中维护状态的一致副本,并以零延迟对其进行操作。内存中的对象将在空闲时关闭,并在以后按需重新创建。

储物:每个物体都有固定的耐用储物。由于此存储是特定对象的私有存储,因此该存储始终与该对象位于同一位置。这意味着存储可以非常快,同时提供强大的事务一致性。持久对象将无服务器理念应用于存储,将传统的大型单片数据库拆分成许多小的逻辑单元。通过这样做,我们获得了您期望从无服务器中获得的优势:轻松扩展,零维护负担。

协调:过去,使用Worker时,每个请求都会随机负载平衡到一个Worker实例。由于无法控制哪个实例收到请求,因此无法强制两个客户端与同一个Worker对话,因此客户端无法通过Worker进行协调。持久对象改变了这一点:与同一主题相关的请求可以被转发到相同的对象,然后该对象可以在它们之间进行协调,而不需要触及存储。例如,这可用于促进实时聊天、协作编辑、视频会议、发布/订阅消息队列、游戏会话等。

精明的读者可能会注意到,许多协调用例都需要WebSocket--事实上,相反,大多数WebSocket用例都需要协调。由于这种互补关系,以及持久对象测试版,我们还向Workers添加了WebSocket支持。有关这方面的更多信息,请参阅下面的问答。

在使用持久对象时,Cloudflare会自动确定每个对象将驻留的Cloudflare数据中心,并且可以根据需要在不同位置之间透明地迁移对象。

传统的数据库和有状态的基础设施通常要求您考虑地理区域,这样您就可以确保将数据存储在离使用位置很近的地方。考虑区域通常是一种不自然的负担,特别是对于那些本质上不具有地理位置的应用程序而言。

使用持久对象,您可以将存储模型设计为与应用程序的逻辑数据模型相匹配。例如,文档编辑器对每个文档都有一个对象,而聊天应用程序对每个聊天都有一个对象。创建数百万或数十亿个对象是没有问题的,因为每个对象的开销都很小。

假设你有一个电子表格编辑器应用程序--或者说,任何一种用户可以编辑复杂文档的应用程序。它对一个用户非常有效,但是现在您希望多个用户能够同时编辑它。你是如何做到这一点的?

对于标准的Web应用堆栈来说,这是一个难题。传统的数据库根本不是为实时而设计的。当Alice和Bob编辑同一电子表格时,您希望Alice的每个击键都立即出现在Bob的屏幕上,反之亦然。但是,如果您只是将击键存储到数据库中,并让用户反复轮询数据库以获取新的更新,则在最好的情况下,您的应用程序将具有较低的延迟,而在最坏的情况下,您可能会发现数据库事务反复失败,因为世界两端的用户都在争夺编辑相同的内容。

解决这个问题的秘诀是有一个实时的协调点。Alice和Bob通常使用WebSocket连接到同一协调器。然后协调器将爱丽丝的击键转发给鲍勃,并将鲍勃的击键转发给爱丽丝,而不必经过存储层。当Alice和Bob同时编辑相同的内容时,协调器会立即解决冲突。然后,协调器可以负责更新存储中的文档--但是因为协调器在内存中保存文档的实时副本,所以可以异步地写回存储。

每个大名鼎鼎的实时协同文档编辑器都是这样工作的。但对于许多Web开发人员,特别是那些建立在无服务器基础设施上的开发人员来说,这种解决方案长期以来一直遥不可及。标准的无服务器基础设施--甚至更一般的云基础设施--只是不容易分配这些协调点并引导用户与您的服务器的同一实例对话。

经久耐用的物品让这一切变得容易。它们不仅使分配协调点变得容易,而且Cloudflare会自动在靠近使用协调点的用户处创建协调器,并根据需要进行迁移,从而将延迟降至最低。本地持久存储的可用性意味着即使最终的长期存储速度较慢,也可以立即可靠地保存对文档的更改。或者,您甚至可以将整个文档存储在边缘并完全放弃您的数据库。

随着耐用物品降低了门槛,我们希望看到实时协作成为整个网络的标准。不再有任何理由让用户刷新以获取更新。

这里是一个非常简单的持久对象示例,它可以通过HTTP递增、递减和读取。即使在接收到来自多个客户端的同时请求时,此计数器也是一致的--不会丢失任何增量或减量。同时,读取完全从内存进行,不需要访问磁盘。

导出类计数器{//需要对象处理请求时系统调用的构造函数。Constructor(Controller,env){//`Controler.storage`是访问Object';s//盘上持久存储的接口。This.storage=Controler.storage}//从下面的FETCH()调用的私有帮助器方法。异步初始化(){let store=等待this.storage.get(";value";);this.value=Stored||0;}//处理来自客户端的HTTP请求。/系统在向Object发送HTTP请求时调用该方法。//。请注意,这些请求严格来自员工的其他//部分,而不是来自公共互联网。异步获取(请求){//确保已从存储完全初始化。If(!this.initializePromise){this.initializePromise=this.initialize();}等待this.initializePromise;//应用请求的操作。让url=new URL(request.url);switch(url.pathname){case";/increate";:++this.value;等待this.storage.put(";value";,this.value);Break;case";/deducment";:--this.value;等待this.storage.put(";value";,this.value);Break;case";/";://只服务当前值。不需要存储呼叫!Break;默认:返回新响应(";未找到";,{status:404});}//返回当前值。返回新响应(this.value);}}。

一旦类被绑定到持久对象命名空间,就可以使用如下代码从世界任何地方访问计数器的特定实例:

//派生名为";my-Counter";的计数器对象的ID。//此名称与//整个世界中的一个实例相关联。//让id=COUNTER_NAMESPACE.idFromName(";my-counter";);//向其发送请求。let Response=等待COUNTER_NAMESPACE.GET(Id).FETCH(Request);

聊天可以说是最纯粹的实时协作。为此,我们构建了一个演示开源聊天应用程序,它完全使用耐用对象在边缘运行。

试试现场演示»查看GitHub上的源代码»这款聊天应用程序使用一个持久的对象来控制每个聊天室。用户使用WebSocket连接到对象。来自一个用户的消息被广播给所有其他用户。聊天历史记录也存储在持久存储中,但这仅用于历史记录。实时消息直接从一个用户中继到其他用户,而无需通过存储层。

此外,此演示还将持久对象用于第二个目的:对来自任何特定IP的消息应用速率限制。每个IP都分配了一个跟踪最近请求频率的持久对象,因此可以临时阻止发送太多消息的用户--即使在多个聊天室之间也是如此。有趣的是,这些对象实际上根本不存储任何持久状态,因为它们只关心最近的历史记录,如果偶尔随机重置速率限制器,也没什么大不了的。因此,这些速率限制器对象是没有存储的纯协调对象的示例。

这款聊天应用程序只有几百行代码。部署配置只有几行。然而,它可以无缝扩展到任意数量的聊天室,仅受Cloudflare可用资源的限制。当然,任何单独聊天室的可伸缩性都是有限的,因为每个对象都是单线程的。但是,这一限制远远超出了人类参与者无论如何都能跟上的范围。

耐久的物品有无限的用途。除了上面描述的几个想法外,这里只有几个想法:

购物车:在线店面可以在物体中跟踪用户的购物车。店面的其余部分可以作为一个完全静态的网站。CloudFlare将在接近最终用户的位置自动托管购物车对象,从而最大限度地减少延迟。

游戏服务器:多人游戏可以在靠近玩家的边缘托管的对象中跟踪比赛状态。

物联网协调:家庭内部的设备可以通过一个对象进行协调,无需与远程服务器通信。

评论/聊天小部件:原本是静态内容的网站可以在个别文章上添加评论小部件,甚至是实时聊天小部件。每个项目将使用单独的持久对象进行协调。这样,源站可以只关注静态内容。

我们将持久对象视为构建分布式系统的低级原语。一些应用程序,如上面提到的那些,可以直接使用对象来实现协调层,甚至可能作为它们的唯一存储层。

然而,今天的持久对象并不是一个完整的数据库解决方案。每个对象只能看到其自己的数据。要跨多个对象执行查询或事务,应用程序需要做一些额外的工作。

也就是说,每个大型分布式数据库--无论是关系数据库、文档数据库还是图形数据库等--在某种程度上都是由存储整体数据的块或碎片组成的。分布式数据库的工作是在块之间进行协调。

我们看到了EDGE数据库的未来,它将每个块存储为持久对象。通过这样做,将有可能构建完全在边缘运行、完全分布、没有区域或主位置的数据库。这些数据库不需要由我们构建;任何人都可以潜在地在持久对象之上构建它们。耐用对象只是边缘存储之旅的第一步。

存储数据是一项重大的责任,我们不能掉以轻心。因为做好这件事至关重要,我们非常小心。在接下来的几个月里,我们将逐步提供耐用的产品。

与任何测试版一样,此产品仍在开发中,本文中描述的某些功能尚未完全启用。测试版限制的完整详细信息可以在文档中找到。

如果您现在想尝试耐用对象,请告诉我们您的用例。我们将选择最有趣的用例进行早期访问。

请求测试版INVITE»作为持久对象测试版的一部分,我们已经使Worker能够充当WebSocket端点--包括作为客户端或服务器。在此之前,Worker可以将WebSocket连接代理到后端服务器,但不能直接使用该协议。

虽然从技术上讲,任何工作者都可以以这种方式使用WebSocket,但WebSocket在与持久对象结合时最有用。当客户端使用WebSocket连接到您的应用程序时,您需要一种将服务器生成的事件发送回现有套接字连接的方法。没有持久对象,就无法向持有WebSocket的特定工作者发送事件。有了持久对象,您现在可以将WebSocket转发到对象。然后,可以通过该对象的唯一ID将消息寻址到该对象,然后该对象可以将这些消息沿WebSocket转发到客户端。

上面展示的聊天应用程序演示使用WebSockets。查看源代码,看看它是如何工作的。

两年前,我们引入了Worker KV,这是一个全局键值数据存储。KV是一个相当简约的全局数据存储,可以很好地服务于某些目的,但并不适合每个人。KV最终是一致的,这意味着在一个位置进行的写入可能不会立即在其他位置可见。此外,它还实现了上次写入的WINS语义,这意味着如果从世界上的多个位置同时修改单个密钥,这些写入很容易相互覆盖。KV旨在支持对不经常更改的数据进行低延迟读取。然而,这些设计决定使得KV不适合于频繁变化的州,或者当变化需要立即在全球范围内看到的时候。

相比之下,持久对象根本不是一种存储产品--它们的许多用例实际上并不利用持久存储。就它们确实提供存储的程度而言,耐用对象位于与KV相反的存储光谱的另一端。它们非常适合需要事务保证和即时一致性的工作负载。但是,由于交易本质上必须在单个位置进行协调,并且由于光速的固有限制,与该位置相对的世界另一端的客户端将经历适度的延迟。持久对象将通过自动迁移到靠近其使用位置的位置来解决此问题。

简而言之,Workers KV仍然是为世界各地的静态内容、配置和其他很少更改的数据提供服务的最佳方式,而持久对象更适合于管理动态和协调。

展望未来,我们计划在Worker KV本身的实现中使用持久对象,以便提供更好的性能。

您可以在持久对象之上构建基于CRDT的存储,但是持久对象不要求您使用CRDT。

无冲突复制数据类型(CRDT)或其近亲操作转换(OT)是一种允许从世界上多个地方同时编辑数据而无需同步且不会丢失数据的技术。例如,这些技术通常用于实现实时协作文档编辑器,因此用户的按键可以实时显示在文档的本地副本中,而无需等待是否有人首先编辑了文档的另一部分。无需详细介绍,您可以将这些技术想象为实时版本的git fork和git merge,其中所有合并冲突都会以确定性的方式自动解决,这样每个人最终都会得到相同的状态。(#34;git fork&34;and";git merge";)在这些技术中,所有合并冲突都会以确定性的方式自动解决,因此每个人最终都会得到相同的状态。

CRDT是一项强大的技术,但正确应用它们可能是具有挑战性的。只有某些类型的数据结构才能以不容易导致数据丢失的方式自动解决冲突。任何熟悉git的开发人员都能看到这个问题:任意解决冲突是很困难的,而且任何针对它的自动算法有时都可能出错。如果算法必须以任意顺序处理合并,并且仍然得到相同的答案,那就更加困难了。

我们认为,对于大多数应用程序来说,CRDT过于复杂,不值得努力。更糟糕的是,可以表示为CRDT的数据结构集对于许多应用程序来说太有限了。通常,为每个文档分配一个权威协调点要容易得多,这正是持久对象所完成的任务。

也就是说,CRDT可以用在持久对象之上。如果对象的状态适合于CRDT处理,则应用程序可以将该对象复制到服务于不同区域的几个对象中,然后这些对象通过CRDT同步它们的状态。如果应用程序发现这是值得的,那么将其作为优化来实现是有意义的。

传统上,无服务器侧重于无状态计算。在无服务器体系结构中,计算的逻辑单元被简化为细粒度的东西:单个事件,如HTTP请求。这特别有效,因为事件恰好是我们在设计服务器应用程序时考虑的逻辑工作单元。没有人以服务器或容器或进程为单位来考虑他们的业务逻辑--我们考虑的是事件。正是由于这种语义一致性,Serverless成功地将维护服务器的大量后勤负担从开发人员转移到了云提供商。

然而,无服务器体系结构传统上一直是无状态的。每个事件都独立执行。如果要存储数据,则必须连接到传统数据库。如果您希望在请求之间进行协调,则必须连接到提供该功能的其他服务。这些外部服务往往会重新引入运营方面的问题。

.