目前,我们的游戏服务器组是按多进程的方式设计的。强调多进程,是想提另外一点,我们每个进程上是单线程的。所以,我们在设计中,系统的复杂点在于进程间如何交换数据;而不需要考虑线程间的数据锁问题。
如果肆意的做进程间通讯,在进程数量不断增加后,会使系统混乱不可控。经过分析后,我决定做如下的限制:
-
如果一个进程需要和多个服务器做双向通讯,那么这个进程不能处理复杂的逻辑,而只是过滤和转发数据用。即,这样的一个进程 S
,只会把进程 A 发过来的数据转发到 B ;或把进程 B 发过来的数据转发到 A
。或者从一端发过来的数据,经过简单的协议分析后,可以分发到不同的地方。例如,把客户端发过来的数据包中的聊天信息分离处理,交到聊天进程处理。
-
有逻辑处理的进程上的数据流一定是单向的,它可以从多个数据源读取数据,但是处理后一定反馈到另外的地方,而不需要和数据源做逻辑上的交互。
-
每个进程尽可能的保持单个输入点,或是单个输出点。
-
所有费时的操作均发到独立的进程,以队列方式处理。
-
按功能和场景划分进程,单一服务和单一场景中不再分离出多个进程做负载均衡。
性能问题上,我是这样考虑的:
我们应该充分利用多核的优势,这会是日后的发展方向。让每个进程要么处理大流量小计算量的工作;要么处理小流量大计算量的工作。这样多个进程放在一台物理机器上可以更加充分的利用机器的资源。
单线程多进程的设计,个人认为更能发挥多核的优势。这是因为没有了锁,每个线程都可以以最大吞吐量工作。增加的负担只是进程间的数据复制,在网游这种复杂逻辑的系统中,一般不会比逻辑计算更早成为瓶颈。如果担心,单线程没有利用多核计算的优势,不妨考虑以下的例子:
计算 a/b+c/d+e/f ,如果我们在一个进程中开三条线程利用三个核同时计算 a/b c/d e/f
固然不错,但它增加了程序设计的复杂度。而换个思路,做成三个进程,第一个只算 a/b 把结果交给第二个进程去算 c/d
于之的和,再交个第三个进程算 e/f
。对于单次运算来算,虽然成本增加了。它需要做额外的进程间通讯复制中间结果。但,如果我们有大量连续的这样的计算要做,整体的吞吐量却增加了。因为在算
某次的 a/b 的时候,前一次的 c/d 可能在另一个核中并行计算着。
具体的设计中,我们只需要把处理数据包的任务切细,适当增加处理流水线的长度,就可以提高整个系统的吞吐量了。由于逻辑操作是单线程的,所以另需要注意的一点是,所有费时的操作都应该转发到独立的进程中异步完成。比如下面会提到的数据存取服务。
对于具体的场景管理是这样做的:
玩
家连接进来后,所有数据包会经过一个叫做位置服务的进程中。这个进程可以区分玩家所在的位置,然后把玩家数据分发到对应的场景服务进程中。这个位置服务同
时还管理玩家间消息的广播。即,单个的场景(逻辑)服务并不关心每个数据包为哪几个玩家所见,而由这个服务将其复制分发。
当玩家切换场景,场景服务器将玩家的数据发送给数据服务,数据服务进程 cache 玩家数据,并将数据写入数据库。然后把玩家的新的场景编号发回位置服务进程,这样位置服务器可以将后续的玩家数据包正确的转发到新的场景服务进程中。
掉落物品和资源生产同样可以统一管理,所以的场景(逻辑)进程都将生产新物件的请求发给物品分配服务,由物品分配服务生产出新物件后通知位置服务器产生新物品。
这样一系列的做法,最终保证了,每个场景服务器都有一个唯一的数据源——位置服务进程。它跟持久化在数据库中的数据无关,跟时钟也无关。由此带来的调试便利是很显著的。
最近,面临诸多进程的设计时,最先面临的一个复杂点在于启动阶段。显然,每个进程都配有一套配置文件指出其它进程的地址并不是一个好主意。而为每个
服务都分配一个子域名在开发期也不太合适。结果我们采取了一个简单的方案:单独开发了一个名字服务器。它的功能类似 DNS
,但是可以让每个进程自由的注册自己的位置,还可以定期汇报自己的当前状态。这样,我们可以方便的用程序查询到需要的服务。名字服务器的协议用的类似
POP3 的文本协议,这让我们可以人手工 telnet 上去查阅。我相信以后我们的维护人员会喜欢这样的设计的。:D
以上,国庆假期结束以来的工作。感谢项目组其他同事的辛勤编码。