Why - changnet/MServer GitHub Wiki
很长一段时间,我都是用多进程架构的服务器。这种服务器每个进程负责一个功能,比如gateway负责网络,game负责游戏逻辑,scene负责场景,database负责数据读写。这种服务器最大的优点就是简单。不仅仅是架构,写业务代码也很简单。比如做个好友模块,所有的玩家信息都在game进程,直接就可以获取。调试和维护也很简单,毕竟一个进程只有一个线程。
可是这个架构有个缺点,就是不方便扩展。一旦项目类型定下来,基本上也就确定了有几个进程,每个进程负责什么数据。后续想要扩展,就比较麻烦了。它不支持 同一个进程开多个来分担负载。
举个例子:
若要做全区服,即整个游戏对玩家而言只有一个服,那也简单。底层可以仍然采用多个服务器分担压力。只要客户端请求入口时,自动选中它所在的服。这样玩家在体验上即是全区服。
这样的架构,大约是:服务器1 + 服务器2 + 服务器3 + ... + 聊天服务器 + 活动服务器 + 好友服务器 + ...
它虽然可以做到全区服,但本质还是区服架构,后面的功能服(聊天服务器等)数据通过玩家服务器中转给玩家。但玩家一旦进入某个服务器,其数据就会在该服务器。除非通过特定的方法转移,否则没法去其他服了。这就导致各个服务器压力不一样,利用率不高。
更致命的问题是,这种架构只能在项目立项就是定好。假如某个项目一开始希望小区运营,后期希望大区,那修改的成本巨大。
所以这种架构越来越不讨喜。
想要做成灵活扩展,那就得向集群的方向靠近。虽然上面也可以通过不断堆服务器或者进程的方式来提高承载,但它有一个特点就是这些服务器的数据是不互通的,并且不能动态增删。比如玩家进了服务器10,数据就保存在服务器10,无法进入服务器1。而这个服务器10也无法删除(通过合服的方式不算)。这是和集群在本质上的区别。
为了达到扩容,需要增加线程、进程;为了达到数据互通,就只能有一份数据。所以演变成执行同一个逻辑(比如战斗)的线程、进程可以按需求扩展,同时访问同一个数据源。基本就变成了BigWorld、skynet这种框架的设计了。
这带来了3个大问题。
第一个是数据竞争。由于同一套逻辑,需要在不同线程执行。而在游戏中,往往是有数据交互的,那就会导致线程同步问题。总不能每个数据访问都加锁,那样不现实。况且增加的可能是进程而不是线程,都不在同一台机器上执行。那只能拆分功能,从设计上杜绝竞争。比如说玩家线程开了5个,当玩家升级装备时,这个数据只和玩家自己相关,所以在玩家线程里执行即可。但若是参加一个限购活动,大家就会抢购同一个商品,这时候限购活动就不能放玩家线程,而是放到一个公用的线程中。这会带来第三个问题。
第二个是硬件成本。拆分成多个线程、进程后,原本同一个线程简单的一个函数调用,变成了一个rpc调用,效率是低很多的。原本简单的多个MySQL,现在因为只有一个,明显是撑不住的,得上集群了吧。所以这不是1 + 1 = 2而是1 + 1 = 1.8甚至1.6。不过好在硬件可以通过钱来解决,不需要伤脑筋。
第三个是开发成本。原本所有的数据都在一个线程,直接获取就行了。但拆分之后,需要开发人员懂得整个框架的设计,知道数据如何分布,并且有针对性的优化。无论rpc模块设计得多么巧妙,这一点也是无法解决的。例如
--
local pkt = {}
for friendId in pairs(friendList) do
local fpkt = {
id = friendId,
pkt.xxx = xxx,
pkt.info = getFriendInfo(friend),-- 单线程
pkt.info = Rpc.getFriendInfo(friend)), -- 多线程通过rpc调用
}
table.insert(pkt, fpkt)
end
上面的代码里,假如是单线程就很直白。假如是多线程,因为部分数据不在同一个线程,需要rpc去另一个线程取回来,和当前的其他数据合并,才能构造完整的数据。如果开发人员不了解框架,就这么在for里面去调用rpc,虽然跑起来没问题,但性能就会比较差。
在实际项目的开发中,这一点成本就是没法抹去的,也是整个开发周期中一直持续要注意的,最伤脑筋。