在接手该项目时,服务器架构特点是每个游戏逻辑相关的服务器LoginServer、WorldServer、GameServer 直接连接数据库,这产生两个问题:
1、作为游戏主要逻辑的GameServer,也是直接连接数据库,在外网线上出现了性能问题,人数达到200人时,出现了卡顿,原因是GameServer 进行存档是同步阻塞式操作数据库,例如一个查询操作如果还没有查询出结果时,线程会一直阻塞于此,玩家人数一多(本项目为200人),就会造成数据库访问而阻塞线程,造成卡顿。这就是数据库同步式编程的弊端,对于多玩家的大量数据库操作,会产生这种性能问题;
2、在WorldServer、每个GameServer(分线)内部都维护一个数据库连接模块(不管是否同步异步),原本代码设计的不太妥当,造成数据一致性问题,例如玩家在GameServer_1玩了一段时间,产生了脏数据,在离线时自动会将脏数据回写到数据库,但这一步什么时候完成还不知道,因为此时GameServer_1可能在处理大量的数据库操作,未必能确保什么时候完成实际数据库操作,如果此时玩家在其他分线GameServer_2 登陆,就可能从数据库加载到旧数据,进而导致更多的错误!
(一开始接触这个问题,我首先想到的是《**屠龙》的做法,在每个GameServer、GateServer、WorldServer维护玩家的状态:登陆中、游戏中、登出中、已登出,对于正在【登出中】的玩家,是不允许进行data-load等进行重新登录操作的,因为脏数据还没有进行落地。但今天看来,这种做法在PT项目是行不通的,一是仅仅PT::GameServer 进行玩家状态维护;二是,在GateServer和WorldServer维护玩家状态其实就是让具有单点性质的服务器应该尽量少的参与游戏逻辑、尽量少做面向玩家状态设计;三是,《**屠龙》是不分线的,而是分场景,也就是一个角色只能在某个GameServer上,所以仅仅在GameServer上维护状态就足够了,但是PT::GameServer就是分线的,分线之间不知道相互的存在,是并行的,《**屠龙》是互斥的)
针对以上两点涉及性能和异步造成的数据一致性问题,后来采用了数据库异步编程架构,主要特点为提供DbManager(数据库服务器)用以做低速IO的隔离的代理服务,GameServer想要进行数据库操作(无论读写)时,通过网络消息协议告知Db-Manager,由其代理地进行实际操作,等有结果出来时,由DbManager回发消息给对应的GameServer:
以上,是从服务器架构上解决数据库相关【性能】与【数据一致性问题】,下面说说在模块逻辑上的异步编程的【弊端、或者潜在性的问题、或陷阱】:
1、最基本地,因为是异步编程,就必须有开发人员确保能够确保异步回调的正确处理:如是否能够在回调时获取到发起异步时的上下文,是否在有了这个异步结果后能够做后续还没有完成的事情;
2、如果一个功能会造成多次异步调用,需要考虑数据的【先后问题】与【数据完整性】问题(举个栗子:以前项目也使用了DbManager,并且DbManager 使用多线程机制以利用多核资源,登陆时需要获取玩家信息、技能列表、邮件列表,由于没有设计好这些部件之间的想关系,在接收到邮件列表的异步回调后,就直接发送消息给客户端,但是此时玩家信息都还没有,到时发送失败。)
3、逻辑先后问题(也就是此次2017-12-05 15:21:46的回档问题之一)玩家进行重新二次登陆时,会将上次登陆的游戏对象顶掉,按理应该对旧对象进行资源释放(数据存档、解锁、断开数据库连接)、对象回收,然后再进行第二次登陆的正常流程。但是,进行顶玩家操作时,没有进行实际的资源释放,也被告知可以进行二次登陆,结果导致二次登陆时加载到旧数据,进而造成回档。
4、(2017-12-12前)进行了一轮的调整:
①、客户端client连接到登陆服务器LS,发起账号登陆;
②、LS访问中心服务器MS,获取该帐号的登陆流程;
③、MS向具体的WS发起账号登陆;
④、WS获取分线GameServer列表和被分配用以登陆游戏的 网关Proxy,并告知对应的网关Proxy进行登陆准备;
⑤⑥⑦、返回Client连接被分配的 proxy、登陆key,分线列表;
⑧、客户端连接指定的网关服务器,并用刚获得的登陆key,请求分线进入游戏;
⑨、网关服务器广播给所有已连接的分线进行该帐号的存档下线操作;
⑩、该帐号的角色所在游戏服务器GS网db-manager发送存档请求;
⑪、独立db-manager进程进行实际入库。
有一些步骤是存在潜在性问题的:
⑨、假如GateServer是单点,没有做负载均衡,而此时GateServer挂了,GameServer就得不到通知让玩家进行存档,不过还好,玩家也自然断线了,系统自动进行存储;假如GateServer由于某种原因正在重启、并且GS只完成了对GS1、GS2的socket连接,而此前该帐号的玩家可能在GS3,也会导致玩家数据不会立马存档;所以一种解决方案是否可以是:在④的基础上,增加一个(4.5),WS也告知隶属下的各个 GS进行马上的玩家数据存档;并且考虑到 GateServer、GameServer都会触发玩家存档操作,那么就需要GameServer内不维护一个玩家状态管理,只在玩家数据脏并且还没有入档时才进行实际入档操作,如果已经有gate或gameserver 触发了入档操作,则不在重复操作。可以删除⑨中关于告知各GameServer进行存储操作而只使用(4.5)呢?好像可以耶。
⑨、目前是GateServer 告知各GameServer进行(A)顶玩家、进行数据落地操作,然后发起(B)登陆数据加载请求;如果解决了上一小节【逻辑先后问题】,那就应该是GameServer在(A)网DB-manager发送存档,接着在(B)发起玩家数据请求,原因是,我们默认了“只要按顺序地往db-manager发送了请求,也应该是按顺序地接收到返回结果”,即使(A)只是进行了操作的请求,而非实际真的同步完成数据落地;如果代码没有问题,没有各种异常安全、逻辑安全问题,确实能保证数据的一致性,但实际很难的保证,万一在(A)中有大量的数据要存档,而且可能会跨越不同的模块,就不能保证(A)里同一帧内进行了“入档请求”,而且万一某一步骤出现了异常安全、逻辑安全问题,而又静悄悄地返回了,(B)这一步还是会被执行到:
上一小结的【逻辑先后问题】,会出现 a* 与 b* 交叉的情况,解决后 a* < b* 得到了解决;但是GameServer中 a* 个小步骤间先没有操作结果判断的话(即及时a3失败了,a4还是会被执行)。
解决方案是,在玩家【登录加载】和【下线存档】各维护一个【操作列表】,如果列表内的某个元素的操作结果失败,则不应该进行后续的操作,并且应该以“强势的方式表现出该错误”(1、PT项目中有些功能逻辑是:
if(成功){xxx}就完了,没有写出现else时应该如何处理,这往往容易丢失正确的状态判断跟进;2、使用面向接口编程时,子类会覆盖基类的某些函数,最好地是用先显示调用一下基类的该函数,如果返回失败则应该直接返回,但是PT是直接在子类写“自以为是”的逻辑;这两个都可能存在潜藏的灰色逻辑bug);如果存储装备失败了,就不应该进行后续的任务存储操作(或者至少有某种方式通知开发人员此次存储失败了),更重要的是,(B)在(A)的存档结果出来后,才进行实际的操作请求,如果成功则才开始请求登陆数据加载,否则就不让登陆:因为数据脏了。
网友评论