基于 lua 的热更新系统设计要点
很久没写 blog 了,主要是忙。项目排的很紧,一个小项目上线,发现不少问题,所以把多余精力都投进去了。最后人手不够,亲自上场写代码。在不动大体的情况下,最大能力的修改了一些设计,并把能重写的代码重新写过了。
这一周,写了三天半程序(其中通宵了一次)。平均每天写了千多行程序。基本上把原来项目里用的几万行东西替换下来了。重构总是能比老的设计更好,不是么? :) 不过这事不能老干,个人精力再充沛。故而还是找到称心的合作人比较好,也不能啥都自己做啊。程序员的效率差别可不只十倍二十倍的差别这么少。btw, 前段时间通过这里找到的新同事不错,呵呵。如果有缘,还想再找一个能干的。我相信,聪明人在一起做事可以获得更多快乐。
闲话扯到这里,总结下这两天做项目的一点经验。那就是:当我们需要一个 7*24 小时工作的系统时,实现热更新需要注意的要点。当然,这次我用的 lua 做实现,相信别的动态语言也差不多。只是我对 lua 最为熟悉,可以快速开发。
热更新对于游戏这种需求多变,且需要和用户保持常连接的程序是比较重要的。这样你可以在设计人员不断提出新的需求时,可以在不间断对用户的服务的基础上更新系统。当然快速修补 bug 也是个很重要的用途。
做这样的系统,关键是各种服务要拆分开。使用多进程的设计尤为重要。当的的系统中的各个模块以独立进程的子系统形式出现时,我们只需要把子系统隐藏在连接服务器后,不跟玩家直接通讯。大多数子系统都可以轻易设计成可以动态装卸的。需要更新的时候只需要重新启动一下即可。
这里,数据服务分离也是很重要的。绝大部分服务不应持有太多游戏数据,尽量减少重起服务器中内存数据持久化的带来的程序设计复杂度。
另外,我们需要在后台留下一个管理器的服务,可以通过管理程序向运行着的系统发送指令并得到反馈。从前,我经历的项目都是别的同事在做。可能许多人受 mud 影响太重,倾向于让服务器对一些帐号授予特权,让特权用户可以从普通客户端上以发送聊天信息的形式发送控制指令。这次我自己做,没有采用这种形式,而是在系统内部开放一个管理端口,可以连接管理服务上来监控系统的运作。
以上这些,这两年我在 blog 上已写的太多,就不再展开写了。
让我们关注一下 lua 。这次是我第一次实战 lua 真正实现一个可以热更新的系统。虽然核心部分并不多(大多数服务已经分离成独立进程的子系统了),但是也有几千行代码的规模。管理这些代码,包括他们的更新过程,必须有一些关键点要留意。否则一不小心,运行者的系统多自我更新几次,就会在犄角旮旯遗留一些不容易发现的问题。轻则内存泄露,重则功能不稳定。
首先,lua 的 module 机制,require 会阻止你重新加载相同的模块。这个很简单,在你要更新系统的时候,卸载掉自己编写的模块。方法是:把 package.loaded 里对应模块名下设置为 nil (这样可以保证下次 require 重新加载一次)并把全局表中的对应的模块表置 nil (这样可以避免重加载后留下垃圾)
其次,更新时要保护后内存中的非代码数据。这个时候,对 local 变量的使用务必小心。因为 local 变量总会被作为 upvalue 绑定在 closure 里。我们的代码经常会依赖这些 local 变量。在更新后,许多保存数据用的 local 变量会生成新的一份。这很可能丢失重要数据。而因为这个问题回避使用 local 也是不合适的。要知道 local 和 global 变量的性能可不只差上一点半点。
我采用的方法是,把数据记录在专用的全局表下,并用 local 去引用它。初始化这些数据的时候,首先应该检查他们是否被初始化过了。这样来保证数据不被更新过程重置。
如果要在 lua 中模拟出类机制,一般会使用 metatable 来保存类似 C++ 中的虚表。我们务必要关注这些 metatable 有没有被正确更新。在构造类的时候,生成一张临时的表记录成员函数的话,那么在更新系统工作时,就应该清空老的虚表,再其上重新填写新版的成员函数。而不应该另外生成一张。否则会导致旧的对象的方法无法被刷新,并和后来生成的对象行为不一致。
在做这套系统时,我还遇到一个有趣的 bug :有时候,我们想简单改写一个对象的行为,重载它的一个方法。因为原方法是放在 metatable 的 index 表中,那么重载的方法直接生成 closure 置入对象即可。可有时候,我在新实现的方法最后还要调用老的方法。
比如,我想对 a 对象的 OnConnect 的行为做一些扩展,可能就会这样写:
local OnConnect=a.OnConnect function a:OnConnect(...) dosomething(...) self:OnConnect(...) end
这样先把老的 OnConnect 行为记录在 local 变量中, 重载它,并允许调用老的方法。
在没有自我更新的情况下,这样做没有问题。可一旦代码允许自我更新,它就可能引起多次嵌套。而正确的方式,应该在 local OnConnect=a.OnConnect 前加上一句 a.OnConnect=nil 。
实际使用中,每次都这样写比较繁琐也容易漏掉。最后我写了个简单的函数封装这个过程。btw, lua 用多了,越来越感觉它更偏向于函数式语言,而非 C 这样的过程式语言。怪不得很多人说 lua 跟 Scheme 的血缘关系比之跟 C 更近一些呢。
最后,要做热更新,最好是整个程序一起做,而不要只更新局部的函数或协议。这样可以使更新后的系统更稳定。
先胡乱记录这些,等有空了再重新整理一下思路。
Comments
Posted by: 涵曦 | (16) January 7, 2019 04:39 PM
Posted by: 金庆 | (15) July 3, 2016 08:46 PM
Posted by: asqbtcupid | (14) February 3, 2016 01:20 PM
Posted by: LJ | (13) April 30, 2015 10:46 PM
Posted by: LJ | (12) April 30, 2015 10:46 PM
Posted by: gauss_t | (11) September 14, 2012 04:22 PM
Posted by: xiaolige | (10) April 14, 2008 07:16 PM
Posted by: 风舞影天 | (9) March 26, 2008 01:44 PM
Posted by: HeavySword | (8) March 19, 2008 11:38 PM
Posted by: heighgun | (7) March 19, 2008 04:25 PM
Posted by: 开心 | (6) March 17, 2008 12:17 PM
Posted by: kingliang123 | (5) March 17, 2008 07:33 AM
Posted by: shunwei | (4) March 16, 2008 07:52 PM
Posted by: GUFAN | (3) March 15, 2008 07:08 PM
Posted by: Cloud | (2) March 15, 2008 10:58 AM
Posted by: kasicass | (1) March 15, 2008 07:58 AM