打破 [无服务器13] 中大多数 WebSocket 实现的假设是让多个 Worker 接受连接解决了扩展问题。它并没有解决这个问题:它创建了几个孤立的世界,每个客户端只与自己的 Worker 实例对话,而看不到谁与其他客户端连接。打开到同一端点的连接的两个客户端可以位于完全不同的 Worker 上,它们之间没有交换消息的通道。这种隔离正是使 Workers 能够扩展的原因,也正是在没有外部协调层的情况下,任何实时存在或协作功能都无法实现的原因。
为什么孤立的 Workers 不足以进行多人游戏
想象一下房间聊天。十个连接的客户端。房间作为应用程序中的概念存在,但在 Worker 中的任何位置都不作为内存中的对象存在。每个 WebSocket 连接都由一个不了解其他连接的 Worker 接受。当客户端 A 发送消息时,接收该消息的 Worker 无法到达连接到其他 Worker 的其他 9 个客户端。
传统的解决方案是添加一个外部层:Pub/Sub(Redis、Upstash)、[用于持久化消息和轮询的数据库14],或专用的 WebSocket 服务(如 Ably 或 Pusher)。所有这些解决方案都有效,但它们增加了延迟一跳、需要管理的服务以及随着活动连接而不是实际使用而扩展的成本。
耐用对象会改变协调点。房间并不隐含在应用程序概念中:它成为一个通过名称标识的 DO。所有想要进入“room-456”的客户端都被路由到同一个 DO,该 DO 将 WebSocket 连接列表保留在内存中,并且可以传递一对多消息而无需外部往返。协调是 DO 的本地工作。
无休眠的基本模型及成本
在 DO 内直接实现广播很简单。 DO 在内存中保留 34 个对象,接受新连接,并迭代它们以传递消息:
0
这段代码有效。当您分析计费模型时,就会出现成本问题:当此 DO 打开 WebSocket 连接并正在处理事件侦听器时,它处于唤醒状态。即使没有客户端发送消息,DO 仍然处于活动状态并消耗 GB 秒。对于一个有 5 个用户闲置 8 小时的房间,DO 处于活动状态 8 小时 — 并且您需要为该期间内的每一秒计算付费。
Hibernation API 及其成本模型的变化
WebSocket Hibernation API 扭转了这种成本。您无需在内存中保留活动连接,而是使用 5 而不是 6 将控制权传递给 Cloudflare。从那里开始,即使在 DO 休眠时,Cloudflare 也会保持 WebSocket 连接打开。当客户端发送消息时,Cloudflare 会唤醒 DO,通过方法 7 传递消息,DO 处理完成后可以再次休眠。
1
在休眠状态下,DO 计算成本与处理消息的时间成正比,而不是与打开连接的时间成正比。一个房间里有五个用户,安静地呆上八个小时,计算成本几乎为零。当用户积极地互相发送消息时,真正的成本就会出现。对于经常不活动的协作应用程序(大多数协作者打开但不持续编辑的共享文档),直接模型和休眠模型之间的成本差异可能是一两个数量级。
8 会返回由睡眠框架管理的所有活动连接 — 相当于您手动维护的 9 ,但由平台在睡眠之间保留。这意味着当 DO 唤醒时您不需要重建会话列表:它已经可用。
休眠之间的持久状态
一个细节吸引了那些来自直接模型的人:当 DO 睡眠并醒来时,构造函数会再次被调用,但内存中的状态(10、11、任何实例变量)都会丢失。只有休眠框架管理的存储和 WebSocket 连接才能存活。
对于需要在休眠状态下生存的状态(房间元数据、消息历史记录、用户状态),存储是正确的位置:
2
有效的模式:仅将可从存储中导出并可在休眠期间丢弃的内容保留在内存中。坚持储存生存所需的一切。必要时从构造函数中的12启动。
通过此模型您可以获得什么
请求序列化、WebSocket 休眠和警报 API 的组合解决了一组特定问题,无需额外的基础设施。实时协作编辑,多个客户端修改同一文档并需要以低延迟查看更新。用户的存在——知道谁在房间里在线——即使同时进入和退出,列表也需要保持一致。具有简单会话状态的多人游戏,其中更新的频率证明了集中协调的合理性。参与者之间共享计时器,就像会议中的倒计时一样,每个人都需要保持一致。
吞吐量上限仍然存在:在 2 毫秒内处理每条消息的 DO 每秒可以处理来自不同客户端的 500 条消息。对于同时有几十个活跃用户的房间来说,这个上限永远不会达到。对于数百个客户端连续向同一个 DO 发送消息的情况,有必要按房间或房间组进行分片。
这个模型没有解决什么问题
离线到达的用户的消息历史记录持久性是一个单独的问题。 DO 存储在 DO 存在时存储历史记录,但它不是查询库。要搜索某个时间段内的消息、按用户筛选或执行任何利用 SQL 的操作,您需要 D1 作为 DO 填充每条消息的补充存储。
地理规模也有局限性。 DO 存在于单个 PoP 中。对于位于相距很远的地区的用户在同一房间进行协作的应用程序,消息延迟包括到 DO 所在 PoP 的往返行程,对于圣保罗的用户来说可能是法兰克福。对于大多数协作应用程序来说,这种延迟是可以接受的。对于要求所有玩家延迟低于 50 毫秒的游戏,架构需要有所不同。
具有 WebSocket 休眠功能的持久对象可以在没有专用服务器的情况下解决多人游戏的真实用例,其成本模型有利于用户花更多时间阅读而不是写作的应用程序。在这个范围之外,局限性很快就会显现出来。
另请阅读
- [Cloudflare Durable Objects:边缘的一致状态 - 真正改变的是什么15
- [生产中的耐用物品:法案将是什么样子以及令人惊讶的限制16
- [持久对象编程模型:与您曾经使用过的任何东西不同的地方17
- [当耐用物品是错误答案时18
- [Cloudflare DNS:远远超出解析名称的网络基础设施19
- [Cloudflare 负载均衡和地理转向:当 DNS 成为智能流量层20
