H2Engine服务器引擎架构是轻量级的,与其说是引擎,个人觉得称之为平台更为合适。因为它封装的功能非常精简,但是提供了非常简洁方便的扩展机制,使得可以用C++、python、lua、js、php来开发具体的服务器功能。H2引擎的灵感来源于web服务器Apache。

H2Engine服务器引擎介绍#

简介

  H2Engine服务器引擎架构是轻量级的,与其说是引擎,个人觉得称之为平台更为合适。因为它封装的功能非常精简,但是提供了非常简洁方便的扩展机制,使得可以用C++、python、lua、js、php来开发具体的服务器功能。H2引擎的灵感来源于web服务器Apache。大家都知道Apache封装了浏览器的的连接和协议通讯,而具体功能逻辑则通过fastcgi的方式交由不同的编程语言实现,本人大学的刚接触php的时候,看到在php里print的字符串直接就出现在浏览器里,当时的感觉就是哇!这接口设计的真是帅!因为每个程序员最先学会的就是print,就会感觉这个接口设计的真是简单易用。所以php真是当之无愧的最好的编程语言(哈哈)。后来一直从事游戏服务器开发,发现在服务器引擎领域就一直没有这种Apache类似的设计非常通用、易理解、易扩展的引擎。现在游戏服务器领域大部分项目都是各搞各的,每个主程各搞一套自己用的舒服的架构。有些大厂或者相关的公司开源了一些服务器引擎,乍一看特别吊,但是跟Apache+php的这种架构相比,其易用性难以望其项背。当然服务器的长连接模式比web的request/response的模式本质上有更大的复杂性,服务器引擎的设计难点主要有如下几点。

  1. 通讯协议没有标准。大家都知道,http有行业标准,所有浏览器都是按照标准与服务器通信的,而通信部分的实现是服务器最为关键的部分,服务器程序员一般都知道,《网络编程》没看过几遍是写不了服务器程序的。一般而言服务器会采用二进制通信,常见的组包格式有2字节协议号+2字节标记+4字节包体长度+包体数据,这种协议格式紧凑,2字节的标记留作扩展也比较够用,比如是否启用压缩、加密等,但是这种对某些编程语言不是很友好,比如js就无法采用此种协议。
  2. 消息封包没有标准。消息封包常见的有struct二进制、自研的序列化、pb、thrift、json等几种形式,而在web领域,一般要不json要不xml。在服务器领域一般采用pb的较多。
  3. 编程语言多样。服务器编程语言为了高效,总体以c++为主,但是java、c#、python、lua、php、js也越来越流行,尤其是c++嵌入lua的模式大行其道。让服务器引擎像Apache一样可以支持各种语言,实现上很有难度。
  4. 并发与异步。通常游戏服务器为了平衡游戏复杂性和性能,采用多进程且每个进程主逻辑单线程的方案,多进程增加吞吐,单线程的程序更好保证稳定性,为了主逻辑不阻塞,几所所有的io操作都是异步完成的,这与Apache的理念有很大的区别,这使得Apache引擎很难封装的像Apache那样简洁,市面上有些人尝试了用协程简化异步,但是目前还形成相对成熟的方案。
  5. 数据同步的复杂性。Apache中php也是多进程的,但是不共享数据,无状态的php设计本身就大大降低了复杂性,但是长连接是有状态的。php中把状态数据放到memcahe、redis等内存数据库中,游戏服务器的多进程架构中也难免有数据需要共享,比如行会数据,但是像php那种通过分布式内存数据库同步方式获取在性能上(比如实时rpg游戏)是无法忍受的。如果采用异步获取,逻辑代码势必支离破碎,到处都是回调,难以维护。通常的解决方案是单独拎出来一个进程处理共享数据,比如CenterServer处理行会请求,所有行会操作都会转到CenterServer处理,再将结果同步到其他进程,这样不存在数据竞争和同步的性能问题,但是逻辑因为异步仍然是复杂了特别多。
  6. 性能难以量化。大家都知道Apache提供了ab程序可以量化服务器的性能,在服务器领域几乎没有通用的量化工具。一般都是会上线前用机器人压力测试一下,不能很好的量化各个接口的性能,web领域对接口性能量化的工具比较多,很成熟,确实值得研究学习,因为优化的原则就是现有数据再优化,必须知道哪些需要优化,优化完有多少效果。

  那么如何解决以上问题呢?经过闭关苦思七七四十九天,终于有所开悟,继而设计出来了H2Engine服务器引擎。接下来本文将阐述H2架构的设计细节,以及是如何演化得来。

H2Engine服务器引擎的演化

  先看下最为常见的游戏服务器架构图:

Alt text

   这个架构是很成熟的,同时充分考虑了系统可伸缩性。Gate和GameServer是性能的关键,这两个都可以平行扩展,H2引擎就是从这个架构抽象而来。首先看Gate这个组件,每个Client连接一个Gate,而GameServer具体有多少个是对client透明的。因为可以启动N个Gate,所以这个架构理论上可以支持N个Client。linux实现的Gate单个进程撑2万连接已经不是问题,但是对于分服方式的RPG游戏,有哪个能做到单服在线2万的?我们的游戏都是限6000在线上限,超过就得排队了。主要是怕后边GameServer太卡,因为玩家有聚集效应,都会集中在比较热门的地图上。所以当今linux epoll单机如此高性能的基础上,单个gate进程玩家就足够应付一个区服的Client连接。所以在上面的架构图中简化为单gate,如下图:

Alt text

   这个时候发现LoginServer的功能就有些鸡肋了。LoginServer本来是类似于DNS的功能,它会返回负载最小的Gate给Client,从而保证Gate的负载均衡,但是现在已经单Gate了,LoginServer变得不是很有必要了,原来的LoginServer上的账户验证功能完全移植到GameServer来做。所以在H2引擎架构中,不再有LoginServer的角色。

Alt text

   Gate和GameServer肯定是不能少的了。DB是不是是必须的组件呢?答案是否定的。如果从DBServer发展的历史来看,当DBServer出现的时候,内存数据库还没有兴起,如今,Memcache、Redis等内存数据库已经大行其道,无论从效率还是稳定性,或者灵活性上,都更值得推荐。从运维角度讲,他们维护通用的内存数据库也更有经验。但是就本人看来,大部分情况下连Memcache、Redis这种都不需要,直接GameServer缓存一下就行了(主要是处理下断线重连,手游闪断还是很频繁的),因为GameServer本身就是有状态的服务器, 从上线后玩家数据就已经载入内存了,相当于所有的读操作都是缓存好的,所有的更新操作直接写数据库理论上完全可以撑住,而且直接写数据库也避免了小回档问题。因为毕竟写操作对于读操作量级小太多。如果真的应用场景需要缓存数据,那么部署一个Redis吧。去掉了DBServer,H2引擎架构简化成了只有Gate和GameServer,这次真的简化到极限了。

Alt text

   下面让我们来讨论N个GameServer应该放几台机器上的问题。标准答案当然是需要几台放几台,但是如果你身边有运维的话,他可能给出的答案是一台机器,为什么呢,原因其一是这样运维更方便管理,下发程序、配置、重启、监控等也更容易。原因其二是现在机器都是多核cpu,内存也是过剩的,单台机器的处理能力与往日不可同日耳语。GameServer是主逻辑单线程的,如果一台机器上部署一个,那么cpu资源无法得到更好的利用。就本人经验而言,GameServer很少需要超过4个,为啥?想想看,如果一个RPG游戏单服设计在线1万人,平均分配到每个进程也就是2500人,很轻松啊,当然如果人过多聚集在单个进程,那还有有可能单个GameServer成为瓶颈,这种情况多开GameServer也解决不了问题。从cpu利用上来说,GameServer主逻辑单线程只能用一个cpu内核,考虑到启停io线程的计算需要一个cpu的计算量,那么平均2个cpu,4个GameServer也就是8个cpu,现在服务器没有8核好意思说是服务器?以往经验来看,玩家会比较集中在热点地图,一般会某个或某两个GameServer相对会cpu较高。另外一个服务器角色Gate是io密集型的,所以和GameServer放到一个机器上,也是扛得住的。这样在H2引擎中,完全有理由将进程全部跑在一个机器上,先上一个架构图,然后再讲一下这样设计有何特点。

Alt text

   到这里大家有没有发现,跑在一台物理机的Gate和GameServer像不像Apache和php的关系?到此,H2引擎的雏形已经形成。Gate在这里扮演Apache的角色,GameServer在这里就是php的角色,Apache有一层fastcgi的东西实现进程间通信,只要按照fastcgi的标准,就可以让Apache支持任何的编程语言,在H2引擎中,也设计了一套进程间通信机制ffrpc,区别于Apache的fastcgi,ffrpc是基于消息+回调机制的长连接通信方式。ffrpc的实现暂时不展开了,现在H2引擎里已经实现了c++、python、lua的支持。H2的雏形已经有了,还需要进一步的抽象完善,因为H2不仅可以用于游戏服务器,在实时聊天、消息推送等需要长连接的应用场景也可以适用。所以为了更加容易理解,对Gate和GameServer组件的名称进行重新命名,变得更加通用一些。

Alt text

   前边讲到服务器引擎设计的6大难题,下面讨论下在H2引擎中是如何解决的。首先是通信问题,Apache通用是因为Client都是用http协议,那么可不可以让游戏服务器的Client统一用某种通信协议呢?坦白说太难了。但是本人认为,随着websocket的逐渐普及,websocket可能有一统江湖的可能。其实有了websocket大家自己设计通信协议的理由已经很小了。H2集成了两种通信协议,websocket和普通的二进制协议,如果你的Client已经使用了websocket,那么接入H2就是so easy了。

   对于问题2数据封包的处理,H2给出的答案就是无为而治,既然没有标准,那么H2也不干涉你的选择自由,交给H2Worker处理,数据封包对于H2引擎是透明的,但是建议大家使用pb或者thrift就好了,H2的ffrpc就是使用了thrift完成的进程间通信。本人更推荐thrift,因为thrift对于各个语言的支持更好,对于js这种处理二进制尴尬的语言都兼容的很好。

   问题3的多语言问题,H2设计了ffrpc库,每个语言只需要接入并实现几个简单接口就可以了,相当于每个语言都需要开发自己专用的H2Worker,比如H2WorkerPhp、H2WorkerPython、H2WorkerLua等,目前C++、Python、Lua、js、php的Worker实现已经集成到H2Engine中,也就是说如果你想用lua或者python来写游戏服务器,那你直接写脚本就可以了。H2Engine晚些会加入支持的语言是C#。

   问题4并发与异步的问题,H2Engine的设计是主逻辑单线程,提供一个IO线程池,IO操作用异步+回调的方式完成。其实IO操作主要就是数据库操作,IO线程会创建一个异步IO句柄,每个IO句柄投递的IO异步操作都是串行保证顺序的,所以IO线程池既能够保证多线程并发,又能够保证比如针对某个User的操作是顺序的、可靠的。

   问题6性能量化的问题,由于客户端的请求通过引擎被处理,那么H2Worker上就可以收集到所有接口的性能数据,统计后格式化定时输出,这样就可以量化各个接口的的性能。甚至可以开发出图形化展示工具,可以看接口性能随时间的变化,或者不同接口间性能的比较。

   最后着重讨论问题5数据共享的问题。前边提到ffrpc提供了基于TCP进程间通信的机制,对于单机还是多机,都是无差别的,那么H2Engine和H2Worker理论上放不同机器也是可以的。事实也的确如此,H2引擎其实对于多机是完美支持的,但是为什么将H2的架构限制在同机器呢,这主要是考虑到数据共享的需求,同机情况下,H2Engine和H2Worker就可以通过共享内存共享数据,其效率和便捷性与多机tcp模式不可同日而语。经过权衡,要比较优雅的实现进程间共享数据,限制在同机可以大大的降低复杂性,虽然牺牲了一些可伸缩性。

Alt text

  首先SharedMemory并不存储共享的数据,只存需要更新的数据,相当于共享内存作为交换数据的媒介。进程间共享数据的流程如下:

  1. 每个H2Worker维护一个自己的ShareMemDataSet,在共享内存中创建一个信号量,并且单独开一个线程,监听在此信号量上,如果被触发,则立即从共享内存拷贝要更新的数据到自己的进程,并投递给主逻辑线程去更新SharedMemDataSet。由于ShareMemDataSet是主逻辑维护的,这样的好处就是主逻辑线程如果只是读取而不修改,那么直接使用本线程的SharedMemDataSet数据,性能自然是杠杠的,比如行会数据一般读取操作远大于写操作。

  2. 如果H2Worker要修改共享数据,他就要获取共享内存上的全局锁,然后拷贝要更新的数据到共享内存,然后唤醒其他H2Worker的信号量,待所有数据被拷贝完毕后,解除全局锁,因为更新操作一定是主逻辑操作的,所以获取完全局锁后,主逻辑会自动检查一下本地要更新的操作是否全部完成,保证加锁完毕后,当前进程的SharedMemDataSet一定是最新的。下面来一段模拟行会操作的伪代码:

  这种数据同步有多个好处,首先是数据竞争,共享内存加锁同步数据,效率非常高,使得加锁的粒度较小,避免多进程锁竞争。其二是更新操作很像发送消息,区别于异步发送消息的机制是,消息发送完,其他worker的数据立即得到了更新,这是异步消息发送机制不能比拟的。

总结

  1. H2引擎集成了websocket,也推荐大家在长连接应用中,逐渐使用websocket。
  2. 协议的封包pb、thrift已经很够用了,H2引擎支持pb、thrift、json以及传统二进制struct,但是推荐thrift,主要是效率和多语言支持都更好。
  3. 基于网游服务器的场景,H2引擎考虑到单台物理机的处理能力当前足以应付单服的需求,所以将H2的架构设计为部署在同机上,这样大大简化了服务器的架构,多gate的架构其实来源于rpg刚兴起的年代,那时候服务器的内存有限,cpu多核也还没流行,但是今非昔比,单机模式也就是伪分布式模式其实更符合实际。
  4. 针对传统网游服务器架构中多进程数据共享的痛点,H2做了特殊的设计,由于H2Worker在同一台机器上,得以使H2可以通过共享内存共享数据。   大家知道,Apache+php之所以在web领域里流行,还有很大一个原因是php的框架又多又好用,相比而言,网游服务器领域的引擎、框架都太落后了,主要原因还是服务器没有形成标准,这也是本人从业多年,孜孜不倦想要有所突破的地方。从web的成熟经验来看,功能开发的快,就要有好多框架,要有好的框架,就要有成熟标准的引擎,现在市面上有些游戏服务器引擎就经常会糅合引擎和框架的功能,有的甚至夹杂了游戏服务器的数据结构和游戏逻辑。H2的设计哲学,引擎的归引擎,框架的归框架,虽然跟Apache相比距离“引擎”的称号相距甚远,但是这是H2的目标。另外,基于H2的框架也会不断的增加完善。举个例子,针对rpg游戏,我们可以设计出一套c++的框架,比如封装地图管理、角色管理、道具管理、任务系统、成就系统、副本系统、npc系统等,想想看,2d rpg领域相关的系统还是很好抽象的。问题是没有标准的、成熟的引擎作为基础。相关从业人员应该有共鸣,比如A团队开发一套任务系统,给B团队也是用不了啊,大家的定时器、数据库接口都不一样,无法做到拿来就用。如果大家都用H2,别人开源的系统分分钟就可以拿来用,想象下还是挺美好的。不同的游戏类型框架实现是不一样的,不同语言实现细节也会不同,使用H2引擎后可以根据不同游戏类型、不同语言分类框架,这个是后续扩展H2引擎的计划。