在使用cluster_server做gameserver后台集群支撑的过程中,逐步暴露出一些问题,在此记之。
1. 节点交互效率低下
我们按照业务职责将集群节点分为,agent,player,map,alliance,battle等,这些节点可以在一台物理机上(操作系统进程间交互),也有可能部署到不同的物理机上(网络IO交互),而玩家一个业务逻辑可能涉及到多个节点,形成一个节点交互链:如玩家在大地图上进行一场战斗,按照流程需要走:agent -> player -> map -> battle -> map -> player -> agent。整个过程都是异步的。这种交互是频繁的且低效的。
反思cluster_server最初的设计:根据业务职责来划分节点,这样做主要优势在于容错(灾)性和负载均衡,在节点调试和调优方面也很方便。但是对于业务逻辑来说,玩家的数据被分散到各个节点,这些节点之间异步交互(我们对call是敏感的),造成了数据同步和逻辑交互都变得复杂,这种复杂度随节点数量和逻辑复杂程度(涉及到的节点数)还会不断上升。
针对于这些问题,我们首先对集群进行了重整,将同一个server_id下的player,map,pvp,alliance等轻量的进程放在一起,由server进程统一管理,并挂载server_node之上。这样整个集群的节点只剩三个:agent,server,battle,满足了数据局部性原则,业务逻辑交互也更高效(Erlang进程之间)。mnesia集群表仍然保留,用于执行快速的消息路由和进程查找。
2. 玩家数据同步
这一直是分布式系统中的一个大问题,玩家的数据被分散在各节点,不同的节点还需要玩家一些基础信息的副本,这部分需要同步的数据很难管理。主要有以下几种方案:
- 玩家跨节点时,将本次处理所需要的信息带上。这种方案只能运用于玩家本身的数据同步。而玩家可能或查看或拉取其它玩家的数据
- 各节点存有玩家部分数据的副本,玩家对应数据更新时,player通过cast消息到相关节点进行数据更新。这适用于简单的数据同步,如玩家name,level等,复杂的数据,比如玩家的英雄信息,会导致通信量过大
- 各节点在需要实时玩家信息时,去player身上拉取。这是不可取的:第一,不能call player 第二,目标player进程不一定还在,特别是流失玩家
- 做一个Cache中心,并且满足单写多读的原则。比如map节点改变了玩家数据,那么它应该将数据变动发给player节点,由player节点来完成数据更新并更新Cache
在重构集群之前,我们用的是方案2,主要原因是数据简单,更新实时。要在跨节点同步中,使用Cache中心,可以直接通过mnesia实现,但是这是软实时,对于一些实时性很高的数据,需要使用事务。但在建立了server_node之后,就简单很多了,通过ets即可实现,但仍然要满足只有一个写入者的原则。
3. 节点交互链过长
可以在agent消息路由处,将map相关消息直接路由到map,而map业务处理完成之后,如果不需要在玩家身上增减资源,则可以直接将Ack发到玩家对应的agent上,流程得以简化。
总结
目前服务器的分布式特性:
- 按照ServerId组成一颗大的监督树,一个Server及其所有包含模块,均作为整体部署在一个server_node上。这样Server内部的逻辑与数据得以聚合。而负载均衡将以Server为基本单位
- 在节点内部,为了方便交互,仍然使用Mnesia作服务注册与发现,但由于Server之间没有隔离,要求服务ID全局唯一
- 对于单服管理,可能需要一个进程维护一个Server的同类服务(player, alliance),以进行LRU,全服广播等服务器逻辑
可选的优化方案:
- 服务隔离:对各Server之间的服务进行隔离。隔离的好处:一是减轻Mnesia单表压力,二是对Server逻辑有更好的支撑:如全服通知,停服操作。但就Mnesia来说,我目前没有找到好的隔离方案
- 本地服务:一些服务是否可以优化至单节点读写,这样Mnesia可退化为ETS,换来更好读取速度,并且极大减轻Mnesia压力
- 控制节点数量:删除不必要的Mnesia节点,通过消息而不是Mnesia来同步服务