« 梦幻西游服务器 IO 的一点优化 | 返回首页 | 网络游戏物品校验系统的设计 »

梦幻西游服务器的优化

在历史工程上修补是件麻烦的事情。

前两天说起梦幻西游服务器的优化。这几天我到广州住下来,打算专门花一周时间搞定这件事。由于以前都是网上聊天,只有坐到一起才能真正理解问题。

目前,梦幻西游,只使用单台机器,最高配置 8 个 CPU ,配置 8G 内存。就算最热闹的服务器,也用不完这些资源(大约只用满了 3 个 CPU ,一半的内存)。核心程序差不多就是 10 年前写的,从大话西游延续至今。这两年一直在享受免费的午餐,随着硬件配置提升,现在单台服务器同时在线容量达到一万两千人。观察服务器回应速度的图表可以发现,目前的问题在于,定期会出现反应迟钝的现象。周期性的,服务器回应时间会超过 1000ms 。查得原因在于那个时候,磁盘 IO 非常拥塞。有定期保存玩家数据的服务对 IO 的占用,以及 SA 做的定期备份数据的脚本占用了大量的 IO 时间。最终造成了机器负荷过重。

IO 负荷过重最终怎样影响到游戏服务的性能,这个暂时不过于深入探讨。我这两天主要是分析以有的系统结构,并想一下改进方案。

其实老的系统并不复杂,代码量也相当之小。相关的服务代码仅仅数千行干净的 C 代码而已。一直没有人动它,因为事关重大,牵扯着数百万用户的数据,以及记费流程。无论设计是好是坏,实现的性能有无问题,都让位于稳定。“历史原因”造成的种种,也只能在闲聊时抱怨一句,如果重新设计,肯定不会这样写了。近两年,我越发的对重构这件事情显的兴趣漠然,为何不这样做,为何不那样? 更多的时候都只是程序员们饭局上的聊资。每个系统一旦编写完成,就充满了种种的遗憾。如果它能用,最大的可能就是它就将一直用下去。一切的新想法,留给下一次吧。

对于已经稳定运行了很多年的陈旧的系统,找到好的方法去改造的意义不大。最重要的是,如何对已有系统影响最小的增加一些东西,提高性能。模块间清晰的划分显得相当重要。服务的独立性也是必要的。现在运行的数据服务和记费以及用户鉴权服务居然放在一个服务程序中恐怕是一个大失误。它使得我们把数据读写剥离出来非常困难。

数据服务采用的是一个 C/S 结构。但没有使用数据库,而是直接使用的本地文件系统。整个设计算是良好,但数据服务本身的机制却很糟糕。C 和 S 之间采用共享内存交换数据,这是为了提高 IPC 性能。C 只有一个,就是游戏主进程,而 S 可以有多个。可以并发的提供服务。多个 S 和 C 之间用管道传输命令,用共享内存交换数据。本意是好的,但协议设计是有问题的。因为 C 直接操控数据区,而有唯一性,结果设计时,把数据区的区块管理放在了 C 上,而不是由 S 提供。

举例来说,如果游戏进程(C) 需要加载一个用户的数据,它自己先寻找数据区中的空位,然后通知 S 把这个用户的数据加载到它指定的数据位置。数据区的清理工作同样是由 C 这边做的。这使得 S 不能直接在数据区上做 Cache ,如果需要 Cache 暂时不用的数据(比如一个玩家离线)就得由 C 自己来做。或者额外的再做一个 Cache 服务(这需要多出一倍的内存,以及内存复制的操作)当初这么实现恐怕是考虑到有多个 S 同时为一个 C 服务的需求,但我只能认为是设计欠佳。

结果就是,整个数据服务,无论是读还是写,都是无 Cache 的。Cache 仅仅依赖 OS 来做。对于当初低一个数量级的时候,这没有问题。但在线人数从千级达到万级后,问题就显露出来了。毕竟你为最终需求最更多的定制,越能充分发挥硬件的性能。


下面记录一下我已经实现好的内存 Key/Value 数据库的设计思路。

要实现前几天想好的,只保存差异信息的策略(经实测,可以减少 90% 的写 IO 操作),必须先统一数据读写服务的位置。不能依赖本地文件系统做数据交换。我之前考察过若干内存数据库,比如 Redis ,最终决定自己实现一个。因为我已经非常了解需求,可以高度定制算法,最大发挥硬件的能力。代码量也不会太大。(控制在 500 行 C 代码之内,最后实际写下来,不过 300 行 C 程序)

我们的需求是这样的:服务程序每周会停机一次。每周总共涉及的玩家数据 10 万组。每组数据 4k 到 32K 之间。都是文本数据。可以看成一个 id 到数据串的 key/value 数据储存服务。经估算,总数据可以全部放入内存。数据会频繁更新,更新后长度会改变。

我花了一天实现这个 k/v 内存数据服务。为了最大利用内存,并同时保证效率,以及代码实现的简洁。我采用预先分配好整块内存的方案,把内存切割成 1K 为单位的区块。并用单向链表串起来。考虑到内存 cache 的命中效率。链表指针本身和数据储存区分离。(大多数时候,我们只需要访问链表指针,而不需要访问具体数据)

链表指针采用序号,而非内存地址。这样即使在 64bit 系统上,依然使用 4 字节索引(可以最大可管理 4T 数据,足够了)。单向链表可以比双向链表节省一半的指针操作以及节约少量内存。代价是代码写起来繁杂一点。

所有内存区块分成两部分:空闲区块和已用区块。一开始全部空间都是空闲。一旦向内放入一段数据,就从空闲链表上摘下够用的区块,放到已用链表的尾部。如果 cache 空间满,则从已用区块链表头部移掉一些空间还给空间区块(这些数据区是长久未访问过的)。每次读取一段数据,都将其调整到已用链表的末尾,保证最后才清理。

另外做一个 hash 表,从 id 映射到在 cache 中区块段的头(由于是单向链表,具体实现时应保存上一个节点)。这样可以用 O(1) 时间查询指定 id 对应的数据区,

保存在 cache 中的数据不必在地址上完全连续,这好比磁盘的分簇管理。和磁盘不同,内存的随机访问性能和顺序访问性能差异更小。这样有利于内存空间利用效率。

Comments

单台服务器同时在线容量达到一万两千人.......
1.是回合制吧?
2.实时触发的设计多吗?比如涉及到心跳,时间定时这些,是触发了之后,进行运算的吧?而不是通过心跳,实时记录和判断吧?

“历史原因”造成的种种,也只能在闲聊时抱怨一句,如果重新设计,肯定不会这样写了。

看到这句话我就笑了。(深有同感)

云风,你好。谢谢你提供这么宝贵的经验。我之前开发的几个项目都是基于多服的,通过一个后端服务器把多个前端服务器连接起来。在这次开发新项目的时候,我一直想做成单服,毕竟这可以大大的降低逻辑程序员的开发难度。但是又忐忑承载人数上会不会上不去,现在我终于下定决心这样做了。但是我的做法可能同你有些不一样。数据库上,我想把mysql换成nosql,所有大区都用一套分布式数据库,这样在日后和服上还有管理上会方便很多。文件系统我是不敢用的,起码运营肯定不肯,而且我也不敢把数据暴露在前端。另一方面,我想把聊天系统提出来,单独做一个聊天服务器,这样服务器的人数上限可以大大增加。最后,如果一切都稳定的话,我期望可以加入P2P传输,包括一些聊天和移动数据。毕竟服务器绝大多数的流量都在这些地方。

hash!!!

对回合制的不太敢兴趣!~~~~~~~~

我最近在把游戏数据从文件存储变为数据库存储,也觉得多余。

怎么判断出只使用了3个CPU呢?

骗子啊。。

不能用html标签啊

<hr>
<p>
some
<br />

<b>some</b>

把database看成是模式,模式当然有着好处,但也不一定模式, 我想“西游”系统说明着朴素的道理,当然普遍场合, database确实是很好,很易于实施的方案

database 只是一个概念. 只要是管理数据的集合就可以看成是 database . 至于用什么实现, 数据在本地文件系统中, 还是在内存,是在云里,还是在集群中. 那是次要的东西.

只要能满足需求足已.

一个游戏产品诞生开始, 大体的程序就定下来了. 后面只是修补工作. 当初为何这样选择,为何那样选择,都是不需要问答案的. 尤其是反问句式更无意义.

梦幻西游的大部分设计,是延续更早的大话西游的东西. 诞生于 10 年前. 放在今天, 当年的开发人员也会坦诚的说, 当初并无 "设计" . 一切都靠经验,在几个月中匆促做出的决定.

为什么不用数据库来作为保存信息的工具,比如WOW就是用ORACLE的集群来做的,WOW同时在线几百万人,东西也多,希望梦幻西游将来的设计程序能达到WOW的等级。

@miosys

cache 中的数据没有必要是连续的。从 cache 中取数据是需要复制数据出来的,而不是直接返回 cache 内的数据地址。(那样也不安全,因为cache 中的数据会被淘汰)

所以,32k 数据的话,cache 中是连续 32k 空间,还是 32 个离散1k 空间没有区别。用链表连起来就好。

“每个系统一旦编写完成,就充满了种种的遗憾。”,呵呵,的确,再怎么花时间设计,时间一长,系统还是会有缺憾,万变的需求啊。

玩家的数据在48K以下,且不定长。而内存分配粒度为1K,那cache中应该如何管理玩家数据呢?是视情况每次向cache申请N个单元么?如果是这样,维护起来会不会有困难?因为cache是基于链表的,一开始能很容易申请到连续内存,但运行一段时间,内存会是离散的,cache如何管理玩家32K的数据呢?

to lin_style:
释放基于这么一种思路:很长时间没有使用过的区域那么接下来用到的概率也很小,因此每次只释放链表最前面的区域。

如果cache中存放一个已经下线用户的信息,那么随着内存的使用,新的区域不断改放到链表尾部,那么这块区域很快会被调整到链表头部,从而在下次内存分配时被其他用户占用。

@max
4字节做索引,一个小块为1KB,
刚好4TB。

上文
"我采用预先分配好整块内存的方案,把内存切割成 1K 为单位的区块。并用单向链表串起来。"


每个小块为1KB.

To max:
云风的意思可能是说4字节的索引,可以管理的数据总量是4T,不是单单索引到4T,索引到4T目前根本没那个必要。

“链表指针采用序号,而非内存地址。这样即使在 64bit 系统上,依然使用 4 字节索引(可以最大可管理 4T 数据,足够了)。”
4字节到不了T的级别

怎么感觉有点麻烦啊

想法不错。内存一次命中,这是我一直以来坚持的标准,特别是在写服务器通信底层的时候

如果游戏应用负载很大的话,在定时同步的时间点可能会产生大量难以消化IO任务。我们说每隔一定时间会将玩家的数据同步到存储系统,但是没有说所有玩家的数据都在同一时间点进行同步。我们有理由相信玩家在职业上的分布是大体相似的,所以先同步和尚,再同步道士,再同步乞丐,最后解决捕快,也不失是一种解决的方法。

云风:
"则从已用区块链表头部移掉一些空间还给空间区块"(这些数据区是长久未访问过的)
请问云风是怎么保证是长久未访问数据里面如果仍然有效的情况。假设现在ID是1,2,3,4,5
。。。n, 其中玩家A对应的1内存块,但是玩家A仍然可能在游戏中,恰好在你移除的时候他下线了,这个时候在使用中的内存块中已经找不到了。而且这种链表应该是不支持随机释放的吧?否则一点都cache不到了。

“链表指针采用序号,而非内存地址”,意思是不是另建一个表,把序号和地址一一对应起来?

EA的FIFA ONLINE用的是MSSQL来储存用户资料。 游戏本身可以用内存数据库,但客户端用SQL还是会更高效省力一点吧.

果然游戏一般都不用数据库啊,以前做过mud游戏改成wap页面的,用pike语言,文本存储

很给力的文章。
“链表指针采用序号,而非内存地址。这样即使在 64bit 系统上,依然使用 4 字节索引(可以最大可管理 4T 数据,足够了)。” ---我在一个项目中也用到了这个技巧。

多谢云风和大家共享自己的知识,第一个留言啊

Post a comment

非这个主题相关的留言请到:留言本