当在多个客户端之间共享一个巨大的集合时,Meteor可以有多有效率?

100

想象以下情况:

  • 有1,000个客户端连接到Meteor页面,显示“Somestuff”集合的内容。

  • “Somestuff”是一个包含1,000个项目的集合。

  • 有人向“Somestuff”集合中插入了一个新项目。

会发生什么:

  • 所有客户端上的Meteor.Collection都将更新,即将插入消息转发给它们所有人(这意味着向1,000个客户端发送一条插入消息)

服务器计算机的成本是多少,以确定需要更新哪个客户端?

只有插入的值会被转发到客户端,而不是整个列表,这是否准确?

在现实生活中,这是如何工作的?是否有可用的如此大规模的基准测试或实验?

4个回答

119

简而言之,只有新数据会通过网络发送。以下是其工作原理。

Meteor服务器有三个重要的部分来管理订阅:发布函数(publish function)定义订阅提供的数据逻辑;Mongo驱动程序(Mongo driver)监视数据库更改;合并盒子(merge box)将客户端的所有活动订阅组合并通过网络发送给客户端。

发布函数

每次Meteor客户端订阅一个集合时,服务器都会运行一个发布函数。发布函数的工作是找出其客户端应该拥有的文档集,并将每个文档属性发送到合并盒子中。它为每个新的订阅客户端运行一次。您可以在发布函数中放置任何JavaScript代码,例如使用this.userId进行任意复杂的访问控制。发布函数通过调用this.addedthis.changedthis.removed将数据发送到合并盒子中。请参见完整的发布文档以获取更多详细信息。

大多数发布函数不需要涉及低级的addedchangedremoved API。如果一个发布函数返回一个Mongo游标,Meteor服务器会自动将Mongo驱动程序的输出(insertupdateremoved回调)连接到合并框的输入(this.addedthis.changedthis.removed)。这很棒,你可以在发布函数中提前进行所有权限检查,然后直接将数据库驱动程序连接到合并框,而不需要任何用户代码的干扰。当自动发布被打开时,甚至这一小部分也会被隐藏:服务器会自动设置一个查询来获取每个集合中的所有文档,并将它们推送到合并框中。
另一方面,您不仅限于发布数据库查询。例如,您可以编写一个发布函数,从Meteor.setInterval内部的设备读取GPS位置,或者从另一个Web服务中轮询遗留REST API。在这些情况下,您将通过调用低级别的addedchangedremoved DDP API向合并框发出更改。

Mongo驱动程序

Mongo驱动程序的工作是监视Mongo数据库以查看实时查询的更改。这些查询持续运行,并通过调用addedremovedchanged回调返回更新的结果。

Mongo不是一个实时数据库,因此驱动程序会进行轮询。它在内存中保留每个活动查询的最后一个查询结果副本。在每个轮询周期中,它将新结果与先前保存的结果进行比较,计算描述差异的addedremovedchanged事件的最小集合。如果多个调用者为同一活动查询注册回调,则驱动程序只监视查询的一个副本,并使用相同的结果调用每个已注册的回调。

每次服务器更新集合时,驱动程序会重新计算该集合上的每个实时查询(Meteor的未来版本将公开一个扩展API,用于限制在更新时重新计算哪些实时查询)。驱动程序还会定期轮询每个实时查询以捕获绕过Meteor服务器而对数据库进行的更新操作。

合并盒子

合并盒子的工作是将客户端的所有活动发布函数的结果(addedchangedremoved调用)合并成单个数据流。每个连接的客户端都有一个合并盒子。它保存了客户端的minimongo缓存的完整副本。

在您的例子中,只有一个订阅时,合并框本质上是一个传递。但更复杂的应用程序可能有多个订阅,这些订阅可能会重叠。如果两个订阅都在同一文档上设置了相同的属性,则合并框决定哪个值具有优先权,并仅将其发送给客户端。我们尚未公开设置订阅优先级的API。目前,优先级由客户端订阅数据集的顺序确定。客户端进行的第一个订阅具有最高优先级,第二个订阅次之,依此类推。
由于合并框保存客户端的状态,因此它可以发送最少量的数据以保持每个客户端的最新状态,无论发布函数提供了什么数据。

更新时发生了什么

现在,我们已经为您的场景做好了准备。
我们有1,000个连接的客户端。每个客户端都订阅了相同的实时Mongo查询(Somestuff.find({}))。由于每个客户端的查询都相同,因此驱动程序仅运行一个实时查询。有1,000个活动的合并框。每个客户端的发布函数在其中一个合并框中注册了addedchangedremoved,这些函数会将实时查询的结果馈送到合并框中。除此之外,没有其他内容连接到合并框上。
首先是Mongo驱动程序。当其中一个客户端向Somestuff插入新文档时,它会触发重新计算。Mongo驱动程序会重新运行Somestuff中所有文档的查询,将结果与内存中的上一个结果进行比较,发现有一个新文档,并调用每个已注册的1,000个insert回调函数。
接下来是发布函数。这里几乎没有什么事情要发生:每个1,000个insert回调函数通过调用added将数据推送到合并框中。

最后,每个合并框都会将这些新属性与其客户端缓存的内存副本进行比较。在每种情况下,它发现这些值尚未出现在客户端上,并且不会掩盖现有值。因此,合并框在SockJS连接上向其客户端发出DDP DATA消息,并更新其服务器端内存副本。

总CPU成本是对一个Mongo查询进行差异化的成本,加上1,000个合并框检查其客户端状态并构造新的DDP消息有效负载的成本。通过线路传输的唯一数据是发送到每个1,000个客户端的单个JSON对象,对应于数据库中的新文档,以及从进行原始插入的客户端向服务器发送的一个RPC消息

优化

以下是我们已经计划好的内容。

  • 更高效的Mongo驱动程序。我们在0.5.1中优化了驱动程序,使其每个不同查询只运行一个观察器。

  • 并非每个数据库更改都应该触发查询的重新计算。我们可以进行一些自动化改进,但最好的方法是提供API让开发人员指定哪些查询需要重新运行。例如,对于开发人员而言,将消息插入一个聊天室显然不应该使第二个房间的实时查询失效。

  • Mongo驱动程序、发布函数和合并盒子不需要在同一进程中运行,甚至不需要在同一台机器上运行。有些应用程序运行复杂的实时查询,并需要更多的CPU来监视数据库。其他应用程序只有几个不同的查询(比如博客引擎),但可能有很多连接的客户端——这些需要更多的CPU来进行合并操作。分离这些组件将使我们能够独立地扩展每个部分。

  • 许多数据库支持触发器,当更新行并提供旧行和新行时触发。有了这个功能,数据库驱动程序可以注册触发器而不是轮询更改。


有没有示例展示如何使用Meteor.publish发布非游标数据?比如提到答案中遗留的rest API接口返回的结果? - Tony
@Tony:文档里有。请查看房间计数示例。 - Mitar
值得注意的是,在0.7、0.7.1和0.7.2版本中,Meteor大多数查询(除了包含skip$near$where的查询)已经切换到OpLog Observe Driver,这种方式在CPU负载、网络带宽方面更加高效,并且允许应用服务器进行扩展。 - imslavko
如果不是每个用户都看到相同的数据怎么办?1.他们订阅了不同的主题。2.他们有不同的角色,因此在同一主题中,有一些消息是不应该达到他们的。 - tgkprog
@debergalis 关于缓存失效,也许你会从我的论文 http://vanisoft.pl/~lopuszanski/public/cache_invalidation.pdf 中找到有价值的想法。 - qbolec
但是每次应用程序/网站启动时,所有数据都必须发送给用户,对吗?与启动负载相比,更改本身很小。 - SoS

29

根据我的经验,在Meteor中共享大量数据集时,使用多个客户端基本上是行不通的,版本为0.7.0.1。我将试着解释一下原因。

如上述帖子所描述以及https://github.com/meteor/meteor/issues/1821中提到的,Meteor服务器必须为合并框中的每个客户端保留已发布数据的一个副本。这就是允许Meteor魔法发生的原因,但也导致任何大型共享数据库在节点进程的内存中被反复保留。即使使用静态集合的可能优化(例如在有办法告诉meteor一个集合是静态的(永远不会改变)吗?中所述),我们也遇到了CPU和内存使用问题。

在我们的情况下,我们向每个完全静态的客户端发布了一个包含15k个文档的集合。问题在于,将这些文档复制到客户端的合并框(在内存中)之后,Node进程基本上将CPU占满了近一秒钟,并导致大量的额外内存使用。这是本质上不可扩展的,因为任何连接的客户端都会使服务器崩溃(同时连接将互相阻塞),而内存使用量将随着客户端数量呈线性增加。在我们的情况下,每个客户端导致额外〜60MB的内存使用,即使传输的原始数据仅约为5MB。

针对我们的情况,因为数据集是静态的,我们通过将所有文档作为 .json 文件发送并使用nginx进行gzip压缩,并将它们加载到匿名集合中解决了这个问题。这样一来,在节点进程中仅需要传输约1MB的数据,而且不会增加额外的CPU或内存负担,加载时间更快。该集合上的所有操作都是使用服务器上较小出版物的 _id 进行的,从而保留了大部分Meteor的优势。这使应用程序能够扩展到更多客户端。此外,由于我们的应用程序主要是只读的,我们通过在nginx后面运行多个Meteor实例进行负载平衡(尽管只有一个Mongo),进一步提高了可扩展性,因为每个节点实例都是单线程的。

然而,将大型可写集合共享给多个客户端的问题是一个需要Meteor解决的工程问题。可能存在比为每个客户端保存一份副本更好的方法,但这需要像处理分布式系统问题那样进行认真思考。当前的巨大CPU和内存使用量无法实现可伸缩性。


@Harry 在这种情况下,oplog 是无关紧要的;数据是静态的。 - Andrew Mao
为什么它不对服务器端的minimongo副本进行差异比较?也许在1.0中所有这些都已经改变了?我的意思是通常它们应该是相同的,我希望即使是它回调的函数也应该是类似的(如果我遵循那个存储在那里并且可能不同的东西)。 - MistereeDevlord
@MistereeDevlord 目前更改的差异和客户端数据的缓存是分开的。即使每个人都有相同的数据,只需要一个差异,由于服务器无法将它们视为相同,因此每个客户端的缓存也不同。这可以通过现有实现更智能地完成。 - Andrew Mao
@AndrewMao 你如何确保在向客户端发送gzip文件时它们是安全的,即只有已登录的客户端才能访问它? - FullStack

4
您可以使用以下实验来回答这个问题:
  1. 安装一个测试用例:meteor create --example todos
  2. 在Webkit Inspector(WKI)下运行它。
  3. 检查通过网络传输的XHR消息的内容。
  4. 观察到整个集合没有被传输到网络上。
如需了解有关如何使用WKI的技巧,请参阅此文章。虽然有点过时,但对于这个问题仍然大部分有效。

2
轮询机制的解释:http://www.eventedmind.com/posts/meteor-liveresultsset - cmather

3

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接