H2Engine服务器引擎架构是轻量级的,与其说是引擎,个人觉得称之为平台更为合适。因为它封装的功能非常精简,但是提供了非常简洁方便的扩展机制,使得可以用C++、python、lua、js、php来开发具体的服务器功能。H2引擎的灵感来源于web服务器Apache。大家都知道Apache封装了浏览器的的连接和协议通讯,而具体功能逻辑则通过fastcgi的方式交由不同的编程语言实现,本人大学的刚接触php的时候,看到在php里print的字符串直接就出现在浏览器里,当时的感觉就是哇!这接口设计的真是帅!因为每个程序员最先学会的就是print,就会感觉这个接口设计的真是简单易用。所以php真是当之无愧的最好的编程语言(哈哈)。后来一直从事游戏服务器开发,发现在服务器引擎领域就一直没有这种Apache类似的设计非常通用、易理解、易扩展的引擎。现在游戏服务器领域大部分项目都是各搞各的,每个主程各搞一套自己用的舒服的架构。有些大厂或者相关的公司开源了一些服务器引擎,乍一看特别吊,但是跟Apache+php的这种架构相比,其易用性难以望其项背。当然服务器的长连接模式比web的request/response的模式本质上有更大的复杂性,服务器引擎的设计难点主要有如下几点。
那么如何解决以上问题呢?经过闭关苦思七七四十九天,终于有所开悟,继而设计出来了H2Engine服务器引擎。接下来本文将阐述H2架构的设计细节,以及是如何演化得来。
先看下最为常见的游戏服务器架构图:
这个架构是很成熟的,同时充分考虑了系统可伸缩性。Gate和GameServer是性能的关键,这两个都可以平行扩展,H2引擎就是从这个架构抽象而来。首先看Gate这个组件,每个Client连接一个Gate,而GameServer具体有多少个是对client透明的。因为可以启动N个Gate,所以这个架构理论上可以支持N个Client。linux实现的Gate单个进程撑2万连接已经不是问题,但是对于分服方式的RPG游戏,有哪个能做到单服在线2万的?我们的游戏都是限6000在线上限,超过就得排队了。主要是怕后边GameServer太卡,因为玩家有聚集效应,都会集中在比较热门的地图上。所以当今linux epoll单机如此高性能的基础上,单个gate进程玩家就足够应付一个区服的Client连接。所以在上面的架构图中简化为单gate,如下图:
这个时候发现LoginServer的功能就有些鸡肋了。LoginServer本来是类似于DNS的功能,它会返回负载最小的Gate给Client,从而保证Gate的负载均衡,但是现在已经单Gate了,LoginServer变得不是很有必要了,原来的LoginServer上的账户验证功能完全移植到GameServer来做。所以在H2引擎架构中,不再有LoginServer的角色。
Gate和GameServer肯定是不能少的了。DB是不是是必须的组件呢?答案是否定的。如果从DBServer发展的历史来看,当DBServer出现的时候,内存数据库还没有兴起,如今,Memcache、Redis等内存数据库已经大行其道,无论从效率还是稳定性,或者灵活性上,都更值得推荐。从运维角度讲,他们维护通用的内存数据库也更有经验。但是就本人看来,大部分情况下连Memcache、Redis这种都不需要,直接GameServer缓存一下就行了(主要是处理下断线重连,手游闪断还是很频繁的),因为GameServer本身就是有状态的服务器, 从上线后玩家数据就已经载入内存了,相当于所有的读操作都是缓存好的,所有的更新操作直接写数据库理论上完全可以撑住,而且直接写数据库也避免了小回档问题。因为毕竟写操作对于读操作量级小太多。如果真的应用场景需要缓存数据,那么部署一个Redis吧。去掉了DBServer,H2引擎架构简化成了只有Gate和GameServer,这次真的简化到极限了。
下面让我们来讨论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引擎中,完全有理由将进程全部跑在一个机器上,先上一个架构图,然后再讲一下这样设计有何特点。
到这里大家有没有发现,跑在一台物理机的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组件的名称进行重新命名,变得更加通用一些。
前边讲到服务器引擎设计的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模式不可同日而语。经过权衡,要比较优雅的实现进程间共享数据,限制在同机可以大大的降低复杂性,虽然牺牲了一些可伸缩性。
首先SharedMemory并不存储共享的数据,只存需要更新的数据,相当于共享内存作为交换数据的媒介。进程间共享数据的流程如下:
这种数据同步有多个好处,首先是数据竞争,共享内存加锁同步数据,效率非常高,使得加锁的粒度较小,避免多进程锁竞争。其二是更新操作很像发送消息,区别于异步发送消息的机制是,消息发送完,其他worker的数据立即得到了更新,这是异步消息发送机制不能比拟的。