如何建立可靠的实时聊天服务?

本文关键点

  • 尽管在实时系统设计方面取得了一定进展,但是实现像实时聊天这样的服务仍然需要做大量的工作。
  • 很少有数据库可以将数据大规模推送到客户端,因此实时应用程序常常不得不将推动更新和将更改持久化的职责分离到独立的子系统。
  • 如果基础服务提供对状态的一致视图,应用程序逻辑可以大大简化。提供高度一致的聊天API可以减少客户的集成工作量。
  • 协调两个或多个系统需要仔细的设计,以避免不一致和竞争条件渗入应用程序逻辑。
  • 让客户端负责协调可能会降低延迟并增加应用程序的开发成本。通常,将这种逻辑放在服务器端可以缓解这些问题。

实时聊天已经成为现代应用程序的一个常见特性。如今,不仅通讯器和社交网络允许用户通过互联网进行交流,聊天在医疗保健、电子商务、游戏和许多其他行业都至关重要。

由于这种转变,许多开发人员面临着为他们的应用程序实现一个聊天后端的任务。

作为平台负责人和Pusher的首席技术官,我和构建 Chatkit(我们的实时聊天服务)的团队一起走过了这段旅程。

从界面的角度来看,聊天似乎很简单,但是实现一个可靠的后端来支持它给软件设计带来了许多有趣的挑战。由于我们的规模化需求,我们的问题更加苛刻:成千上万的客户和数百万的终端用户依赖我们的产品,我们希望我们的聊天产品也能得到类似的应用。

现在有超过8000名开发人员使用我们的服务,我们很高兴能够说明是什么使系统能够处理大量聊天应用程序。许多其他用户场景也提出了类似的挑战,所以理解如何构建可靠且易于维护的实时应用程序对于任何软件工程师来说都是一项有价值的技能。

实时仍然是一个挑战

自2011年IETF将WebSockets 标准化以来,构建实时网络和移动应用程序的麻烦已经少了许多。超过90%的Web浏览器支持websocket,所以回退并不像七年前那么重要。加密也变得更便宜了,所以在TLS上管理WebSocket连接也更容易了,这大大提高了客户端连接性。

尽管客户端有了很大的改进,但在后端实现实时业务逻辑仍然需要软件工程师的大量经验。

现代实时系统通常需要高度分布式,这意味着开发人员需要在一致性和可用性之间进行权衡。大多数都是围绕着数据库在做权衡。

即便数据库管理系统如此丰富多样,找到一个支持实时更新的解决方案仍然会让软件工程师是不可能的事。关系数据库非常适合许多应用程序,但它们不能将数据推送给客户端——至少不能推给数百万客户端。非关系选项根本就没有很强的实时性。RethinkDB 是迄今为止我们得到的最接近于通用实时存储解决方案的那一款,但它很难获得支持,也无法维护。

托管实时存储服务确实存在,但它们常常需要在应用程序设计中做出牺牲。上了一定规模,它也会变得很昂贵,因为一般实时同步算法和数据结构都很难优化。

从技术上讲,可能仍然得使用不支持实时更新的数据库构建聊天系统。

最流行的不需要实时数据库的方法包括轮询(客户机或服务器端)和信号机制(客户机在需要提取数据时接收通知)。不幸的是,尽管轮询在概念上很简单,但上一定规模的话就很昂贵了,处理所有这些数据请求需要大量的CPU周期、内存和带宽。

重组,而不是改造

因为我们需要基于推送的聊天服务,而且没有现成的数据库可以将数据推送到客户端,所以我们需要为存储开发一个聪明的解决方案。

我们考虑过编写一个内部存储系统,该系统可以为我们的数千名客户可靠地处理数据,但我们很快意识到这项任务将是多么艰巨。花几个月的时间为服务做基础功能,这事听起来对任何人都没有吸引力。

我们做出的关键设计决定是将问题分成两个较小的任务:

  • 首先,我们需要找到一个系统,它可以以实时延迟保证将消息分发给最终用户。
  • 其次,我们需要填补持久性的空白,但是存储解决方案不会关心推送更新。

对于实时部分,选择对我们来说很容易,我们已经在生产环境中使用Redis多年,并取得了巨大的成功。Redis非常适合低延迟更新,但由于它的内存特性,不适合用于存储。

对于长期存储,我们选择了PostgreSQL,我们的工程师们非常了解的另一个数据库。PostgreSQL以其一致性保证而闻名,在我们的场景中,它将填补Redis留下的数据持久性缺口。

我们为Redis和Postgres设计的数据模型能够很好地分片,为将来横向扩展服务提供了空间。

一旦我们有信心能够独立地管理这两个系统,将它们集成到一个统一的聊天后端就成了我们的下一个挑战。

同步

将两个分布式系统组合在一起总是会产生棘手的同步问题。

因为我们的服务将通过网络与数据库通信,所以我们必须权衡CAP定理的影响,这意味着在网络分区期间,我们的系统将不得不降低一致性,或者变得对最终用户不可用。我们还考虑了PACELC定理,它是CAP的扩展,指出在没有网络分区的情况下,系统必须牺牲延迟或一致性。

为遵循我们的API设计理念,我们决定为了客户的便利进行优化。

减少一致性保证,需要使用我们聊天服务的开发人员付出更多的努力,因此我们在可用性(在网络分区的情况下)和延迟(在网络完全功能的情况下)之间选择一致性。尽管可用性和延迟受到了冲击,但我们相信我们的客户会喜欢编写更少的集成代码,而他们的用户不会注意到现实场景中的差异。

为了说明这种权衡,我将描述应用程序如何获取聊天室的消息:

每当终端用户在其应用程序中打开聊天窗口时,其聊天客户端需要获取历史消息并订阅新的消息。如前一节所示,在我们的设计中,这两个操作到达了两个独立的系统:客户机从PostgreSQL检索旧消息,并从Redis实时接收新消息。这种安排使得CAP和PACELC定理以三种方式表现出来:数据丢失、数据重复和顺序错误。

数据丢失的可能性来自于Redis发布-订阅系统的短暂性,因为客户只在建立订阅后才接收发布的消息。如果客户端在建立Redis订阅之前获取历史数据,它将无法判断是否遗漏了什么消息。

\"image\"

在获取旧消息之前订阅Redis解决了第一个问题,但留下了数据重复和顺序问题。

顺序问题很容易发现,客户端在建立Redis订阅后对Postgres执行历史查询,这意味着查询将在新消息在订阅中已经开始流转数小时甚至数天之后返回旧消息。这需要客户端缓冲实时消息,直到历史查询完成。

\"image\"

由于需要在短时间内执行多个操作,所以很难注意到重复的操作。

客户端建立Redis的订阅后,它将向Postgres发送历史消息查询。当该查询正在检索数据时,另一个用户可以向同一个聊天室发送消息。幸运的是,Postgres可以在为第一个客户机的历史查询完成获取数据之前保存该消息,并在查询结果中返回新消息。因此,第一个客户机可以接收两次消息,一次来自Redis,一次来自Postgres。

\"image\"

为了处理这个场景,Chatkit使用完全有序的消息标识符。当历史查询完成,Redis订阅所缓冲的消息列表与查询结果合并后,客户端通过检查消息标识符来消除重复。

上述逻辑取决于很多实现细节,但是如果写得正确,这个算法就能确保我们的聊天客户端提供可靠的更新流。

转到服务器

虽然技术上在web和移动客户机中实现上述逻辑是可行的,但我们决定将订阅逻辑推到服务器端。这个决定背后的一些原因,对于任何聊天实现来说都很常见,其中一些针对的是我们的特殊需要。

首先,我们必须在三个平台上支持Chatkit,分别是Web、iOS和Android。每个平台都有自己的怪癖,比如不同的网络api和并发基本类型。我们不仅需要把复杂的订阅逻辑重新实现三遍,还需要对每个环境进行调整,这使得我们的工程师维护这些库更具挑战性。

其次,客户端实现在延迟方面要糟糕得多,特别是在网络速度慢的情况下。它需要两个客户机-服务器的往返时间(一个是实时API,一个是存储API)和两个服务器端往返的时间(从实时API到Redis,一个是从存储API到Postgres)。将协调逻辑移动到服务器端可以让客户端在往返一次的时间内检索历史数据并订阅更新。在移动网络上,这可以节省好几秒应用程序加载时间。

第三,通过使用服务器API抽象这种逻辑,我们为许多优化留下了空间。在许多地方,缓冲区和缓存可以提高订阅和历史查询的延迟和效率。服务器端逻辑更容易控制和演进,尤其是在我们的案例中,因为我们不能强迫客户升级他们应用程序中的客户端库。

关于作者

\"image\"

Pawel Ledwon是一名软件工程师,拥有超过10年为快速成长的初创公司建立分布式系统的经验。他从复杂系统的理论中衍生出一套新的实践和思维模型来研究工程领导和管理。借助这些不同的方法,Paweł在过去的5年成功培养和带领了Pusher的平台团队。你可以在Hackernoon网站上,以及The Mission 和The Startup的文章中找到他对技术领导力的一些想法。

查看英文原文:Challenges of Building a Reliable Realtime Chat Service

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值