4

给silly增加热更新

 2 years ago
source link: https://blog.gotocoding.com/archives/797
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

给silly增加热更新

最新抽了点时间给silly增加了一个silly.patch模块,用于对热更新提供一些有限的支持。

热更新最麻烦之处莫过于“数据迁移”, 即怎么使新函数(要更新的函数)以“运行时数据”的状态运行。

其实http这类无状态协议是最为简单的,因为他们不需要“数据迁移”的过程。http的这种架构,使得所有的函数都是无副作用的,所有的数据在请求结束给出Response的同时, 数据就已经存入了数据库。当需要热更新函数时,根本就不需要考虑数据的问题,直接替换就可以完美解决。

与此相对的是,通常的服务器或应用程序都会有“非局部变量”的存在(所有生命周期不是在函数被call时建立,ret时销毁的变量都可以认为是非局部变量,比如lua中的全局变量或上值)。

对于这类程序,在热更新时,就必须要小心处理这类数据,使热更新之后的函数安全的以“运行时数据”所代表的状态运行。当涉及数据结构或功能变化较大时,这种“数据迁移”的安全性很难面面俱道,也很难提出一个通用的解决方案。

再考虑一下解bug的场景,大多数情况下,bug可能只会出现在某几行代码或某几个函数之中,一般会延续之前的设计而不太会有大量数据结构或大量代码的改变。

在这种情况下,热更新实现的复杂度就可以降低不止一个数量级。在实现上也可以有更好的保证。

因此,这次新增的silly.patch模块也仅仅对热更新bugfix做了一些支持。


silly.patch模块只提供一个功能,即将a函数中的所有“非局部变量” patch 到b函数中去,以便b函数以a当前的运行时状态继续运行。

借助luaVM提供了一组调试函数,使得我们可以方便的对a,b两个函数进行“数据迁移”。

比如,使用debug.getinfo来遍历出a,b两个函数的所有上值,然后使用debug.getuservalue和debug.upvaluejoin将b函数的所有上值均引用至a函数的上值。这样就可以使逻辑以b函数的代码以a函数的数据去运行。

但是这里面有些问题需要处理.

a,b两个函数必然不会相同(不然也不会去更新了),那也就不能保证a函数的第一个上值意义与b函数的第一个上值意义完全相同。比如下面代码:

local foo = "hello"
local bar = "world"
local function f1()
print(foo)
print(bar)
end
local function f2()
print(bar)
print(foo)
end

如果a函数是f1, b函数是f2。f1的第二个上值是foo(因为使用了print,所以第一个上值是_ENV),而f2的第二个上值是bar。如果按上值的id去patch,上述代码就会出现很诡异的bug。

因此silly.patch模块做了一个简单的约定,如果要拿b函数去修复a函数,就必须保证a函数中使用的所有上值在b函数中必须不得改变其意义。

有了这个约定,silly.patch就可以根据“非局部变量”的名字去进行“数据迁移”。 比如f1使用了上值foo, f2中只要foo的意义不变,不管他属于f2的第几个上值,都可以保证“数据迁移”的正确性。

还需要注意的是,如果a和b函数的上值变量是函数时,需要递归对其上值函数进行“数据迁移”。


silly.patch仅仅是对热更新做了支持,他并不是一个完整的热更新模块。还需要对silly.patch进行一定的封装才可以使用。

在进行封装时一般的步骤为,生成新函数b,找到等修复函数a, 执行silly.patch, 将a的数据迁移到b函数上, 然后使用b函数替换为a函数。

生成新函数b,一般是通过load/loadfile来生成一个chunk, 然后调用chunk来生成.

为了避免在调用chunk函数时有副作用,一般在调用load/loadfile时,会传入一个新的_ENV表作来将chunk置于沙盒之中,如果有顾虑新的函数b会使用新的全局变量(即函数a从没使用过的全局变量),可以在整个热更新的最后, 将load/loadfile时传入的_ENV表有选择性的合并运行时环境中(只合并运行时环境不存在的变量)。

比较麻烦的是怎么找到要修复的函数并进行替换,lua中提供的debug接口中并不能获取一个chunk中的所有函数。

当然就算提供了这样一个接口也很难使用。在lua中,function是first class, 这就意味着当你定义两个变量指向同一个函数时,这个函数就拥有了两个名字。

因此在我们约定不给可能会热更新的函数起别名的情况下,有两种实现方式。

一种不太通用的实现是,在每个chunk中实现两个函数,一个函数提供通过名字对chunk中的任意函数进行定位,一个函数提供通过名字对chunk中任意函数进行替换。这种方法比较麻烦,而且容易出错。

另一种方式依赖于一个事实,一般每一个lua模块都会导出一些接口函数供其他函数使用,那么就从这些接口上做文章。

比如我们想要将module模块中a函数热更新为b函数,我们可以直接require “module”得到module模块的接口函数表,然后再根据名字定位到相应的函数,如果替换则直接将module模块函数表中的相应字段重新赋值就可以了。

如果想要热更module中某个local函数,就比较很麻烦,但是也可以办到。在上文silly.patch的实现中可以得知,如果一个函数有上值,而且上值是一个函数的情况下,silly.patch同样会对上值函数进行“数据迁移”,这也意味着,上值函数同时也会被热更新到最新。所以,在需要热更新某个local函数时,可以通过热更调用他的模块接口函数来实现。

但是需要注意的是,这里有一个坑。如果函数module.a1和函数module.a2同时引用了module中的一个名为foo的局部函数。如果只热更module.a1的话,module.a2将依然会使用旧的foo函数。

基于通用考虑,在silly的console模块提供了一个patch命令, 这个命令正是基于方式2来实现的。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK