« Go 语言初学实践(3) | 返回首页 | 方便的分享照片 »

梦幻西游服务器 IO 问题

当多核解决了 CPU 运算能力问题,当 64 bit 系统解决了内存不足问题,IO 问题依然让人困扰。

梦幻西游的服务器从更早的产品延续,已经跑了 10 年了。当初只求快速把项目做出来,用最简单的方法做出来,保证稳定可以用,自然遗留了无数问题。逻辑脚本中充斥随手可见的磁盘操作。

最终,当磁盘操作堆积起来,尤其是阻塞方式请求,并行的过程全部都在磁盘访问处串行起来了。固然整个系统的处理能力并没有下降。但用户的反应时间却会变长。

仔细分析了问题后,我发现,系统利用磁盘其实是有两种不同的用途。

一是备份需求,定期需要把数据持久化下来。因为服务器数量很多,硬件故障率几乎是每周一到两次。所以必须保证定期(不长于半小时)的数据备份。避免硬件故障的意外导致长期回档。

二是数据交换的需求。由于游戏逻辑写的很随意,以快速实现的新功能为主。许多脚本中随便读写文件,提供给逻辑需要来使用。这部分甚至许多是阻塞的。整个游戏逻辑进程串行执行逻辑,任何点的阻塞都会导致服务阻塞。这有很大的历史原因在里面,是不可能彻底修改其结构的。

根据系统调用的监测,我们发现,在定期写盘的高峰期段,逻辑进程中偶尔的读文件操作 API 可能用长达 1 到 2 秒读一个较小的文件。据我理解,导致这个现象的产生多是因为硬盘设备上的操作队列被阻塞。有大量未完成的磁盘 IO 工作在进行,导致系统延迟。

理论上,所以真正写到磁盘上的操作都应该被安排到优先级很低,保证所有的读操作可以以最快的速度完成。这样才能让反映速度变快。即,写盘操作,应该先刷新内存 cache ,然后把真正的写操作推迟到 IO 队列尾上。中间有任何的读操作都应该允许插队完成。由于我们的逻辑进程只有一个线程在跑逻辑,而所有的读文件操作都只发生在这个线程里。(其它工作进程都无读操作)整个系统对磁盘的读操作本身是不会竞争。而写操作则几乎是允许无限推迟的。

可惜,我们很难在 os 的底层控制 IO 操作队列的细节。

今天,想了一个变通的方案来解决这个问题:


为服务器配备两块硬盘,或是配置一个硬盘和一个独立网卡做网盘分区。此目的可以让两种需求的磁盘应用分摊到两个独立设备上去。

另外,在系统启动时,配置一个 ram disk 分区。

至此,我们有三个独立的盘,分别看成 R(ram) ,D(data) ,B(backup)

系统启动前,我们首先用脚本保证 D 和 B 的数据一致,以 B 的数据为主。然后把 B 上面较新文件,复制入 R ,但不将 R 填满,留一定空间。

系统启动后,当遇到 load file 的请求时,首先检查 R 中,看是否有需要的文件,如果有则加载返回。若没有,则从 D 中加载,并同时复制文件到 R

当遇到 save file 的请求时,把文件写入 R ,然后在另一线程中,把写入 R 的文件复制一份到 B 。

系统正常停机后,将所有 R 中的文件写到 D 。 如果系统异常停机,则同步 B 到 D 。(即前面所述的启动前工作)


这个方案之所以可以解决问题,在于,系统工作时 D 是 Read Only 的,而 B 是 write only 的。R 则相当于人为的磁盘 cache ,并保证了数据一致性。

write only 的 B 盘,可以在独立线程和独立设备上慢慢工作,尽可能的不影响 Read Only 的 D 盘上的工作。


这里,假设的是 R 在系统工作中空间无限大。但实际上是比较难做到的。必须考虑 R 这个 cache 满的情况。

这种情况应该如下处理:

在写盘时,如果写入 R 的操作失败(通常为空间满),则需要把数据也写入 D 盘。不过这样,D 就不是 read only 了。不过,在我们的实际运行中,每次的写文件操作都是小文件。可以后台跑一个脚本定期检查 R 的空间状态,一旦空间紧缺,则按时间先后淘汰一部分文件,在此空闲时把文件写到 R 中。这个后台脚本可以缓慢进行,清理 cache 。

我们的服务每天凌晨最为空闲。这个时段做一次清理工作,剩下的时间 R 盘留出支撑 24 小时的容量即可。

Comments

这不就是磁盘Cache嘛,还不如加多些内存,整体形成个内存磁盘cache

@tigerblue,单个队列1个Worker是效率最高的(M/M/1 Queue),K个队列M个Worker是效率最低的(M/M/K Queue)。问题是前一种情况会导致单热点,而且分析的时候是假设对一个Queue进行操作是O(1)的。

不错,挺实用。。

在錯誤的基礎上做正確的優化,意義何在?

换到食堂排队问题,增加了并发处理,也让那些紧急请求的人的等待时间也缩短了,不是吗?

除非上层指定,底层是分辨不出优先级高低的。

开额外窗口,也是增加了额外的带宽,而且对于全局的带宽利用率不利,因为重要且急迫的人不多。而如果太多,就说明是带宽不够了。

从一个设备读,是无法并发的。
============================
肯定是可以的,只不过不同的设备效果不同。

比如我有四个线程并发的读一个设备:
- 如果该设备是个单一的设备,比如一个磁盘或磁盘分区,则并发与串行的效果是一样的,甚至有些情况下会变坏(把原来顺序读变成了并发的顺序读)
- 如果该设备是个在RAID级别以上分出来的逻辑设备(Linux中多是LVM分出来的LV),则该逻辑设备是跨多个磁盘的。网易应该有专门的存储服务器吧?这时,并发读就有好处了,因为这些请求所对应的数据存放区有很大可能在不同磁盘上,则并发地读来让多个磁盘全都工作起来,比一个个的读,每次只有一个或很少的磁盘运作,很定吞吐量要大,平均响应时间要短。

从一个设备读,是无法并发的。

串行逻辑改成并行逻辑也是无法实现的。

食堂排队的长度,靠增加带宽固然有效,但阻塞的根源不在于处理带宽不够,而在于突发请求过多。两个窗口固然可以减少一半的队伍,但理论上,我们是做的到零排队时间的不是吗?

换到这个问题上,要解决的是部分重要人士的响应时间,他们想吃饭的时候马上可以吃到。这个可以该进调度算法,也可以靠开专门窗口解决。

比如那个食堂排队问题,多排几个队伍,队伍长度不就缩短几倍吗?
记得春节刚回来,华为食堂就开了一排打饭队列,结果队伍排的N长(幸亏我是去蹭饭吃的,去的早)。第二天马上弄了两个队列,情况就好多了,呵呵。

读操作一般都是阻塞的,因为一般的情况是,我必需拿到数据后,才能进行下面的处理。而写入一般都是异步的,因为我只要把写请求交给了底层,底层给了我返回,我就可以继续下面的处理。如果不是write-through(写穿)模式,底层一般是放进了该层的缓存后就返回给上层,该写请求已经完成了,真正去写到磁盘上,都是延迟一段时间后再进行的,或者脏数据太多到达一定比例了,或者有的脏数据太老了,才让后台的刷写线程再往下发写请求(在linux中是bio),然后bio成功后,把脏数据的脏标记清除,表示是clean的了,磁盘上的数据已经和缓存中的数据一致了。
这么做,是和磁盘的物理特性相关的,随机写性能比顺序写差的多。所以存储控制器会积累一定量的脏页,然后按照顺序去刷写,来达到最大的顺序性,也就获得了最大的写性能。备份,是典型的顺序写,也是存储控制器最喜欢的IO类型,嘿嘿。

另外,在写队列中,强制插入读操作并优先处理,是危险的。如果读请求的扇区与写请求的扇区有重叠,则有可能会读到磁盘中的老数据,而最新的其实在缓存中,但是被人为的放在了读的后面。在存储中,是不会去更改请求的先后顺序的,主要就是顺据一致性的问题。

cloud说的东西在存储中,其实是存在的,而且肯定案要全面深入的多。从描述看,其实原因不在写请求太多,也不在于读请求是阻塞的(本来就是这样),而在读请求没有被并发的处理。

所以,既然读操作的同步属性无法避免,而只有你们的逻辑才会进行读,而且只有一个进程,为什么不多弄几个进程进行读处理呢?保证效果立杆见影,几乎是多加几个,性能就翻几倍。


些许意见,不很清晰,仅供参考。

由于load save的不一致性,会不会潜在道具复制的bug?

加快磁盘速度并没有解决问题,只是改善了问题。除非磁盘和内存一样快,那是永远不可能发生的。总有比磁盘快的内存出现。

这就和前几天讨论的吃饭排队的问题一样。

如果每天都有几个有急事的人(比如赶飞机)需要尽快吃饭,光是改良食堂供应流程加快分发速度是不够的。如果允许这些人一到食堂就可以插队到最前面就可以解决了。

如果食堂很拥挤,插队也做不到。那么就可以安排一个新窗口专门为他们服务。银行的 vip 室就是这么干的。

看来快速磁盘还是有发展的潜力

是否可以这样,因为在游戏中的数据可以分为:
临时的(完全不用存储的,随计算机启动而重置)
定期的(保证服务器在宕机的时候,数据不会丢失太多,或者定期从一个源那里刷新最新的数据情况)
不变的(比如一些配置信息,任务脚本等等,一次读入不需要反复的验证)
针对这三种,我是这样解决的:
临时的不说了,指在本机存在,不变的根绝需求,比如加载最常用的到一个内存区,LRU或者MRU。
定期的数据,我一般采用一块共享内存,读和写是完全分开的,写由逻辑服务器完成,可以随时写入,另外一个进程去控制此共享内存的数据刷新到介质的过程,这个进程可以安排到优先级较低的环节进行。不影响逻辑线程的正常操作。
比如 player的数据,逻辑服务器不连接DB,只和这个共享区打交道,如果共享区没有,则让其他进程去加载这个数据到共享区,然后通知逻辑服务器去取得。逻辑只管里面的写入动作,而存储的进程完全无权修改数据,只做读,定期去写入介质。至于需要定期将数据刷新的需求,为了避免进程锁(这个资源消耗较大)我一般在内存区增加一个数据标签,这个标签是一个版本号,逻辑线程可以根据版本号去取得相应刷新的数据,维护进程去负责定期清理不用的数据。
以上仅仅是个人观点,很想知道你是怎么解决进程间数据同步问题的。

很好的思路
感谢分享

磁盘I/O慢. Facebook 提供了一个叫Flashcache的开源项目, 是一个linux的内核模块, 它可以将一个读写比较快(尤其是随即读写,通常是高速的SSD磁盘)块设备和另一个块设备, 虚拟成一个带缓存的块设备. 可以有效的提高块设备的读写性能(主要是随机读写), 可以看看介绍: http://www.orczhou.com/index.php/2010/09/flachcache-first-view/

ramdisk有很多弊端的。可以用redis等来做缓存啊!

怎么现在你的blog不是全文输出了呢?

今天,想了一个变通的方案来解决这个问题:

这个之后就没有了。

恩,不错的解决方案啊

为什么不用memcached之类的工具做可伸缩的缓存呢?

Post a comment

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