当多核解决了 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 小时的容量即可。