可扩展应用中的点对点消息传递?

7
在搜索了类似WhatsApp这样的聊天应用程序中如何发送/接收消息后,我发现它们使用基于队列的消息系统。我正在尝试找出此功能的高级设计。
根据我的理解,高级设计如下:假设好友1和好友2在线。好友1已经建立了与Web服务器1的HTTP Web连接,而好友2已经建立了与Web服务器2的HTTP Web连接。好友1向好友2发送消息。
现在,当消息到达Web服务器1时,我需要将消息传递给Web服务器2,以便通过已建立的Web连接将消息推回到好友2。
我认为可以在此处使用分布式自定义Java队列,以将消息从一个服务器传播到另一个服务器。一旦消息到达一个服务器,它将使用消息内容、fromUserId、toUserId将其推送到分布式队列(由于负载平衡和高可用性而使用分布式队列)。队列上会有监听器,它会查看刚刚弹出的消息的目标用户ID,并查找目标用户ID在哪个Web服务器上是活动的。如果用户处于活动状态,则弹出消息并将其推送到客户端;否则,将其存储在数据库中,以便在用户上线时可以拉取。为了查看哪个用户在哪个服务器上处于活动状态,我们可以使用userId作为键,serverName作为值来维护treemap以进行高效查找。
实际设计可能比上述简要描述更复杂/可扩展。想知道这是否是可扩展聊天应用程序的正确方向?
此外,我认为我们需要拥有多个分布式队列而不是一个分布式队列来支持这样的可扩展应用程序。但是,如果我们拥有多个分布式队列,系统如何确保在分布式队列之间进行先进先出的消息传递?
2个回答

2
想知道这是否是可扩展聊天应用程序的正确方向?
使用消息队列设计此应用程序具有以下好处:
- 客户端和服务器之间的解耦以及减少故障影响:队列可以优雅地处理峰值流量,只需暂时增加队列大小,只要流量恢复正常(或任何瞬态故障已经修复),队列大小就会回到正常状态。 - 在消息应用程序中,客户端(移动设备)可能长时间离线。因此,同步设计不起作用,因为客户端可能无法访问以便进行消息传递。但是,使用异步设计(如使用消息队列),消息传递的责任在客户端上。因此,客户端一旦上线就可以轮询新消息。
因此,是的,这种设计在性能和可用性方面可能非常可扩展。唯一需要注意的是,这种设计需要为每个用户分别设置一个单独的队列,因此队列数量将与应用程序用户数量成线性比例增长(这可能是重要的财务和可扩展性问题)。
但是,如果我们有多个分布式队列,系统如何确保跨分布式队列的FIFO消息传递?
许多队列,无论是开源(rabbitMQ、activeMQ)还是商业(AWS SQS),都支持FIFO排序。但是,队列内部的FIFO保证还不够,因为由单个客户端发送的消息可能会由于网络中的异步问题而以不同的顺序传递到队列中(除非您使用单个、非分布式队列和TCP,它保证有序传递)。
但是,您可以在客户端上实现FIFO排序。按照这种方法,消息将包括一个时间戳,每个客户端在接收消息时将使用该时间戳对消息进行排序。这样做的唯一副作用是,客户端可能会看到一条消息,而没有先看到所有之前的消息。但是,当之前的消息到达时,它们将按正确的顺序显示在客户端的用户界面中,因此最终用户将看到所有的消息并按正确的顺序显示。

  1. 队列可以在用户创建帐户时创建一次。此队列将用于向该用户传递所有消息(对于他参与的所有聊天)。
  2. 当然还有其他可行的设计。提出单个用户每个队列的建议是因为队列消息只读一次。例如,如果您为每个聊天设置一个队列,则只有第一个用户会读取消息(而不是所有参与者)。
- Dimos
消息传递后,应存储在移动设备中。但是,您提到的情况适用于希望用户能够在卸载和重新安装应用程序时检索消息的情况。 - Dimos
当你说“现在大多数队列都将消息持久化到磁盘上。相对于数据库的优越性来自于访问模式(而不是内存与磁盘之间的区别)。”时,你是指现在的队列可以同时将数据保存在内存和数据库中吗?我相信这些队列必须是异步地将数据放入数据库中的。 - user3198603
1
@Dimos 谢谢。最后一个问题。您是否知道 WhatsApp 或任何其他主流聊天应用在创建用户帐户时是否真的使用了为每个用户创建专用内存队列的模型?我的意思是,WhatsApp 有超过 60 亿用户,所以我想知道他们是否真的创建了 60 亿个队列? - user3198603
@Dimos,你有时间的话能否分享一下你对http://stackoverflow.com/questions/42188593/facebook-services-architecture 的看法? - user3198603
显示剩余17条评论

1
 Would like to know if this is the right direction for scalable chat messenger?

我希望采用稍微不同的方法。你的想法是正确的,但我想再加一点东西。几年前,我偶然创建了这样一个聊天工具,它应该与WhatsApp相似。我相信当你谷歌搜索时,你会发现XMPP可扩展消息和状态协议。我们使用openfire作为维护连接的服务器。你解释的概念是
Say Friend 1 and Friend 2 are online . Friend 1 has established HTTP web connection to web server 1 and Friend 2 has established HTTP web connection to web server 2. Friend 1 send the message to Friend 2.

被称为联邦,openfire可以在联邦模式下运行。阅读您的评论后,我发现了每个用户一个队列的观点。我相信您已经知道这种方法不可扩展,因为它需要很多资源。一个好的方法是使用Actor框架,比如akka。每个actor就像Java中的轻量级线程,每个actor都有一个收件箱。所以在这种情况下消息传递是被照顾到的。
因此,您的场景转换为Friend 1打开与openfire xmpp服务器的连接并初始化Friend 1 Actor。当他输入一条消息时,它会传输到Friend 1 actor的收件箱中(akka中的每个actor都有一个内存收件箱)。这将被通知给xmpp服务器。服务器有自己的数据库,并且由于它与其他xmpp服务器联合,它将尝试查找friend 2是否在线。xmpp服务器将在其数据库中保留消息,直到friend 2上线。一旦friend 2与任何一个xmpp服务器建立连接,就会创建一个friend 2 actor,并将其存在传播到所有其他服务器和xmpp服务器1将通知friend 2的actor。friend 2的actor收件箱现在将收到消息。

可选:还有一种送达回执的选项。一旦Friend2阅读了消息,可以向朋友1发送送达回执,以指示消息的状态,即已读、未读、已送达、未送达等。


正如你所说,“每个演员就像Java中的轻量级线程,每个演员都有一个收件箱。”我认为为每个演员设置收件箱有点类似于为每个用户设置专用队列。两者都是内存模型。与队列一样,用于保存消息的收件箱也会消耗内存。你看到了什么主要区别吗?因此,我认为你建议的模型与消息队列模型更或多或少相同,只是实现上有些不同。 - user3198603
  1. 在队列模型中,当发送方传递消息时,系统将搜索接收方队列的位置(从用户上线时创建的一些元数据中)并进行传递。我相信会有现成的框架可以提供这个功能。我相信你所说的Akka在联邦模式下运行,提供了类似的功能。
- user3198603
你在两个方面都是正确的。邮箱就像是在ConcurrentLinkedQueue之上实现的一个包装器。而且邮箱既可以是每个演员私有的,也可以在演员之间共享,这听起来很类似于联邦。 - Raveesh Sharma
@Raveesh,你在谈论使用像ConcurrentLinkedQueue这样的数据结构将消息保留在服务器中。你不认为这是一个坏主意吗?因为如果服务器在尝试传递消息之前就崩溃了,那么消息就会丢失。难道它不应该是一个分布式队列而不是服务器内部的队列吗? - Pratz

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