RIOT消息服务 - Riot messaging service

本文原作者Michal Ptaszek(链接为原文通道),仅供学习用途

RIOT的消息服务

我们将RIOT消息服务Riot messaging service简写为RMS.

若以高层划分,服务之间的通信可以被分为两组:

  • 同步请求:发送者在发出请求后堵塞直到收到回复
  • 异步请求:发送之后不会堵塞

本文将着重讨论异步请求,此类型适用于后端服务异步通知客户端特定事件的场景 - 比如说状态切换。Riot的消息服务的设计就是为了解决不同场景,约束条件下的客户消息传递。

本文将从讨论状态变化和stateful服务的重要性入手,接着介绍Riot的消息传输服务的架构,这个架构具有可线性扩展和高容错率的特性。最后将讨论一个可支持1千万玩家连接的系统实例。

状态

在拳头公司里,微服务架构被应用的很广泛,微服务带来了特别多的好处,包括软件的分离,独立的部署,可扩展性提升,支持不同编程语言等等。

英雄联盟中每个微服务都负责他们对应的状态。我们把战队系统作为例子:战队是一个由玩家创建,由玩家组织,由玩家控制的群组。战队系统储存了成员信息,tag,今日公告,成员排名以及一些其他的信息。每次从客户端登录将从战队服务获得最新的状态然后显示出来。

然而当我们有了最初的状态后,我们将怎么样更新他呢?比如说,当战队管理员改变了今日公告时,其他成员的客户端是否应该主动从服务端获取数据然后检测是否由改变呢?或者只有当成员重新登录时才获取信息呢?还是说我们应该建立一个连接来实时更新?

RMS针对具体情况做出了选择,根据此问题的特性,合理的解决方法应该是: 每当管理员更新公告时,会通知战队服务,接着战队服务会通知RMS告诉每一个成员更新最近的战队状态。

RMS其实和手机上的推送系统类似,然后手机的推送系统有部分为同步请求,而RMS使用的是异步请求。

RMS高层架构

RMS的高层架构包括两个部分:

  • RMS Edge(RMSE)
  • RMS Routing(RMSR)

RMS_UnderTheHood

RMSE是一组负责玩家客户端连接的服务器。LOL客户端会通过一个load balancer使用加密的WebSocket与一个RMSE节点连接。该连接在通过身份验证后是永久存在的直到客户端退出。

在处理身份验证和负责客户端连接之上,每一个RMSE服务器也负责把消息传输给连接的客户端。每一次由新客户端连接进来或者登出时,RMSE节点将通知RMSR层。

由于RMSE服务器没有相互之间的交互,所以他们都是100%可线性扩展的。值得一提的是,就算是相邻的server,一台server如果down掉,不会对其他的服务器产生影响。

以下是RMSE层的架构:

RMS_EdgeTier

RMSR层是一个负责全局客户端状态的服务器集群。每个RMSR节点都有一个全局的分布式表,此表映射了从玩家到特定的RMSE节点的信息,以保持连接状态。RMSR层还负责处理来自其他服务的消息并发送消息至正确的RMSE节点。最后,RMSR会跟踪RMSE层节点的状态,在有客户端登出时触发清理的一个步骤。

RMSR的架构如下:

RMS_RoutingTier

消息发布

RMS消息传输使用的是JSON格式,以下是一个实例:

{
"resource": "clubs/v1/clubs/665632A9-EF44-41CB-BF03-01F2BA533FE7",
"service": "https://clubs.na.lol.riotgames.com",
"version": "2016-10-12 09:33:43.1245",
"recipients": ["3029B94E-412F-484C-B4E7-BD01073EA629"],
"payload": "{\"method\": \"GET\"}"
}

每条消息都是以resource和service作为标志符,他们都遵循RESTful的格式。

当RMSR接收到上述消息后将会对其进行解析,将recipients域移除后转发至对应的客户端连接的RMSE服务器,最后再转发至对应的客户端。

客户端将会接收到如下信息:

{
"resource": "clubs/v1/clubs/665632A9-EF44-41CB-BF03-01F2BA533FE7",
"service": "https://clubs.na.lol.riotgames.com",
"version": "2015-10-12 09:33:43.1245",
"timestamp": 1444677045952,
"payload": "{\"method\": \"GET\"}"
}

RESTful的消息传输使该过程变的简单并灵活,很轻松就可以添加或删除新的服务,这对大型游戏的开发是很有益的。

实际实现

拳头公司使用Erlang实现了整个RMS,Erlang的各种好处巴拉巴拉,总之就是写起来简单适用于游戏。

RMS被打包部署为Docker镜像,保证了更好的隔离性和可移植性,简化了部署和调试过程,以及与操作系统的分离。

整个服务部署在AWS上,使用云平台使得灵活性,可扩展性,错误检测,以及开发速度大大提高。所有的AWS资源由Terraform进行自动管理。

RMS本身是一个全局的服务,所以他的健壮性的优先级是很高的、

由于服务的CCU(Concurrently Connected Users - 并发连接用户数)随着活动客户端的数量增长而增长,因此我们希望为单个RMSE和RMSR节点建立一个可知容量,然后根据某种公式,以便在需要时扩展集群。

负载测试

负载测试的基本思路是模拟真实客户端连接,使消息跑完整个系统。

负载测试工具结构图:

RMS_LoadTestTool

直接上测试结果:

由于TCP端口数的限制,每台机器可以处理约60k的RMS连接,我们可以通过增加toolbox的数量通。我们发现一个AWS的r3.8xlarge EC2 instance可以处理10M的连接。这远远超过了预期。但我们决定使每一个服务器承载的连接小一点,以防止实例failure后所有的连接同时丢失。

差错容忍控制

使用了Jepsen工具模拟网络的不稳定以及各种差错的测试,本文未对差错容忍的实现进行讨论。(猜测:master/slave arch; sharding)

0 DOWNTIME DEPLOYMENTS

由于每个RMSE节点都与客户端有WebSocket连接,我们不能简单的使用blue green deployment并随意关掉需要随意更新的节点。对了解决这一问题,我们对RMS加入了会话持久性的功能。只要RMSE检测到客户端连接意外断开连接,或RMSR检测到RMSE节点丢失时,受影响的会话就会进入“持久”模式。发送给受影响播放器的消息被缓存在内存中,并在成功重新连接到系统时传送给客户端。 如果客户端在接下来的几分钟内没有重新连接或系统接近耗尽内存,则会清除持久会话及其排队的消息。(说的仅仅是RMS)

客户端的消息处理可能会因此有延迟,但是可能被重连的时间所掩盖。若不能及时重连,客户端将重新同步所有服务的状态,因为这可能是由于RMS未连接时错过了某些重要的状态更新。

对于RMSR,我们可以启动一个辅助群集,将其状态与当前正在运行的状态同步(并处理production load?),并将所有通信路由到更新后的服务器。

RMSE和RMSR层可以彼此独立部署,不受面对玩家的影响,使我们能够引入新功能并修复错误,而无需昂贵的维护时间。