Centrifugo v2-Go实时消息传递服务器和库的未来

有些读者以前可能听说过Centrifugo 。 本文将重点介绍服务器第二个版本以及作为它基础的Go语言的新实时库的开发。


我叫亚历山大·埃梅林。 去年夏天,我加入了Avito团队,现在我在那里帮助开发Avito Messenger后端。 新工作与向用户快速传递消息直接相关,新同事启发了我继续从事开源Centrifugo项目。



简而言之-这是一台服务器,负责保持与应用程序用户的持续连接。 Websocket或SockJS polyfill用作传输;如果无法建立Websocket连接,则可以通过Eventsource,XHR流,长轮询和其他基于HTTP的传输来工作。 客户端订阅频道,后端通过离心API将发布的新消息发布到这些频道上-之后,这些消息将传递给订阅该频道的用户。 换句话说,它是一个PUB / SUB服务器。



当前,服务器用于相当多的项目中。 例如,其中包括一些Mail.Ru项目(内联网,Technopark / Technosphere培训平台,认证中心等),以及Centrifugo,这是一个美丽的仪表板,位于Badoo Moscow办公室的接待处,有35万用户同时连接到spot.im服务离心机。


对于那些首先了解该项目的人,一些指向服务器及其应用程序上的文章的链接:



我从去年12月开始研究第二版,一直持续到今天。 让我们看看会发生什么。 我写这篇文章不仅是为了以某种方式普及该项目,而且是在Centrifugo v2发行之前获得更多建设性的反馈-现在有回旋余地和向后不兼容的更改的空间。


Go的实时库


在Go社区中,这个问题不时出现-Go上的socket.io是否有替代方案? 有时,我注意到建议开发人员针对此做出的建议,将其转向Centrifugo。 但是,Centrifugo是一台自托管的服务器,而不是库–比较起来不公平。 我也多次被问到是否可以重用Centrifugo代码在Go中编写实时应用程序。 答案是:理论上可行,但我不能保证内部软件包的API向后兼容,后果自负。 显然,没有任何人有理由冒险,并且分叉也是一种选择。 另外,我不会说内部软件包的API通常已准备好用于此类用途。


因此,在使用第二版服务器的过程中,我要解决的一项雄心勃勃的任务是尝试将服务器核心分离到Go上的单独库中。 考虑到离心机具有多少功能才能适应生产,我相信这是有道理的。 开箱即用的许多功能可帮助构建可扩展的实时应用程序,而无需开发人员编写自己的解决方案。 我之前写过关于这些功能的文章,下面还将概述其中的一些功能。


我将尝试为这种图书馆的存在再加上一个理由。 大多数Centrifugo用户是使用并发支持差的语言/框架编写后端的开发人员(例如Django / Flask / Laravel / ...):如果可能的话,以不明显或低效的方式使用大量持久连接。 因此,并不是所有的用户都可以帮助开发用Go编写的服务器(由于缺乏语言知识,所以很讨厌)。 因此,即使是在该库周围的极少数Go开发人员社区也都可以使用它来帮助开发Centrifugo服务器。


结果是一个离心机库。 这仍然是WIP,但是绝对可以在Github的描述中说明并实现所有功能。 由于该库提供了相当丰富的API,因此在保证向后兼容性之前,我想听听几个在Go上实际项目中成功使用的示例。 还没有 以及不成功的:)。 没有。


我知道通过以与服务器几乎相同的方式命名库,我将永远处理混乱。 但是我认为这是正确的选择,因为客户端(例如centrifuge-js,centrifuge-go)可同时与Centrifuge库和Centrifugo服务器一起使用。 另外,这个名称已经在用户的脑海中牢牢扎根,我也不想失去这些联系。 但是,为了更加清楚,我将再次澄清:


  • 离心机-Go语言库,
  • Centrifugo是一个交钥匙解决方案,是一项单独的服务,在第2版中,它将在Centrifuge库上构建。

由于其设计,Centrifugo(一项对后端一无所知的独立服务)假定通过实时传输的消息流将从服务器流向客户端。 什么意思 例如,如果用户在聊天中写了一条消息,则必须首先将此消息发送到应用程序后端(例如,浏览器中的AJAX),在后端进行验证,必要时保存到数据库中,然后发送到Centrifuge API。 该库消除了此限制,使您可以组织服务器与客户端之间的异步消息双向交换以及RPC调用。



让我们看一个简单的示例:我们使用Centrifuge库在Go上实现一个小型服务器。 服务器将通过Websocket从浏览器客户端接收消息,客户端将具有一个文本字段,您可以在其中驱动消息,然后按Enter键,然后消息将发送给所有订阅该频道的用户。 即,聊天的最简化版本。 在我看来,将其以要点的形式放置将是最方便的。


您可以照常运行:


git clone https://gist.github.com/2f1a38ae2dcb21e2c5937328253c29bf.git cd 2f1a38ae2dcb21e2c5937328253c29bf go get -u github.com/centrifugal/centrifuge go run main.go 

然后转到http:// localhost:8000 ,打开几个浏览器选项卡。


如您所见,应用程序的业务逻辑的入口点是挂On().Connect()回调函数上的:


 node.On().Connect(func(ctx context.Context, client *centrifuge.Client, e centrifuge.ConnectEvent) centrifuge.ConnectReply { client.On().Disconnect(func(e centrifuge.DisconnectEvent) centrifuge.DisconnectReply { log.Printf("client disconnected") return centrifuge.DisconnectReply{} }) log.Printf("client connected via %s", client.Transport().Name()) return centrifuge.ConnectReply{} }) 

在我看来,基于回调的方法与库进行交互最方便。 另外, 在Go上的socket-io服务器的实现中使用了一种类似的,只有弱类型的方法。 如果突然之间您对如何惯用API有所想法,我将很高兴听到。


这是一个非常简单的示例,没有演示库的所有功能。 有人可能会注意到,出于这种目的,使用库来使用Websocket更容易。 例如,Gorilla Websocket。 实际上是这样。 但是,即使在这种情况下,您也必须从Gorilla Websocket存储库中的示例中复制一段不错的服务器代码。 如果:


  • 您需要将应用程序扩展到多台计算机,
  • 或者您不需要一个公共频道,而是多个频道,并且用户在浏览应用程序时可以动态订阅和取消订阅,
  • 或者您需要在无法建立Websocket连接时进行工作(客户端浏览器中不存在支持,浏览器扩展,客户端与服务器之间的某种代理切断了连接),
  • 或者您需要在Internet连接短暂中断期间恢复客户端丢失的消息而无需加载主数据库,
  • 或者您需要控制频道中的用户授权,
  • 或您需要断开与该应用程序中停用的用户的永久连接,
  • 或您需要有关当前谁在频道上或某人已订阅/取消订阅该频道的事件的信息,
  • 还是需要指标和监控?

Centrifuge库可以为您提供帮助-实际上,它继承了Centrifugo以前可用的所有基本功能。 可以在Github上找到更多显示上述观点的示例。


Centrifugo的强大遗产可以说是一个缺点,因为该库采用了所有服务器机制,这些机制都是非常原始的,也许对于某些人来说似乎不太明显或带有不必要的功能。 我试图以不使用功能不会影响整体性能的方式来组织代码。


库中进行了一些优化,可以更有效地利用资源。 这是将多个消息组合到一个Websocket框架中,以保存在Write系统调用中,或者例如使用Gogoprotobuf序列化Protobuf消息和其他消息。 说到Protobuf。


二进制协议


我真的希望Centrifugo处理二进制数据( 而不仅仅是我 ),所以在新版本中,除了现有的基于JSON协议之外,我还想添加一种二进制协议。 现在,整个协议被描述为Protobuf方案 。 这使我们可以使其结构化,重新考虑第一版协议中的一些不明显的决定。


我认为您无需长时间讲出Protobuf相对于JSON的优点-紧凑性,序列化速度,严格的方案。 存在难以辨认形式的缺点,但是现在用户有机会决定在特定情况下对他们来说更重要的事情。


通常,使用Protobuf而不是JSON时,由Centrifugo协议生成的流量应减少约2倍(不包括应用程序数据)。 与JSON相比,我的综合负载测试中的CPU消耗减少了约2倍。 实际上,这些数字几乎没有说明什么将取决于特定应用程序的负载配置文件。


为了引起兴趣,我在配备Debian 9.4和32个Intel®Xeon®Platinum 8168 CPU @ 2.70GHz vCPU基准的计算机上启动了该计算机,这使我们能够比较使用JSON协议和Protobuf协议时客户端-服务器交互的带宽。 1个频道有1000位订阅者。 在此通道中,消息以4个流发布,并传递给所有订户。 每个消息的大小为128个字节。


JSON的结果:


 $ go run main.go -s ws://localhost:8000/connection/websocket -n 1000 -ns 1000 -np 4 channel Starting benchmark [msgs=1000, msgsize=128, pubs=4, subs=1000] Centrifuge Pub/Sub stats: 265,900 msgs/sec ~ 32.46 MB/sec Pub stats: 278 msgs/sec ~ 34.85 KB/sec [1] 73 msgs/sec ~ 9.22 KB/sec (250 msgs) [2] 71 msgs/sec ~ 9.00 KB/sec (250 msgs) [3] 71 msgs/sec ~ 8.90 KB/sec (250 msgs) [4] 69 msgs/sec ~ 8.71 KB/sec (250 msgs) min 69 | avg 71 | max 73 | stddev 1 msgs Sub stats: 265,635 msgs/sec ~ 32.43 MB/sec [1] 273 msgs/sec ~ 34.16 KB/sec (1000 msgs) ... [1000] 277 msgs/sec ~ 34.67 KB/sec (1000 msgs) min 265 | avg 275 | max 278 | stddev 2 msgs 

Protobuf案的结果:


 $ go run main.go -s ws://localhost:8000/connection/websocket?format=protobuf -n 100000 -ns 1000 -np 4 channel Starting benchmark [msgs=100000, msgsize=128, pubs=4, subs=1000] Centrifuge Pub/Sub stats: 681,212 msgs/sec ~ 83.16 MB/sec Pub stats: 685 msgs/sec ~ 85.69 KB/sec [1] 172 msgs/sec ~ 21.57 KB/sec (25000 msgs) [2] 171 msgs/sec ~ 21.47 KB/sec (25000 msgs) [3] 171 msgs/sec ~ 21.42 KB/sec (25000 msgs) [4] 171 msgs/sec ~ 21.42 KB/sec (25000 msgs) min 171 | avg 171 | max 172 | stddev 0 msgs Sub stats: 680,531 msgs/sec ~ 83.07 MB/sec [1] 681 msgs/sec ~ 85.14 KB/sec (100000 msgs) ... [1000] 681 msgs/sec ~ 85.13 KB/sec (100000 msgs) min 680 | avg 680 | max 685 | stddev 1 msgs 

您可能会注意到,对于Protobuf,这种安装的吞吐量要高出2倍以上。 客户脚本可在此处找到-这是适应于Centrifuge现实Nats基准脚本


还值得注意的是,可以使用与gogoprotobuf中相同的方法来“提高”服务器上JSON序列化的性能-缓冲池和代码生成-当前,JSON是通过反射建立的Go标准库中的程序包序列化的。 例如,在Centrifugo中,使用提供缓冲池对JSON的第一个版本进行了手动序列化。 作为第二版的一部分,将来可以做类似的事情。


值得强调的是,当通过浏览器与服务器通信时,也可以使用protobuf。 javascript客户端为此使用protobuf.js库。 由于protobufjs库非常繁重,并且使用webpack及其树摇算法,二进制格式的用户数量将很小,因此我们生成了两个版本的客户端-一个仅支持JSON协议,另一个支持JSON和protobuf。 对于其他资源没有发挥关键作用的环境,客户不必担心这种分离。


JSON Web令牌(JWT)


使用像Centrifugo这样的独立服务器的问题之一是,它不了解有关用户及其身份验证方法以及后端使用哪种会话机制的任何信息。 并且您需要以某种方式对连接进行身份验证。


为此,在第一版离心机中,在连接时,基于仅后端和离心机已知的密钥使用SHA-256 HMAC签名。 这样可以确保客户端发送的用户ID确实属于他。


连接参数的正确传递和令牌的生成也许是将Centrifugo集成到项目中的主要困难之一。


当离心机出现时, JWT标准尚未普及。 现在,几年后,可用于大多数流行语言的 JWT生成库。 JWT的主要思想正是离心机所需要的:确认传输数据的真实性。 在HMAC的第二个版本中,手动生成的签名让位于JWT的使用。 这样就可以消除对支持功能的支持,以在不同语言的库中正确生成令牌。


例如,在Python中,可以如下生成用于连接到Centrifugo的令牌:


 import jwt import time token = jwt.encode({"user": "42", "exp": int(time.time()) + 10*60}, "secret").decode() print(token) 

重要的是要注意,如果您使用Centrifuge库,则可以使用本地Go方法(在中间件内部)对用户进行身份验证。 示例在存储库中。


GRPC


在开发过程中,我尝试了GRPC双向流传输作为客户端和服务器之间通信的传输(除了Websocket和基于HTTP的SockJS后备)。 我能说什么 他工作了。 但是,我没有找到一种双向GRPC流传输比Websocket更好的方案。 我主要研究服务器指标:通过网络接口生成的流量,具有大量传入连接的服务器的CPU消耗,每个连接的内存消耗。


GRPC在所有方面都输给了Websocket:


  • 在类似的情况下,GRPC会产生20%的流量增长,
  • GRPC会多消耗2-3倍的CPU(取决于连接的配置-所有订阅到不同的通道,或者全部订阅一个通道),
  • 每个连接GRPC消耗4倍的RAM。 例如,在10k连接上,Websocket服务器吃了500Mb的内存,而GRPC-2Gb。

结果是相当……预期的。 总的来说,在GRPC中,作为客户端传输,我没有多大意义-直到可能更好的时候,才清楚地删除代码。


但是,GRPC擅长于其最初创建的目的-生成允许您使用预定方案在服务之间进行RPC调用的代码。 因此,除了HTTP API之外,离心机现在还将具有基于GRPC的API支持,例如,用于将新消息发布到通道和其他可用的服务器API方法。


与客户的困难


在第二个版本中进行的更改中,我删除了对服务器API的强制性支持-变得更易于在服务器端进行集成,但是,项目中的客户端协议已更改,并且具有足够的功能。 这使客户的实施非常困难。 对于第二个版本,我们现在有一个Javascript客户端 ,可在浏览器中使用,应与NodeJS和React-Native一起使用。 Go上有一个客户端,它是基于该客户端以及基于iOS和Android 的gomobile项目绑定程序 构建的


为了获得完全的幸福,没有足够的iOS和Android本机库。 对于第一个Centrifugo版本,它们是由开源社区的人购买的。 我想相信现在会发生这样的事情。


最近,我从Mozilla发送了一份申请MOSS资助的申请,以期在客户开发方面进行投资,但最终还是被我拒绝了。 原因是Github上的社区活动不足。 不幸的是,这是事实,但是正如您所看到的,我正在采取一些措施来改善这种情况。



结论


我没有宣布Centrifugo v2中将出现的所有功能-Github上问题中有更多信息。 服务器尚未发布,但很快就会发布。 仍然有未完成的时刻,包括需要完成文档。 文档的原型可以在这里查看。 如果您是Centrifugo用户,那么现在是影响第二版本服务器的合适时机。 打破某件事并不那么害怕,以后再做的时候。 对于那些感兴趣的人:开发集中在c2分支


对于我来说,很难判断Centrifugo v2底层的Centrifuge库的需求量。 目前,我很高兴能够将其恢复到当前状态。 对我而言,最重要的指标是对以下问题的答案:“我自己会在个人项目中使用该库吗?” 我的回答是。 在上班吗 是的 因此,我相信其他开发人员也会对此表示赞赏。


PS:我要感谢那些提供工作和建议的人-Dmitry Korolkov,Artemy Ryabinkov,Oleg Kuzmin。 没有你,那会很紧张。

Source: https://habr.com/ru/post/zh-CN416915/


All Articles