« November 2010 | Main | January 2011 »

December 24, 2010

梦幻西游服务器 IO 的一点优化

关注梦幻西游服务器的性能问题,是源于前几天跟同事的聊天。谈到能否把梦幻西游服务器做成无盘站,或是放进虚拟机里,便于日常维护管理。

意外的了解到,现在磁盘 IO 性能居然成了梦幻西游服务器的瓶颈。而不是 CPU 或是网络带宽。据我所知,梦幻西游的服务器数据储存是这样做的:

主游戏进程不负责储存,一切都在内存中。所有玩家的数据就是内存数据结构。只是在玩家登陆的时候去读取一下本地的文本文件,以及登出的时候把数据序列化成文本,然后保存在本地文件中。

为了防止中途发生意外,游戏进程会定期把内存全部数据序列化,然后通过共享内存的方式让另一个 IO 进程不断的把数据保存在磁盘上。

这些都是 10 年前做的设计决策,无论是否合理,都已经稳定运行了很多年了。不少朋友问起,我们的游戏服务用的什么数据库系统,我都只好说,我们没有用数据库,只用了文件系统。面对诧异的目光,我都不想过多解释。好吧,其实我也觉得 SQL 神马的都是浮云。

目前在 8 千人以上同时在线的服务器上,磁盘 IO 非常繁忙,据说已经影响到了正常的游戏。由于长年的修修补补,整个系统已经不是上面提到的那些单纯。还有一些额外的 IO 操作,这些被定期写盘的 IO 操作影响到了。


直觉告诉我,这个环节做优化比较容易,事半功倍。之所以之前没有人做,只是因为很少人愿意去碰那些已经看起来稳定运行了很多年的系统,通过改善硬件就可以缓解的问题。

方法其实很简单。只需要简单部署一个内存 cache ,把所有需要读取的数据都在 cache 里存放起来,而不直接去读文件。这个东西可以找现成的方案,也可以自己写一个(不会太难)。之所以自己来写这个 cache ,是为了下一步方便。

接下来就是,每次定时存盘时,不在保存全部的数据,而只保存跟上一次数据的差异。简单说,就是先做一个 diff ,再保存。定时存盘仅仅是为了危机处理,只要信息都在,其实真没必要保存每个时间点的快照的。这是个通用的方法。找不到合适的通用工具的原因之一在于传统的 diff 软件是针对文本行的。而二进制 diff 的开源方案较少,且算法更复杂一些。

梦幻西游玩家持久化数据虽然是文本的,但没有特别的格式规范,明显带有多年演化的痕迹。有的文本行长达数千字节,简单的基于文本行的 diff 处理,效果不好。

而且总数据量较大。每个玩家的数据在 48K 以下。玩家数量级在 1 万左右。这样,每批数据量在百兆级。如果用独立工具,数据传递本身的开销就比较大了。定制一个服务来处理这个事情代价要小的多。以梦幻西游这个每月给网易带来上亿收入的产品来说。开发这种小程序的成本几乎可以忽略不计,需要的只是稳定可靠。

这两天我简单写了几百行 C 程序,实现了个简单的 diff ,没有怎么优化,只是把传统的 diff 中的回车分段改成了更多的可定义的分割符。把每个玩家几十 K 的文本数据块,分割成了 2000 来个数据元,用传统 diff 算法处理。

性能还可以接受,一秒可以处理 20 组玩家数据。玩家平均游戏时间半小时间的 diff 量是总数据量的 10% 左右。现在我们标准配置 8 核的服务器,通常都会闲置几个 CPU 出来,正好用来计算 diff 。减少 90% 的磁盘 IO 量(如果加上压缩,将更客观),优化后的效果将非常明显。

December 22, 2010

12 月二三事

这个月做了不少杂事。记个流水帐总结一下。

梦幻西游的 Client 需要优化,我之前提了个方案。对 2d engine 的底层库做了些小修改后,大部分工作都是同事在做。最后完成后,说是性能提升到了可以接受的程度。

我抽了好几天时间研究 XMPP 协议。这个前两年看过,不过这几年开源界内容增加很快。因为对 lua 有偏爱,所以着重阅读了 Prosody 的源码。设计的不错,很容易读,量也不大。

因为对有道词典Chrome 插件的一个 Bug 有所抱怨。认识了做维护的同学。我把我自己对 bug 的 patch 提交了。就是个 js 的小程序,读起来也就是几分钟的事儿。嗯,在听取意见这件事儿上,有道的同学的态度比泡泡组的同学好多了。

我们一合计,就合计到另件事儿上了。想做个 XMPP 聊天群。当然了,想法早有,就是没实施罢了。花了几天,用了 prosody 和 xmppy 把个机器人整了出来,可以群聊了。不过这兴趣后来也就过了。

前几天在魔都参加 SD 大会,饭桌上跟钱宏武同学聊,居然他做个类似的项目已经正儿八经的做了快两年了。好吧,我只是一时热情,证明是可行的就算了。

前几天一直在折腾 codingnow.com 上最后一点数据迁移的事情。主要是天朝衙门催得紧,要本人去帝都登记核实备案信息。我懒的去。三番五次被警告,说可能会封了我在北京联通机房的 ip 。我就把一些个数据向 linode 移。反正迟早都要移过去的啦。在国内总不安心。

就这事突然联想到虚拟机的事情。跟同事闲扯就扯到虚拟机上面来。听说 wow 都是用无盘站的了。我觉得梦幻西游最终要这么干也不错。打听了一下,现在梦幻西游高负载的机器的瓶颈居然是磁盘 IO 而不是网络或是 CPU 神马的。

我万分的觉得这不太正常。然后跟梦幻服务器开发的同学仔细了解了一下。感觉这里面优化的余地很大。好吧,这个事情接下来可以专门写一篇 Blog 。过完元旦我就专心做这个优化了。

这两天在看 Redis ,跟上面说的梦幻西游服务器优化的事情无关。梦幻西游服务器是没有用任何数据库的,不管是 SQL 还是 NoSQL 。也没有分布式数据储存。就是用的本地文件系统。看这个纯粹是兴趣。因为我发现这个东西简直就是我前两年闭门造车设计实现的一个东西的翻版。这引得我极大的好奇。后来看的东西还有 zeromq ,我记得前年也有同学跟我提起过,当时没在意,今天在看来的确不错。尤其是官方文档里演示 video 居然用的 lua ,那个感动啊。


记点技术无关的东西。

我买了一套基于 BattleLore 规则的战棋,中文版的《维斯特洛》。花了好几个小时才把模型粘好。玩了两局感觉很不错。下面贴在图 show 一下:

2010-12-15.22.28.24.jpg

希望过年前还有人陪我玩。有兴趣的同学可以来我的小店。我负责教规则啦。

以前基本没玩过 TCG ,包括传说中的魔法风云会。这次选了一个坑跳。打算尝试一下 WOW TCG 。现在组了套小德,但愁没有对手 :(

元旦前想办一次银河竞逐的比赛。1v1 和 4P ,都按双败淘汰制,现在已经凑到 7 个人参加了,争取到 8 人或 16 人开赛。

还有,感谢给我发机器人资料的同学,过完年可能抽点精力玩玩 :)

December 21, 2010

想要这么一个网盘

DropBox 已经满好用了。但是还是需要安装 client 的。能不能做一个不需要安装额外软件,且不依赖特定操作系统的硬件网盘呢?

长的像一个 U 盘即可。内置 wifi 。差在电脑上就自动连接网络服务。里面自己带一些 RAM 做数据 cache 。用户的鉴权分成两个:一是 wifi 的鉴权,二是网盘服务的鉴权。

最简单的方法就是模拟出一个磁盘来,让用户把鉴权信息写在上面。比如把 wifi 的 SSID 和密码。

网盘服务的鉴权还可以有另一个方法。就是在网站上由用户选择跟这个硬件设备绑定。(绑定时,从服务器传输 key 到设备上)

如果连不到网络,也可以做一个 RAM 盘用用的。

December 14, 2010

lua cothread

前段时间在玩 Go ,非常喜欢 goroutine 的编程模型。采用 chan 进行 thread 间的通讯写起来很舒适。今天花了一个下午,为 lua 写了一个简单的库,模拟这种编程方式。暂且把这个东西叫作 lua cothread 。它基于 lua 的 coroutine ,只是写了个简单的调度器。

这个库有如下几个 api :

cothread.run(f,...)

启动一个函数 f ,放在调度器中。... 会被传入这个函数。这类似于 Go 的 go 关键字。

cothread.resume()

运行一个 tick 。这个库不同于 Go 的多线程模型。它是按 tick 一步步运行的。这适合于嵌入别的框架中。由框架调用一次 resume ,所有被 cothread 调度器管理的 thread 都运行一个 tick 。

resume 函数会返回当前活跃的 thread 数量。

cothread.sleep(ti)

当前 thread 休眠 ti 个 tick 。ti 可以为 0 ,但这可能会使得 resume 一直运行下去。

cothread.chan()

创建一个 chan (消息通道),不同于 Go ,这段玩具代码只有阻塞模式。当向 chan 写入时,thread 会阻塞到有另一个 thread 从这个 chan 中读取数据,或是把 chan 关闭。反之读操作亦然。

chan:read()

从 chan 中阻塞读取一个数据。如果 chan 被关闭,则返回 nil 。

chan:write()

向 chan 阻塞写一个数据,并返回 true 。如果 chan 被关闭,函数会返回 nil 。

cothread.select(tbl)

类似于 Go 的 select 。可以传入一张表。表中任何一项 chan 有数据可读时,便会触发对应的分支。表项可以填入 default 项,当没有任何备选数据时,将执行 default 项。若无 default 分支,select 将阻塞等待第一次的 chan 输入。具体使用见示例。

代码和示例可以看这里