面向微服务的系统架构

一年前的这个时候,小说君在知乎上提了个问题,关于pomelo框架与网易内部部分开源的服务端框架mobile_server。

当然,后来的发展也在预料之中,回答者寥寥。不少回答的同学要么是匿名喷,要么是灌了个水之后删掉。一年过去,也并没有哪位既用过mobile_server,又了解过pomelo的同学参与回答,非常遗憾。

考虑到有些同学并不是做游戏服务端的,一是不了解游戏服务端开发中的一些术语,二是可能不太了解前述两个架构,所以这里就简单提一下。

这两个架构实际上主体部分都与之前的「面向中间件的开发模式」一文最后形成的架构大致相似,贴张图:

当然,除了节点命名的区别之外,两种架构还是与图中有所出入。

比如除了Gate之外,仍然有一个独立的前端服务器,供玩家登录以及负责为玩家分配要建立长连接的Gate服务器。

另外简单介绍下:

  • 图中的Gate是一种反向代理服务器,可以理解为web开发中的nginx。

  • 图中的中心进程和场景服务器都是应用服务器,可以理解为web开发中的tomcat节点。

  • 图中的DbProxy既有部分业务逻辑,又具有一定的缓存功能,不过完全不影响将之理解为web开发中的缓存服务器,比如redis。

mobile_server大体如此,而pomelo则是在这个基础上引入了更多web开发的思路。还是举一些例子:

  • 看pomelo的官网,我们就能发现,里面用的route、filter、channel,app应用框架的插件化形式加载模块,以及一些实现的较独立的monitor、watchdog、admin-tool等,都带着web开发的影子。

  • pomelo的配置比较统一,都是json。不像游戏服务端框架喜欢用ini或者xml。

  • 对分布式的支持。大部分游戏服务端框架虽然号称 「 分布式框架 」,其实对分布式的支持很有限,能支持个gate集群或者game集群已经算是相当不错了。但是服务发现、守护进程这些都不会有。

  • pomelo还借助zookeeper帮一些全局单点做了高可用,当然细节我没仔细看,可能首先还是要先把这些全局单点搞成无状态服务才行。

说了这么多,想必各位可能觉得小说君是在安利pomelo。当然不是,pomelo有一个致命的硬伤,就是这是js写的。且不说在大佬们眼中C#/JAVA写服务端就已经非主流了,纯js简直死刑,单说团队协作这块,js这种弱类型语言对于中等规模的开发团队简直是噩梦。这也是pomelo在网易内部都一直不温不火的原因。但是说来也怪,这些语言上的硬伤都并不能阻挡js成为程序员中的主流。

相比之下,mobile_server虽然是c++搭python,但是由于是bigworld这种老牌引擎改过来的,上层顾虑少,资源投入多,有成功产品用了大家就都会用。但是像一开始提到的那个知乎问题下面,有人答了又删的答案所说mobile_server就是屌、pomelo就是渣这种论断,就显得成败论英雄了。

扯了这么多八卦,下面我们还是言归正传,看看标题「面向微服务的服务端架构」。

十年前,有几个缩写比较火,一个是SOA(Service-Oriented Architecture,面向服务架构),一个是SaaS(Software as a Service,软件即服务)。

两个概念描述的其实是差不多的意思,那就是业务系统一定要服务化,才能有一个比较高的抽象层次,继而做到服务级别的复用。

现在两个概念已经没什么人提及了,一方面是因为SOA和SaaS还是企业应用这块吹的比较多的,另一方面是互联网技术日新月异,框架层出不穷,用一个新框架三分钟搭出一个业务,谁还关心服务如何复用?

但是,不可否认, 「服务化」一直都是一种比较现代化的解耦思路。因此SOA虽然已经淡出视野,但是后续的类似概念立刻跟上。

「微服务  」是这样一种概念,逻辑被划分为一个个的独立单元,每个单元的逻辑非常简单,高度内聚,单元之间是正交的。单元与单元是物理隔离的,相互之间借助异步机制通信。如此一来,每个单元就可以称为一个 「微服务 」。

继续拿出之前对比的pomelo与mobile_server,小说君之所以更欣赏pomelo,也是因为其在服务化上做的探索要比mobile_server多很多。

pomelo把游戏服务端中一些常见的逻辑集以  「 组件   」的形式呈现,一个pomelo节点可以选择加载多种不同的 「组件  」。服务器  本身是鸭子类型的,也就是说,只要某个服务器  加载了某些组件,那么这个服务器 就能提供这些服务,属于这些服务的一个实例。

但是这样还不够。

我们说微服务有一个特点,那就是 「物理隔离 」。如果仅仅是通过插件化来做服务加载,那是无法做到物理隔离的。

典型比如两个微服务都依赖了同一个lib,这个lib中有一个单例,这样两个微服务就都能访问到这个单例,违反了物理隔离的要求。

除此之外,还有一个致命问题,我们需要画个架构图才能看出问题所在。

假设我们有一套服务端架构,采用了  「 微服务  」 的设计理念。其中有两种微服务分别是A和B,各有4和3个服务实例。

A需要访问B提供的服务,B需要访问A提供的服务。那么简单的连接拓扑图如下所示:

相互之间需要的连接数是n*m。

当然,这还是比较简单的情况。在真实应用中,需要互调的微服务不可能只有A和B两种,更多的情况是A、B、C、D四者,甚至更多微服务之间互调,假设每类服务的实例都有复数个,那连接拓扑简直没法看。

那么,面向微服务的服务端架构,如何才能解决上述两个问题?

其实只要稍微换个思路,就能轻易解决。

首先看物理隔离。如果服务是一个基本单元,那我们如果想做到单元之间的物理隔离,需要用什么样的容器?

最简单的就是操作系统级别的,一个进程即是一个单元,而且是绝对物理隔离的。

这样实现的好处就是足够简单——框架简单,开发起来更简单。

但是问题也有,想实现一套统一的服务间通信机制就只能依赖进程间通信(同物理机),或网络(跨物理机)。

其次就是语言级别的,这方面也有比较成熟的方案,那就是Erlang的actor模型。一个actor就是一个单元,同样是物理隔离。

而且Erlang/OTP还提供了消息通信机制,语言层面做到这些,自然可以在底层做更多的优化,比如同物理机的actor间通信完全可以zero copy。

还有一种是虚拟机级别的,云风的skynet就是如此,lua_State具有物理隔离特性,因此skynet的每个服务单元的容器就是一个lua_State。

再看连接拓扑的问题。这个问题想解决就更简单了,假设所有的服务都与一个harbor建立连接,由harbor统一路由消息,拓扑就变成了下图的样子:

连接数量简化为了n+m。

这个harbor其实就是一个消息队列中间件,具体实现中,可以像skynet那样用一个高度简化的、数据结构层面上的消息队列来做,也可以用一些第三方的成熟消息队列中间件来做。

之后一篇服务端系列文章,小说君就会详细介绍下如何基于RabbitMQ这个消息队列中间件来实现这样一种服务端架构。

微服务与服务划分作为web开发中的一种比较现代化的理念,已经得到了充分的应用,但是游戏程序员往往对此不以为然。 文章最后,小说君想再聊聊对游戏服务端的意义。

游戏服务端实践中,针对之前提到的连接拓扑复杂的问题,一种治标不治本的方法是抬高添加新进程的成本,比如如非必要不会允许你增加额外进程,但是这样更深度的解耦合就成了幻想。

另一方面,游戏服务端的应用层与连接层难以分离。举个例子,在文章开头介绍的设计思路中,两个进程有没有连接是一种不确定态,设计的时候觉得没有,结果某个需求来了,不建立连接就很难实现。这样对于应用层来说,就需要提供连接的概念,某个进程必须先跟其他进程连接建立成功了,然后才能调用其他进程提供的服务。

而实际上,更优雅的设计是应用层完全不关注连接细节,只需要知道其他的进程提供了服务,自己就能获取到这种服务。

当然,游戏服务端开发中,也是会出现服务划分的需求的,最典型比如聊天服务。但是,公司内不同项目,往往直接用同一套聊天服务代码库,达到代码级别的复用。这样做的结果往往就是,每个团队都会从更早的团队拿过来聊天业务代码,然后自己改造改造,成了完全不同的分支,最后连代码复用都做不到了。

从另一个思路来讲,同一款游戏的不同组服务器,其实也只需要同样的一组聊天服务。但是如果按传统的模式,一组服务器只能开零或一个聊天服务器,事实上,有可能某10组服务器用1个聊天服务器就够了,而某1组服务器用1个聊天服务器压力都有些大。

但是很显然,如果做了服务划分的改造,比如聊天服务直接采用一些第三方的IM云服务,这些问题就都烟消云散了。

文章标签: 微服务 架构


关注微信公众号“架构说”,加入Q群微群,让架构师带你飞︿( ̄︶ ̄)︿。


原文链接: 阅读原文
免责申明: 架构说任何转载的文章都会明确标注原文链接。如有侵权,请与本站联系。
转载说明: 架构说原创文章转载时请务必注明文章作者、链接和来源。