纵横九州跨服服务器架构

发表于2016-01-27
评论6 3.3k浏览
  纵横九州是魔方工作室群首款分区分服游戏,2014年1月上线以来受到不少玩家的喜爱,特别是很多跨服玩法到现在还有很高的参与率。 九州至2014上线以来,由于支持少,新近少,急需新玩法留住老玩家,而跨服玩法也是分区分服游戏的主流玩法,因此在15年重点上了好几个跨服玩法。
  这里介绍下九州跨服服务器的设计思路以及在运营当中遇到的问题。跟大部分游戏一样,跨服服务器指的是一个服务器,而不是一个混合大区。玩家必须正常进入大区以后,从而参与跨服玩法。

跨服服务器接入方式
1、客户端直连。客户端在连接跨服服务器之前先从大区服务器获得改跨服玩法对应的服务器ip和port,然后直接tcp发起连接。这种方式就要求客户端同时维护两个socket,由于跨服不同玩法可能部署在不同的服务器上,所以也会导致客户端-跨服的socket频繁切换链接。这对客户端框架改造会带来不小的麻烦。
2、接入层直连。客户端只维护一个链接,所有的命令有接入svr或者dispatch svr进行转发。这种方式就把接入层和跨服的逻辑耦合在一起,不利于玩法的扩展。比如九州的跨服国战玩法(1000人分一个区,同时多个区开打。后台根据服务器压力可以进行机器的扩容),玩家可以在多个区自由的切换观看局势,由于跨服服务器必然是有状态的,需要加载玩家数据到共享内存里,所以再登陆其他机器的时候要先将上次登陆的数据写回db,否则就会发生数据被覆盖的情况。这就要求玩家身上要有一些状态的存储。而转发层没办法做这个事。另外如果接入层和跨服服务器用tbus通道 直连,会占据跨服服务器不少内存。
3、大区服务器转发。考虑到九州原有的服务器架构,以及当时游戏现状(在线不高,机器负载低),所以采用了大区转发的方式。这样客户端也不用费力维护两个链接,同时也可以解决跨服玩法扩展带来的问题。

跨服服务器的框架设计
1、已上线游戏最大的敌人就是开发时间,这是所有项目不可避免的。
2、另外跨服玩法多样性要求我们能求同存异,即能满足不同玩法的差异,又能提取他们的相同点,从而减少工作量。比如大部分跨服玩法都是要求玩家找一张桌子,坐下,举手,队长开始游戏等流程。
3、当我们有一个新玩法要上架时,我们希望外网能快速更新,而不需要停机。或者当我们修复某个玩法的一个bug时,我们希望能够快速的发布。
基于以上的考虑,这里采用了框架+挂载so的方式。每种游戏玩法就是一个so。
简单介绍下几个模块的用途
Module Manager:用于so管理,load时获取so的GameModule对象。
Table Manager:用于桌子的创建和销毁。每张桌子创建时实例化一个so的逻辑对象。其中so的过程数据就序列化存储在桌子里。桌子和so分别抽象出接口,面向接口编程。框架和so各持有对象的基类指针。桌子给框架的接口并不暴露给so。
Maintenance Manager:服务器状态维护管理器。给玩家好的体验。在外网发版本停机前,发送信号给game,15分钟内不允许新玩家登陆跨服,正在玩跨服的玩家则不影响。

跨服玩家数据 
  跨服服务器的玩家数据分为两部分。跨服服务器没有玩家大区数据,所有奖励都会发送到大区进行处理。
1、玩家基本信息,主要用于展示以及战斗数据,不存db,每次从大区登陆到跨服时带过来进行更新
跨服玩法数据,存db

框架和so如何解耦
1、数据层解耦。存储一些游戏的通用字段,而和具体so的数据是解耦的,并基于桌子给so提供存取服务。
协议层解耦。框架只解析通用的协议,而so具体的协议框架是通过子协议的方式,回调so的接口,让so中自己解析处理
接口解耦。图中的绿色部分,是需要so实现的.so和桌子互相持有对方的基类指针。其中IGameModule其实就是so的工厂类,可能取名叫IGameFactory更容易理解。

公共桌子    
  以上所讲框架和玩法都是基于桌子的,但有时候策划想要的玩法是跟桌子无关,是基于服务器触发的,比如九州的名人堂玩法,他是全区所有玩家的行为的一个排行榜。对于另一些玩法,用基于桌子的方式也可以实现,但是需要一个桌子长期存在,而框架的桌子都是由玩家创造的,玩家全部离开后,桌子就会解散掉。对于这种特殊玩法,我们定义了公共桌子的概念,服务器启动的时候主动去分配一张桌子,长期存在,不会释放。

效果
  九州目前的跨服已有多人副本,多人竞技场,名人堂,三国群英传,跨服国战等,新玩法的开发只需专注于游戏的逻辑,节省了一半工作量。

遇到的问题
  跨服玩法在开发以及上限运营以来也遇到了一些问题,这里记录下来供大家参考。
1、路由转发
问题:大区svr主动发起请求到跨服,跨服进行回包,Router很容易就能根据目的bus转发到大区里。但是有些跨服玩法有主动发请求到大区的需求,跨服只知道玩家注册时的大区id,而不关心这个服被合到哪去了。而Router也只能感知和自己集连的大区,所以根本不知道被合掉的大区应该转发给谁。
 解决方案:我们再deploy.xml里,对每个zone添加一个自定义属性节点,叫MergeSvr。合服的时候会记录合并过来有的大区id。然后router在生成配置的时候去解析这个属性,生成map<大区id,busid>的映射。
2、So拷贝
问题:在预发布的时候操作跨服更新,先进行解压,然后再重启,跨服服务器就会core掉。
原因:这是动态库加载的问题。具体可以参考这篇文章。
单例
图中可以看出框架和so调用单例在调用时会产生两个不同的对象。
解决方案:
a桌子提供接口时将对象往下传递;b.编译时使用 -rdynamic参数。个人比较建议使用a方案,毕竟框架和so本身就是要解耦,暴露过多的符号就违反了这个原则。
3、double free
问题:跨服服务器在停服的时候会有core,用gbd跟进去发现竟然是core在了protobuf的文件描述符里,google::protobuf::FileDescriptorTables::~FileDescriptorTables()。
FileDescriptorTables中定义了几个静态的hash_map,game和so退出时都会对其析构,导致double free。
解决办法:如图所示。如果已经安装了protobuf,需要重新安装,生成.a。
貌似也可以用fvisibility=hidden对某些符号进行隐藏来解决这个问题,可以参考这篇文章 https://gcc.gnu.org/wiki/Visibility



如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引