以消息队列为中心的服务端架构
Posted 说给开发游戏的你
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了以消息队列为中心的服务端架构相关的知识,希望对你有一定的参考价值。
小说君在中,简单介绍了经过服务化改造后的服务端拓扑结构。今天就接着这个话题,不再聊形而上的东西,讨论下如何用一个具体的消息队列中间件实现服务端的服务化。
先做个简单的前情提要,贴一下上篇文章最后产出的架构图:
图中的「Harbor」既可以理解为skynet框架中的harbor组件,也可以理解为一个消息队列中间件。
在谈以消息队列为中心的设计之前,我们首先来聊聊什么是消息队列。
最简单的理解,消息队列就是一个类似「队列」的数据结构,通常用于线程间通信——线程A向队列中丢一个消息,线程B从队列中拿一个消息。
「丢」和「拿」,通常对应push和pop,两者的逻辑都会有一部分临界区,具体实现时可以加锁,也可以借助一些比较巧妙的数据结构实现lock-free。
Erlang/OTP中每个actor有一个mailbox的概念,其实就是个消息队列。而skynet实际上就是云风用来替代Erlang的C实现,因此本质也是消息队列。
每个skynet进程维护一个全局的消息队列,同时又为每个寄宿于进程内的skynet服务维护一个消息队列。
当然,消息队列本身并不是一个高深的概念。围绕消息队列这个简单的数据结构构建出的解决问题模型,才是关键。
做过服务端的同学一定听说过ZeroMQ,这也是一个消息队列。不过跟业界成熟的RabbitMQ,以及不太像MQ的Kafka相比,ZeroMQ并不是一个中间件,而是一个Lib,或者说,ZeroMQ是用来开发消息队列中间件的基础库。
但是,ZeroMQ出名的一般不是因为这个产品本身,而是因为其作者写的一本书,《ZGuide》。这本书并没怎么讲ZeroMQ怎么用,重点讲的是ZeroMQ的设计思路,讲业务在服务端都有哪几类典型的抽象,不同抽象需要怎样的模型去解决。小说君个人认为,每位有志手写服务端框架的同学都要从头到尾阅读一下《ZGuide》。
在书的开始,作者一句话就提领了分布式服务端框架的核心思想:定义框架中的动态(dynamic parts)与静态(static parts)。
这样说可能有点抽象,展开一下,小说君认为要从三个层次来理解这两个概念:
第一层含义,描述的是连接关系。理解起来也很简单,动态的总是去主动连接静态的。
第二层含义,描述的是依赖关系。整个服务端启动的过程,总是静态先于动态初始化完毕。
第三层含义,描述的是可用性保证。根据分布式实践级别的不同,静态对外提供的信息可以是最原始的ip:port,可以是host name,也可以是service name。动态访问静态,借助的就是这些对外提供的信息。在这些信息背后,静态的物理结构对动态是完全透明的,可以按需提供不同层次的可用性保证。
动态与静态是相对的,比如说,场景服务(动态)与场景管理服务(静态),场景管理服务(动态)与消息队列(静态),消息队列(动态)与权限管理中心(静态),等等。
skynet与ZeroMQ都是消息队列,而且对消息队列这个概念的应用程度也很类似,那就是拿消息队列来解决问题。
但是,我们今天要讨论的是用一个现成的消息队列中间件来实现服务端框架,所以既不能用skynet这种完善的服务端框架,也不能用ZeroMQ这种的消息队列库。
思考一下,我们在做消息队列中间件选型的时候,一般会考虑哪些因素?
第一点是对协议的支持程度。
协议定义了消息相关API的语义,对协议的支持程度实际上是中间件的feature set。比如最常用的qos1要求消息至少送达一次,那中间件就要支持ack,支持重连等等。那要想支持qos2,中间件就要支持消息持久化,支持一定程度的replication。
搞过RabbitMQ的同学应该都了解AMQP这个协议,协议本身定义了多个层次(Transport、Session、Model),多个概念(Broker、Host、Exchange、Queue、Binding),学习成本极高。
还有一种叫MQTT协议,没有繁多的概念,只涉及了几种消息队列的核心概念:仅有的pub-sub模型、基本的三级qos、基本的broker模型等等,简单易懂易用。
第二点是分布式与可用性支持。
不过现在流行的消息队列基本都是03年之后的产物,所以对分布式的支持都不成问题。
可用性可以说是这类MQ中间件与ZeroMQ或skynet这种的最大区别。我们之前说过,MQ相对于服务端的具体服务是静态,那么如果静态成为单点,相当于整个系统的稳定性都成了问题。MQ如果提供了灾备机制,动态部分就可以认为静态部分是安全可靠、持续提供服务的,然后再以这个为前提实现各自问题域内的灾备即可。
第三点是性能指标。
不同的消息队列中间件解决的问题不太一样,这块也没个统一的标准。只要根据自己的需求写好跑分脚本,选一个合适的就可以了。
最后一点就是社区支持。
这方面就不用说了。好的社区意味着有问题能及时发现,不只是服务端的部分,各种客户端库社区的活跃度也很重要,毕竟我们作为用户的话大部分时间其实是跟客户端库打交道。
小说君之前在某个框架的实践中,采用的是RabbitMQ。服务端连MQ的时候走的AMQP协议,RabbitMQ还可以挂一个MQTT的adapter,这样客户端连MQ的时候走的MQTT协议。生产环境中,两边用到的MQ实例还可以做个物理隔离。
RabbitMQ最初就是作为AMQP的一种开源实现进入人们视野的,因此其中概念跟AMQP中的并无二致。主要概念有三个,exchange(交换机)、queue、binding(绑定规则)。
如果以最典型的生产者消费者模型来类比的话,exchange对应生产者一侧,queue对应消费者一侧,binding则描述了消息从exchange到queue的路由关系。
exchange有两种常用类型direct、topic。其中direct exchange接收到的消息不会dup,而topic exchange则会将接收到的消息根据匹配到的binding决定要dup到哪个target queue上。
当然,理解到这种程度只是能拿来用,想深入的话还是需要研究复杂的AMQP协议的。RabbitMQ官网上也给了常用情景以及不同语言客户端的代码实现:
RabbitMQ的路由方式配置灵活又简单,举几个简单的例子。
假设有n个实例提供服务A,但是实例内并不维护每个session的状态,换言之,每次client发一个消息,实例都会去一个第三方的组件拿消息的session状态,然后做处理,修改状态,再返回。这种情况下,可以让n个实例消费一个同名queue,对client开放direct exchange,MQ就直接帮我们做了round-robin,而且可以根据每个实例的消息处理情况决定负载分布。
topic exchange的话由于会对消息做dup,所以应用上有些局限。实现一些通知类的通信pattern可以用上,比如可以借助topic exchange实现一些pub-sub pattern。
具体一些的话,假设服务B描述的是一种无条件接受对时,但是只有B1、B2类型的实例提供服务B,B3类型的实例并不提供服务B。一般的思路就是用传统的组播实现,但是用基于topic exchange的pub-sub我们可以做到提供服务B就订阅相关keyword,不提供服务B就不订阅,消息会dup但是又不会冗余。
其中服务A可能做web的同学都比较熟悉了,就是一种stateless service,具体话题不再展开,下篇文章再讨论。
说到这里,对游戏服务端有执念的同学可能还是会一头雾水,不知道消息队列中间件在游戏服务端中怎么用,或者说用了有什么意义。
先放一张的典型游戏服务端架构图:
图中的业务节点实际上只有场景服务一种,正如所说,如果业务节点类型多了,连接拓扑会变成网状:
类似的问题大家都会遇到,有志之士当然也都想解决这类问题。于是就出现了各种游戏圈特色的解决问题方式。
最常见的是让中心节点承担MQ的角色,由MQ来做节点间消息组播,支持最简形式的pub-sub pattern。
还有的是因为在Gate上实现了类似组播的功能,于是把这块后端间消息组播的逻辑也加到Gate上,让Gate变成了MQ和Gate的融合体。
但是这样做除了只是产出了一个just works的玩具,并没有太大意义。
自己在Gate或中心节点中做类似MQ的feature,相当于是在造一个MQ的轮子,而像RabbitMQ这种消息队列中间件所支持的feature,比如消息的ack机制和confirm机制、qos、各种pattern的支持、权限控制、集群、高可用、甚至是现成的图形化监控等等,远非业务开发者所能承担。
这其实也是游戏开发业界的一大问题,离C++太近,导致所有组件都想自己做,服务端框架想用自己的,网络库想用自己的,标准库也想用自己的。即使是大公司,复用级别也保持在copy-paste的程度,可悲。
更何况,中心节点这种自不必说,MQ与Gate解决的就完全不是一类问题。重写一个Gate还可以说是因为游戏场景同步数据量大、实时要求高,而MQ这种作为内部节点harbor的存在重写是完全没必要的。
MQ与Gate的定位类似,都是服务端中的static parts。Kafka的作者写过一篇文章叫《the log》,做过分布式的同学应该都研读过。小说君认为里面提出的一个概念很重要,基础设施抽象(infrastructure abstraction)。MQ与Gate是两种不同的基础设施抽象(Kafka自然也是不能跟其他MQ归为一类),提供的语义也不尽相同。
Gate要解决的问题是以最低的成本构建消息流模型,仅提供传输层所能提供的消息送到质量保证。client与Gate的连接断开,Gate没有义务再保留连接上下文。Gate其实是在游戏场景同步需求情景下诞生的特殊的MQ,具有一部分MQ的职责,比如发布订阅(客户端发布,服务端订阅,带组播的话还支持message dup),协议高度简化(对比AMQP协议的复杂度),没有一些MQ专有的capability(qos保证,消息持久化等)。
而MQ要解决的问题是提供普适的消息队列抽象。性能不敏感的服务可以依赖MQ构建,将自己的连接维护与会话保持两块状态寄存在MQ上。
现在,游戏服务端中就需要两种static parts——Gate与MQ。两者先于其他所有dynamic parts启动、构建。场景服务连接Gate与内网MQ(前提是确实有其他服务会与场景服务进行通信),聊天服务连接内网MQ,client连接Gate与外网MQ(或MQ代理)。
最后的拓扑结构就不再画了,大家脑补一下即可。
写完回头看下,一个不小心就又形而上了。
现在虽然是解决了一些问题,但是还是产生了新的问题。
又是MQTT,又是AMQP,再加上跟Gate的私有协议。如果用最传统的拼包形式进行服务调用的话,那对上层程序员来说简直是噩梦。莫以为这种项目在现实中并不存在,前段时间见识过的某棒子项目里面就是这样,数个第三方服务,不同形式的API,千奇百怪的逻辑层组包,让小说君大开眼界。
所以下篇文章我们聊聊RPC,用规范来解决这些问题。
个人订阅号:gamedev101「说给开发游戏的你」,聊聊服务端,聊聊游戏开发。
以上是关于以消息队列为中心的服务端架构的主要内容,如果未能解决你的问题,请参考以下文章