crazyjohn / crazyjohn.github.io

crazyjohn's blog
9 stars 3 forks source link

share the wow's architecture(wow架构分享) #12

Open crazyjohn opened 9 years ago

crazyjohn commented 9 years ago

这篇要分享下GUAJIA目前的server端的架构,包括它的引擎以及业务。分析下它的有点和缺点,以及如何能更好。
@author crazyjohn

前言

time flies,目前为止已加入203快8个月了。早期负责一个新项目GONGTING的开发,这个项目的研发至今历历在目,server只有我带着航儿,苦杀了一个月,没早没晚没日没夜,杀得天昏地暗,杀出了一条血路。后期到了WOW负责server端的开发。不过两个项目来看核心的引擎基本没怎么变化,但是GONGTING时期我对引擎的并发部分进行了重构,业务层也相应进行了大改造。下面我把这套结构分享给大家。

1. 整体结构

先来看类图:

按照包为单元进行结构组织划分:

从部署角度来说,WOW在逻辑上和物理上都是单服,也就是所有的业务处理都放到一个进程里,然后部署到单台CVM云虚拟机上。下面分模块来说。

1.1 线程结构

也就是HawkThread体系。这里是server引擎中线程的部分,是重中之重,引擎的发动机。一个HawkThread扩展子一个Thread类,内部聚合了一个任务队列,并且对外暴露了addTask方法,用来供外部调用给thread添加执行任务。HawkThread是线程安全的,通过ReentrantLock可重入锁来保证并发的安全性。HawkTask是对任务的抽象,根据不同的应用场景它又派生了一些典型的子类,比如HawkProtolTask用来代表client发送过来的协议任务,HawkMsgTask代表服务器内部的事件任务等。HawkThreadPool是线程池的概念,也就是HawkThread的管理器,提供了一些对线程生命周期的管理办法,比如start启动和stop停止等。而且也可以通过参数来配置启动的线程数目,方便scale up纵向扩展。

1.2 网络层

  1. 网络层使用nio框架mina。所以一些相关组件基本都是对mina的封装。关于mina我有一篇文章专门介绍,但是很惭愧没有写完:【怎么写一个像mina一样的网络框架】。HawkSession是对IoSession的封装,用来封装client跟server的会话信息。HawkIoHandler是对IoHandler的封装,离业务最近的一层,同时也要注意回调接口的线程安全性。HawkNetManager是对Acceptor的封装,即网络设计架构Connector-Acceptor中的的Acceptor角色,用来接收client端的连接。HawkDecoder和HawkEncoder分别对应解码器和编码器。
  2. 序列化层。序列化层我们使用protobuf来做序列化和反序列的工作。pb的好处首先就是多语种支持以及各种thirdparty组件。本身又有版本号和varint压缩等优点。

    1.3 数据层

  3. 组件简单介绍。HawkDBEntity是DB端的基础实体,其它各个业务实体派生自它。HawkDBManager是DB端的业务管理器,内部聚合一个HawkThreadManager用来进行更新实体的执行。PlayerData是玩家的数据层,内部聚合所有玩家相关的DB实体以及封装部分访问方法和业务方法。
  4. 首先是ORM层,wow使用hibernate进行DB实体和DB表的映射,算是中规中矩的实现。
  5. 异步写。DB实体更新这里使用的结构也是HawkTheadManager,所以是可以根据情况配置写线程的数量。HawkDBManager内部有一个队列容器用来管理异步写请求,然后DB主线程定时执行更新操作,从队列中依次取出请求,然后派发给不同的写线程去执行更新。这样设计的好处也是可以在一定程度上提高并发和吞吐。

    1.4 模板层

HawkConfigBase是所有模板的基类,各个业务的模板都从此处派生。HawkConfigStorage是模板的容器类,每类型的模板都放到自己的容器中。HawkConfigManager是模板服务的入口,对外暴露了获取指定模板的各种接口。模板数据对外是只读的,不暴露写接口。

1.5 基础对象层

基础对象结构。HawkObjeBase体系。这是游戏中的基础对象,HawkObjManager是它的管理器,提供一系列的生命周期管理方法,比如queryObject查询对象,freeObject释放对象,removeTimeoutObj移除超时对象等等。还有很重要的两个方法就是:管理器提供对基础对象锁定lockObject和解锁unlockObject方法,用来解决在有并发操作时候的问题。

1.6 业务层

业务层按照Module模块进行划分。模块这里有几个典型接口:

  1. onPlayerLogin处理登陆业务。
  2. onPlayerAssemble来处理装配逻辑。
  3. onPlayerLogout来处理登出逻辑。
  4. onProtocol用来响应网络协议。
  5. onMessage用来响应内部通信事件。

网络协议处理这里后期我们又添加了注解代替注册来减少出错几率。如图:

1.7 日志

日志使用self4j + log4j。基础组件使用log4j的DailyRollingFileAppender来记录行为日志按照天为单位进行文件分割。一些功能性的日志比如异常之类的也是一样的机制。

2. 核心循环

核心循环:

这个时序图很简单,介绍的是引擎组件的心跳时序,大概机制跟下面介绍到的实体Player和Manager跟对应的线程绑定,然后由指定线程触发实体的心跳的机制是一样的,结合着看。

3. 核心时序

核心时序:

时序图介绍的是一次网络消息的处理流,这里不详细介绍,可以结合底下优点部分的分析一起看。

4. 其它服务

相对完善的周边服务:

  1. cdk。后期做过重构,目前持久化支持memcache,redis,mysql等。
  2. collector。数据收集服务。
  3. IpServer。IP服务。

    5. 架构优点

并发处理方式

  1. 网络消息处理方面。wow的任务分发体系很有特点。看图: 接着说一次典型的client到server的请求交互时序:当mina层收到client的请求被反序列化成一个Protocol对象以后会回调到App的onSessionProtocol接口,然后App会取出对应的XID,然后根据XID的值和HawkThreadManager的线程池的长度做一个模运算得到一个index,然后拿这个index取出一个HawkThread并且把这个Protol包装成一个HawkProtoTask投递(addTask)给这个线程,然后由这个线程作为驱动方来处理这个网络请求。所以你可以理解为引擎会把所有的玩家按照id分配(绑定)到指定的N个逻辑线程,然后所有对应玩家的网络请求都会用对应的绑定线程来驱动处理。但是背后的线程体系对于业务实体Player和Manager
  2. 内部通信方面。HawkAppObject有两种实现,其一是Player代表玩家抽象,其二是Manager代表全局的业务管理器。那么这两种实体之间应该如何通信呢?还是使用App层的接口:postMsg,也就是PlayerA想跟PlayerB或者Manager通信的时候,可以通过Post接口抛事件给它,内部机制类似上面处理网络协议,App会根据HawkMsg(代表一次内部通信事件)的target字段把事件分发到指定的实体绑定的线程去驱动执行。
  3. 这样设计并发的好处是,一定程度上对内隐藏了线程,锁等机制,同时通过暴露参数可以很方便的scale up纵向扩展,充分利用单机硬件性能,很大程度提高了逻辑层的并发性,服务器整体的吞吐性。而且隐藏了很多并发带来的复杂性,减少了coder犯错的机会。
  4. 这个并发结构跟前人聊是来自于之前PerfectWorld的C++端游服务器的思想,其实它已经抽象出了一个actor模式的雏形。【科普下1.什么是actor2. scala/java的actor实现=>akka】用来从另外一个角度来处理并发,注意我说的只是一个雏形,而且wow中的业务层很多地方写的无比混乱,很多没有按照设计的通信方式去走,所以很多地方已经破坏了这种设计带来的好处,同时会带入很多并发问题。但本身引擎是清白的。

    6. 更好的结构

  5. 数据层的设计。目前数据层的设计是最简单粗暴的把玩家所有的数据都放到PlayerData这个类里,导致这个类成了一个巨类,根本无法维护。更好的设计方式应该是把数据分散的各个模块,各模块自己维护自己的数据,把业务层和数据层分开,这样职责就会很单一,结构很清晰,不会出现通信紊乱的情况。
  6. 缓存层次过高。游戏中的缓存机制设计的很简单,就是时效性的缓存清理机制,通过心跳的机制去做过期缓存的清理。但是它的层次很高,这个怎么讲呢?就是wow的缓存层不在数据层,而是在业务这一层,那业务层的玩家就出现了多个状态,比如isOnline,isInCache等,这导致业务层在写相关代码的时候就会很困惑,很容易出错(wow的历届server人员一定深受其害),展开讲,缓存出现的意义是缓解db的读,也就是缓解玩家数据在开始加载的时候dbLoad读io慢的问题,所以其实业务层是不用关心这个玩家到底是在缓存中还是db表中这种事情的,数据层只是给业务暴露相关get接口即可,让业务来获取数据。解决方案也很简单,就是降低缓存层次,放到数据层,这样业务的读和写请求都可以先跟缓存打交道(对业务是透明的),而业务层也只有isOnline这样的状态,也就没有了状态相关的困惑了。更近一步我们可以加入类似LRU这样的缓存机制,可以一定程度提高缓存的命中率。
  7. 读异步,预加载。wow目前的所有db读都是同步的,这样有一个问题:游戏中业务请求的处理时间量级和db级别的读写请求的量级其实是不一样的,我用放大些的数据做个对比。游戏业务的请求处理时间在1ms左右,而db端的请求处理在100ms左右,所以db请求如果和业务请求在一个队列里,势必会影响到对应载体线程的吞吐,所以更好的做法是读异步化,把不同类型的任务分配到不同的线程池处理。或者还有就是我们可以在数据层做开关来实现预加载,这样的好处是读响应会加快,坏处是启服会慢,开始会吃掉相应量的内存,不过我们可以加一些机制去优化之类的。
  8. 切面引入。切面即AOP的思想,可以应用的很广,但是这里提出来我针对的还是数据层。wow的数据层在DBEntity实体更新的时候,需要手动去调用相应的接口叫notifyUpdate,这个操作coder其实很容易忘记,忘记那这次写就会丢掉。切面可以很优雅的解决这个问题,比如aspectj,它兼容java的语法,而且采用的切入方式是在编译期植入字节码,对效率是零影响。我们可以在DBEntity的指定切入点切入,让切面替coder去做notifyUpdate这件事情,替coder省了心,降低出错概率。唯一问题是有些许学习曲线。
  9. 模板层自动化。wow的模版层设计的还可以,整体中规中矩。目前有个不方便的地方就是模版类需要coder手动去写,其实这个事情是可以交给工具去做的,让工具根据策划表(xml/excel/json/DSL)等去生成模版类,进一步还可以工具把表中的某几个字段映射成一个对象或者一些重复字段映射成一个集合等等。
Joker-Qian commented 9 years ago

4被谁吃了? @田志远