« 跟踪 Component 的修改 | 返回首页

Lua 虚拟机间函数原型共享的改进

在我们的服务器框架 skynet 中,往往会在同一进程启动非常多的 lua 虚拟机,而大多数几乎运行着相同的代码(为不同的用户服务)。

因为 Lua 的代码也是数据,和 Erlang 这种天生设计成多服务的语言不通,Lua 并没有为多虚拟机同时运行做内存上的优化。所以,我在 5 年前给 Lua 制作了一个 patch ,可以把不同虚拟机间的函数原型数据提取出来共享一份,节省了不少内存。

不过,这个方案只能功能函数原型中的字节码以及调试信息,无法共享函数原型用到的常量表。这是因为,字符串常量是一个对象,尤其是短字符串,Lua 在虚拟机内部做了 interning 已加快字符串比较速度,使得它们很难在虚拟机间共享。

导致 Lua 和 Erlang 在处理多虚拟机共享代码这个问题上不同的本质原因,我认为是 Erlang 在语言层面区分了 atom 和 string 。atom 是虚拟机间共享的,维护在一个大的 atom 池中,只增不减。而 Erlang 的代码本身引用都是 atom ,这样就很方便做共享。而 Lua 不分 atom 和 string ,string 是无法承受只增不减策略带来的内存开销的。

为了解决构成 Lua 函数中大量短字符串常量的问题,我后来又给了一个方案 。它模拟了一个类似 Erlang atom 管理的机制,把所有虚拟机间共享的短字符串维护在同一张 hash 表内。为了解决内存无限增长问题,我给这个 hash 表设定了一个阙值上线,让它只在进程启动时期工作。


昨天,同事在分析我们最近的线上服务器时发现,即使使用了代码共享机制,一个刚加载完代码的虚拟机,还是占用了接近 6M 内存。其中,有大约 1.6M 内存是函数原型使用的常量表。如果我们能节省下这块内存,整体可以有很大的收益。(因为我们大约会在一个进程内启动 2000~3000 个这样的虚拟机)

我仔细考虑一下,其实基于过去做的工作,还能做如下的改进:

在共享函数原型的数据结构中,增加常量表项。过去这一项是放在外面的。

常量表中的长字符串其实可以直接用指针引用,而不必拷贝。

当常量表中是短字符串时,尽可能克隆这个字符串,它有可能在全局短字符串表中,这样,指针是唯一的,也可以被直接共享。

当整个常量表都可以被共享时,直接引用共享数据,而不需要额外建立复制表。

不过针对这些改进,还需要修改 gc 的标记和回收阶段的流程。如果函数原型并非共享,就要正常做标记和删除;如果是共享版本,就需要跳过它。

在修改过后,我们非常完美了减少了 1.6M 内存的使用,整个进程总内存开销减少了 4G 左右。


那么为什么不能强制把所有常量表中的短字符串都放到全局表中共享呢?因为在机器罕见的情况下,加载代码之前,虚拟机内已经构建出来了本地的短字符串,并未置入全局表,之后,克隆函数原型阶段,我们就无法使用全局表中的相同字符串。因为那会导致 lua 的 string interning 机制失效。

如果一个函数原型内包含的所有子函数都被共享,那么为何不干脆复用 Lua 的函数原型对象呢?这是因为 Lua 5.3.5 在每个函数原型内还放置了一个闭包指针,用于 Cache 最后创建出来的闭包。这个是和虚拟机相关的。不过在 Lua 5.4 的开发版中,这个 Cache 已经被取消,未来 Lua 5.4 在这方面应该可以做的更好。


有在使用 Skynet 的同学,可以去开发分支上取下代码 ,试一下这次的新改进能否减少你的项目的内存开销。

Comments

@菜鸟浮出水
我明白你说的意思,把玩家当成一个服务是另一种对skynet的应用,同时按照这个思路,场景也可能是一种服务,这样必然是需要成千个虚拟机的。
当然游戏的类型很多,有些确实可能用上面的设计会更好。
但就传统RPG这种类型来说,我不大赞同每个玩家一个服务。一方面是用这种设计内存负担会比较大,刚开始10个G,跑一段时间后,可能会长到20个G甚至更多。另一方面是玩家之间,系统玩法之间的交互要复杂得多。大多数通讯须通过skynet.send, skynet.call来完成,而call这种阻塞行为是滋生BUG最频繁的地方。

@colin 看起来你们的做法并没有什么大的问题,你们区分 gate game db 等服务,按照你们游戏服的量级,你们单个 skynet 进程可能启动的服务在十几个到几十个左右,所以觉得每个lua虚拟机减少1.6M并不重要。有的开发者使用skynet单个进程会开启上百上千个服务,这种情况很常见,即使是做游戏服务器,也可能会出现这种设计,这个时候每个服务减少1.6M就很显著了。我之前遇到过一个 skynet 服务器的设计,单个玩家会独占一个服务,同时上千玩家在线,就会存在上千个服务:)

我们在自己的MMO中使用了另一种设计,就是把一个虚拟机当成“进程”来看看待,大体区别了:Gate,Game,DB等等“进程”。其中玩家,场景,怪物等等,都是Game的对象。
然后按玩法逻辑,把场景切分到多个Game去,玩家跳场景时,有可能在一个Game中,也有可能跳到另一个Game去。
每个虚拟机的内存可以达到上百M的大小,这样就对这个优化不是很敏感了。
我们这种典型的传统MMO,在多Game设计下,还是可以满足要求的,同时在线2000人可以正常跑(当然实际的情形能达到同时在线300人已经非常不错了)。
然后内存方面就优化很多了,正常情况下同时在线300人算,内存只有2G左右。也就是一个物理机可以开好几个服。
上面看到你们的游戏要占用接近20个G吧,这个代价似乎挺大的,不知道是什么类型的游戏。

Post a comment

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