
图片来源:Mindandi- www.freepik.com
最近我意识到,虽然我个人理解Centrifugo的用处,并且认为它非常棒,但我无法快速解释它的好处,以及为什么有人会想在生产应用中使用它的原因。真的--用Go或NodeJS服务持久性连接(Websocket、EventSource等)如今已经很简单了,尤其是在尝试用Python、PHP、Ruby或类似语言做同样的事情而没有内置的并发性之后,真的是一股清新的空气。
对于一个非常简单的用例,任何手工制作的或许多可用的开源解决方案之一都可以很好地工作。但是,一旦你的目标是一个关键业务的生产应用,你就需要开始考虑诸如可扩展性、适当的连接管理、重新连接时的消息恢复、可观察性等问题。许多问题的实现并非易事,许多问题需要合理的时间来完成。而其中大部分都是由Centrifugo服务器有效解决的。在这篇文章中,我们将介绍一些实时信息应用开发中的实际问题,并说明为什么你可以考虑在你的项目中使用Centrifugo这样的服务器。
Centrifugo适用于所有后端
这里的第一个卖点是Centrifugo可以和任何后端一起工作。当后端是用没有内置并发功能的语言或框架编写时,这一点尤其有价值。
在我之前的一篇文章中,我描述了Centrifugo背后的最初动机--它最初是为了与Django后端一起工作。Django是基于worker/thread模型的,用它来服务许多并发的持久连接会很快导致可用的worker耗尽。Centrifugo是一个独立的服务,可以轻松处理成千上万的持久性连接,并提供一个API来与连接的用户进行沟通。
在Mail.Ru,我第一次将Centrifugo与Django应用集成,我们开发了很多有趣的东西,但没有走到滑坡的地步--比如使用Gevent或Django Channels。我们开发了聊天、实时计数器、实时游戏、情人节仪表板和其他许多很酷的东西。所有这些都没有进行巨大的重构,也没有从Django转移到异步栈。
与Django一起工作意味着可以与任何后端技术一起工作。你可以继续使用你最喜欢的框架--无论是Django、Yii、Laravel还是Rails--并且仍然为你的应用程序添加实时事件。不需要黑掉你的后端和改变项目理念。只要你了解基本的网络和安全概念,整合过程就很简单明了。
事实上,Centrifugo作为独立的服务工作,并提供API与连接进行通信,这意味着它可以成为一个通用的元素,你可以从一个项目带到另一个项目,即使你是用新的语言开始你的新项目。
这里还有一个值得关注的问题是用户认证。通常情况下,你会在后端使用某种会话机制来验证WebSocket或HTTP连接。Centrifugo不知道你的应用认证策略,因此提供了一种通过JWT认证连接的方法。JWT必须在应用程序的后端创建并传递给客户端。Centrifugo还支持JWT过期和自动刷新钩。

还有一个正在进行的拉动请求,即在每次连接尝试时,通过HTTP代理认证到应用后端。在大多数情况下,这意味着你必须将Centrifugo和你的主程序运行在同一个域,以便在连接时将cookie传递给Centrifugo(至少在浏览器上)。虽然代理认证到后端可以满足一些使用情况,但JWT仍有其优势--特别是在大规模的重新连接工作流程中。有了JWT连接,只要令牌没有过期,就可以重新连接到Centrifugo,而不需要你的主程序参与。否则,你必须建立一个能够提供大量请求的应用程序,几乎在同一时间重新连接。对于许多野外部署,这可能是一个真正的灾难。
可扩展性
另一件重要的事情是可扩展性。随着你的应用程序的增长--越来越多的用户将与你的实时终端建立持久的连接。一个现代的服务器机器可以处理成千上万的开放连接,但一个进程的力量是有限的--你最终会耗尽可用的CPU或内存。所以在某些时候,你可能不得不在几台机器上扩展用户连接。在几台机器上扩展连接的另一个原因是高可用性(当一个服务器失灵时)。
Github上有很多实时消息解决方案,也有付费的在线服务。但其中只有少数提供开箱即用的可扩展性--它们中的大多数只在一个进程中工作。我并不想说Centrifugo是唯一可以扩展的服务器。还有许多替代方案,如Socket.IO、SocketCluster、Pushpin和其他许多方案。我的观点是,在寻找实时解决方案或从头开始建立它时,扩展的可能性是你应该考虑的主要问题之一。你真的无法预测你的应用程序在单台机器上的可用资源会以多快的速度耗尽--软件的可扩展性不是一个过早的优化,在大多数情况下,拥有可扩展的解决方案,只是给你更多的空间来改善应用程序的功能。
许多在线服务也有能力进行扩展。但看看定价--这些解决方案中的大多数都相当昂贵。在pusher.com的情况下,你一个月要付500美元,但最多只能得到10k个连接,而且你应该关心的每月信息量非常有限。这是很荒谬的。当然,Centrifugo是自我托管的,你必须花费你的服务器的容量来运行它。但我想在很多情况下,成本是无法比较的。
Centrifugo与Redis PUB/SUB的扩展性很好,支持应用端Redis的一致分片,并与Redis Sentinel集成,实现高可用性。我们用Centrifugo提供了多达50万个连接,在Kubernetes中有10个用于连接的Centrifugo节点pod,而只有一个Redis实例,只消耗了单处理器核心的60%!
还有一个正在进行的拉动请求,增加了使用Nats服务器作为代理扩展PUB/SUB的可能性。
结构良好的Protobuf协议
Centrifugo并不是一个年轻的项目。开发始于7年前,在2018年底,服务器随着v2版本的发布进行了大规模重构。随着Centrifugo v2的发布,我们现在已经将客户端-服务器协议定义为Protobuf模式。
虽然Centrifugo仍然可以通过电线传输JSON数据,但它现在也支持Protobuf消息格式的二进制Websocket连接。这意味着客户端和服务器之间的数据可以被紧凑和非常有效地序列化。
该协议被设计成只通过电线发送所需的数据,省略不必要的空域。它也可以简单地以向后兼容的方式扩展,以增加新的功能。我们将很快讨论Centrifugo提供的一些协议功能。
浏览器的Websocket多边填充
虽然现在Websocket大部分都能正常工作,但在有些情况下,用户无法建立WebSocket连接(即使使用TLS)。这可能是由于浏览器不支持、企业代理或浏览器扩展造成的。在某些类型的应用程序中,这是可以接受的,但如果你有一个要求,即每个用户可以从任何地方连接到你的应用程序呢?在这种情况下,你真的需要一个后备选项。
Centrifugo在SockJS上提供了后备选项--这是一个非常成熟和流行的polyfill库,有几种基于HTTP的传输方式,如Eventsource、XHR-streaming、XHR-polling等。通过原始WebSocket和SockJS,几乎每个用户都能连接并接收实时更新。
我们曾遇到过这样的情况:Websocket与我们服务器的连接被流行的广告拦截器浏览器扩展所阻止。而我们的用户都没有受到这一事实的影响--他们有效地转而使用XHR流。另一个故事是关于我开发的实时企业智力游戏,每个玩家(公司员工)都有自己的设备--智能手机、平板电脑、笔记本电脑--而游戏在任何地方都能运行。这对游戏组织者来说是一个很大的帮助--不需要花时间解决连接问题。
构建实时应用程序的有用基元
实时应用有其自身的特点。为了抽象出与单个连接的通信,Centrifugo提供了一个通道机制。每个连接可以订阅一个或多个通道,后台和客户端之间的通信可以通过向通道发布数据来完成。所以实际上这只是一个PUB/SUB的机制。在后端,Centrifugo提供HTTP和GRPC API来发布数据到通道。
有几种渠道有不同的安全级别和一些选项,可以在配置中定义。例如,所谓的私有通道,每一个订阅尝试都必须额外签名。
在频道内,开发者有一些有用的功能,如历史缓存、存在信息(关于活跃的频道订阅者的信息),以及当客户订阅频道(或取消订阅)时的加入和离开通知。这对建立游戏大厅来说是非常重要的,比如说。
还有一个在某些情况下必须解决的问题是如何恢复客户端在重新连接时错过的信息(临时网络连接丢失,实时后端重启)。这在重启的情况下特别有用,可以防止你的主数据库被成千上万的客户请求恢复状态而压垮。Centrifugo通过提供恢复功能解决了这个问题。每个通道可以选择保留一个出版物流,客户端可以在重新连接时恢复错过的信息,提供通道中最后看到的出版物的序列号。从而有效地恢复其状态,就像连接根本没有丢失一样。Centrifugo还解决了一个问题,即你需要在第一次订阅频道时将初始序列号传递给客户端(即使在EventSource的情况下,这也是很棘手的,因为协议中内置了最后的事件ID机制,但初始事件ID必须以某种方式传递给客户端)。
最重要的是,Centrifugo使用了高效的PUB/SUB机制(即目前的Redis),并将其与历史存储中的出版物流相结合(目前也是Redis)。在消息恢复的时候,Centrifugo会同步PUB/SUB和错过的出版物从存储中提取,结果客户会以正确的顺序收到出版物。这背后的算法在我之前的文章中有所描述。因此,在历史恢复功能下,你得到的不是最多一次的交付保证,而是至少一次的保证。
有一个正在进行的拉动请求,要求通过HTTP代理从客户端到应用程序后端的RPC调用。这样,开发者就可以选择利用一个开放的持久性连接来发送由客户端发起的任何命令到后端,并对其作出反应。因此,与HTTP相比,减少了客户端和服务器之间的信息量,因为HTTP的每个请求都包含所有的头信息,而且必须经过验证。
适当的持久性连接管理
连接需要适当的管理。在请求-回复的情况下,一切都很简单--你有包含所有必要头信息的请求,并且每次从客户端收到请求时都要对其进行认证。现在有了持久连接,你只需在建立连接时认证一次,然后连接就可以永远开放。如果用户在你的应用程序中被停用了,你需要关闭它的连接。Centrifugo提供了钩子来使这些会话失效,这样你的数据就会得到保护。
现代的实时应用程序可以在瞬间提供数千或数十万的在线连接。在实时后端重启的情况下,大量的用户连接将开始重新连接。同样--在请求-回复的世界里,当你的应用程序重启时,客户不会向你的应用程序发送任何额外的请求,因为只有当用户在应用程序页面上执行操作时才会启动请求。在持久连接的情况下,每一个连接在被中断后都必须重新建立。Centrifugo在处理高比率的重新连接和重新订阅频道方面是相当快的。客户端库在重新连接时使用指数退避算法,可以提供一些帮助。
性能表现
Centrifugo是相当快的。这不仅是因为它是用Go编程语言构建的,而且也是由于一些内部优化的结果。
根据我做的[基准测试](http://503,660 msgs/sec),Centrifugo在Protobuf协议下每秒可以广播70万条信息,在JSON协议下可以广播27万条。这些数字和任何基准测试一样,都是人为的,而且高度依赖于许多因素,但我认为这个顺序是相当好的。我相信,就性能而言,它应该适合周围的许多应用。
Centrifugo已经高度优化了与Redis的通信--通过有限的连接,使用流水线和智能批处理技术来减少网络RTT。
例如,通往客户端的信息可以自动合并在一起,以减少写系统调用量。协议的设计方式是,在JSON编码的情况下,使用JSON流格式,在Protobuf编码的情况下,使用长度限定的帧,可以有效地合并通往客户端连接的不同信息。
你可以在我最近的一篇文章中找到更多关于优化的内容- https://medium.com/@fzambia/building-real-time-messaging-server in-go-5661c0a45248
准备部署
为了简化部署过程,有Docker镜像、RPM和DEB包,用于流行的Linux发行版。事实上,由于Centrifugo是用Go编写的,开发者可以选择下载目标操作系统的预构建二进制文件,并以他想要的方式运行它。不需要安装额外的运行时间--只需要一个静态链接所有必要内容的二进制文件。
Centrifugo带有Prometheus格式的指标,并且可以选择将指标导出到Graphite中。也可以通过API提取指标,或者直接在管理界面上观察(管理界面也嵌入到服务器二进制中)。这意味着开发者可以监控Centrifugo节点的运行状态。
用于流行应用环境的客户端库
所以Centrifugo在原始连接的基础上提供了一些有用的基元,并有自己的协议来封装它们。这意味着要连接到Centrifugo需要特殊的客户端库。在我看来,这既是Centrifugo的优点,也是它最大的缺点。优点是所有上述的功能都是由客户端库封装的。你可以在客户端免费获得这些功能。缺点是,客户端库的实现和维护相当困难。虽然目前我们用以下官方客户端覆盖了大多数流行的应用环境。
- centrifuge-js用于浏览器、nodeJS 和 React Native
- 用于iOS应用的centrifuge-swift
- 用于Android和普通Java的centrifuge-java
- 用于 Dart 和 Flutter 生态系统的 centrifuge-dart
- 针对Go语言的centrifuge-go
- centrifuge-mobile基于gomobile项目,用于iOS和Android开发。
建立在Go语言的库之上
更有趣的是,Centrifugo是建立在围棋语言的Centrifuge库之上的。这意味着Go开发者可以额外调整实时消息服务器,提供自定义认证、通道授权规则、自定义PUB/SUB代理(而不是Redis)、自定义存在和历史存储。虽然Centrifugo服务器主要被设计成单向的消息流--从服务器到客户端--但库允许以完全双向的方式交换消息,并对每个客户端连接进行完全控制。
上面提到的所有客户端库都可以与使用Centrifuge库的服务器一起工作,因为底层协议基本上是相同的。
总结
正如你所看到的,在建立实时信息传递应用时,有许多事情必须考虑到。在这篇文章中,我的目标是解释Centrifugo存在的原因,并强调它是在考虑到生产使用的情况下开发的,解决了一些现实生活中的问题。
由于Centrifugo是一个由MIT授权的开源软件,你可以免费得到以上的一切。当然,免费的奶酪只是在捕鼠器中(正如我们在俄罗斯所说)。你仍然需要了解风险,适当考虑什么更适合你的使用情况。