一种以动态库的方式使用资源表的方案

这段时间研究了一下资源表的优化方案,算是有了一些成果,在此记录下来。

先交代一下背景吧:我们的服务器把资源表放在共享内存上。这么做的原因主要是,进程core掉后再拉起时不需要重新再构建一遍资源表(构建资源表主要就是构建索引查询的数据结构,比如构建一个哈希表用于根据HeroID查询英雄配置这种)。然后,考虑到同一个机器上可能部署多个进程,于是自然就想到,能否有一种机制能够让一个机器上的多个进程共享同一块资源表的内存?

一个比较直观的想法就是,让多个进程挂在同一块资源表共享内存。这样做确实是可以达到内存共享的目的,但是需要考虑资源表reload的情况。reload时会对这块内存做写操作,而我们知道,一写多读是会有并发问题。因此这种方案还需要再引入一种机制来处理并发问题。常见的比如是双缓冲区,构建好后再一次性切换到新的资源表内存上。了解到了一些项目组确实也是这么做的,具体就不细说了。

而我想到的方法是这样的:使用代码生成技术,将资源表完全的导成c++的代码。所有用到于查询的数据结构都是通过自动代码生成的,并且在编译期就被静态构建好了。进程运行时直接载入这块内存即可,不需要像以前那样在起服时跑资源表构建的流程。因此,这块内存也不需要再手动放共享内存上了。core之后再拉起进程,最多就是资源表的内存被卸载了,并且会到下次用到时通过触发缺页中断被再次载入进内存。为了做同一机器上的内存共享,可以把资源表打包成一个动态库。于是共享内存的这块工作自然而然就推给了操作系统去做。

这种做法的优点有几个。而其中我觉得最重要的是,这是一种一次性解决问题的做法(这也是我在前一篇博客中所提到的:P)。基本上资源表的大部分代码都被以是自动生成。并且当某个进程要使用某些资源表时,直接加载这个动态库即可。甚至,加载动态库的代码都可以写在框架层,默认为每个进程都加载资源表。在以前我们在其它进程(除了gamesvr之外的其它进程,如场景进程)中如果要使用资源表是很麻烦的:要在一个指定的文件中定义要使用到了哪些资源表,并且如果资源表之间有依赖关系,也需要把依赖的资源表也配置进来。而现在,我们可以为所有进程都载入资源表,并且还不需要担心带来内存增长的风险。

其次,不需要再为资源表的内存预估一个上限的经验值。比如说定义了一个长度为1000的数组来存装备配置,当策划同学配了超过1000个装备时,就要手动修改这代码修改这个经验值(因为之前是把资源表放共享内存,因此需要内存预分配)。因为资源表都是被静态构建的,数组大小完全可以通过自动代码生成来做。再者,也不需要自己手写建索引的代码,全部都通过工具自动生成。比如技能配置表可能会有根据ID和Level查询的需求,现在只需要在导表时指定主键,便可以自动的生成这些索引的代码。

静态构建数据结构还有一个优势是,理论上可以使用一种更优的内存结构来存储。一般来说,资源表内存再载入后就不会再作修改了,针对这种场景我们可以设计一种比哈希表更快、更省内存的数据结构来做索引。比如通过minima perfect hash方式为每个主键生成不会冲突的哈希函数。并且由于不会动态扩容,因此也不需要使用稀疏的哈希表来存储(一个紧凑的数组即可)。基本思路就是把计算由运行期推到编译期,甚至可以是推到生成代码的时候。

还有就是reload过程非常简单,卸载旧的动态库加载新的动态库即可。由于资源表不放共享内存,因此也不需要之前那样为新增的内容预留一些内存空间,也不需要考虑共享内存上的结构体兼容性问题。

最后一点就是实现非常简单。所有代码加起来还不足1000行,包括了一个用python写的代码静态代码生成工具,和c++的资源表管理器代码等。

 

在实现过程中也遇到了几个有意思的问题,在这里也一并记录一下。

(1)首先一点,linux动态库是只会把.text段的内存共享。而我们这里要共享的主要是数据,怎么办呢?首先要明白的是,动态库之所以不共享.data段的内存,是因为这部分数据可能在被载入进程后修改(包括可写的数据、以及重定位表等结构),因此必须是每个进程维护一份私有的内存。但我们这里没有修改数据的情况,因此是完全可以被共享内存的。解决方法很简单:用const来修饰资源表数组即可。这样做这块内存会被放到.rodata中。并且在链接阶段,linker会合并所有.rodata到.text段。

P.S. 通过readelf -l 命令查看elf文件的program header就能看到哪些section是被合并到.text段,哪些被合并到.data段

 

(2)另外,还有一个问题就是关于如何热更新一个动态库。之前我一直以为直接把目录下的库的文件替换一下是没问题的(由于有文件的引用计数)。但是老大指出,在linux下用cp命令覆盖实际上是会修改原来文件的内容的,并不是等价于在目录下删掉原来的目录项,再创建一个新的目录项指向新文件内容。原理就是覆盖时cp会修改原来文件的inode,并不是创建一个新的inode(详情可以看这里)。不过我感觉此处更像是cp命令的一个bug:在拷贝时应该考虑是否有进程正在引用原来的文件,如果有则应该考虑创建一个新的inode,而不是复写原来的inode(类似于写时拷贝)。不过这个问题感觉可以再深入研究一下,是不是有其它什么顾虑?

 

posted @ 2019-09-18 23:25  adinosaur  阅读(137)  评论(0编辑  收藏