今天有同事问到,能不能不关闭 skynet 进程,直接重新加载一个 lua 服务。
简单的回答的不能。如果要详细回答,并非完全不行,但这个需求需要使用 skynet 的人自己定制出来。
其实,这涉及服务的热更新问题。由于 lua 的函数是 first class 对象,所以,它即好做热更新,又无法做成业务完全无关的。
容易做的一方面在于,lua 函数本身就是一个对象,只要你找到 lua vm 里的需要更新的函数对象被哪些地方应用,就可以生成(通过 load 新的脚本代码)新对象,取代值前的引用即可。
不容易做的一方面是,lua 完全不区分什么是代码,什么是数据,所以没有 C 语言中所谓符号表。所以并没有统一的地方可以查找 lua vm 里已有的函数。甚至函数也没有名字,你不能用名字索引出新版本的函数,替换掉老版本的。除非你的业务框架做了适当的约定。
另外,在 lua 中,所有函数都是闭包。如果你只想替换闭包中函数原型的实现,那么还需要做 upvalue 的重新关联。这是一个繁杂的过程,如果没有适当的约定,也无法彻底做对。btw, snax 里就做了一些约定,可以一定程度的做到热更新。
那么,直接把旧服务杀掉,启动一个新服务行不行?
如果服务是无状态的,或是你可以在停止服务前把状态保存再安全的地方,新服务启动后可以恢复状态的话,这是个不错的思路。
但是,对 skynet 框架而言,还有一个障碍:新的服务的地址和旧服务不同。
早期的 skynet 版本的 lua 服务是支持就地重启的,也就是并不关闭当前的服务,只是把 lua vm 关闭掉,然后重新开启一份新 lua vm 。这个特性从来没有人用过,所以我后来从 skynet 中移除了,这样可以简化实现。如果你有兴趣,可以从 github 的历史中找回来。
今天,同事又重提此事,希望还是可以支持这个特性,而因此引起的一些问题,他再另行解决。
他需要这个特性是因为不想每次修改一个服务的实现就重新启动整个系统,非常影响开发效率。我想了想,如果只是开发需要,未必需要修改 skynet 底层,使用一个额外的独立库也可以做到。
我便实现了这个东西:
https://github.com/cloudwu/skynet-reload
这个库只有一个 api ,在 require "reload" 后,返回一个函数。只要你在需要重启的服务内调用它(并传入启动参数),服务便会开启一个新的 lua vm 重新加载服务的脚本,顶替原有的服务。
这里有一个 test 程序做示范。它会初始化 echo_reload
服务,并不断向其发送消息。正常情况下, echo_reload
服务会延迟 1 秒后返回。但 echo_reload
服务自身在启动 4.5 秒后,会调用 reload 重启。
在演示中,你会看到,前四次 echo 请求都会成功,而第 5 次请求因为服务重启而异常;接下来的请求又能成功。
这里有几个小问题需要注意:
服务重启时,如果有对外的请求尚未回应(比如示例中的最后一次时钟请求),重启后的服务将收到这个回应。但由于没有对应的处理协程,而会抛出异常。这点是有可能改进的,比如在重启时,记录下发出的请求,记录下来,传递到新版服务中让其忽略。不过我觉得实现它的意义不大。
重启服务后,老版的 vm 并不会立刻关闭。而是要等再重启一次时,才会将上个版本关掉;且第一个版本的 vm 只能等服务推出时才会销毁。这会浪费一些内存,但如果只是开发期使用,不会有太大的影响。这样实现是为了简单,并不改变 skynet 原有的底层代码。
老版本的服务在销毁时,会触发一些 skynet 实现的 gc 方法。比如在 socket channel 模块中,gc 方法会关闭一些 socket fd ,这未必是你想见到的,需要留意。
那么如何让一个已经在运行的服务执行 reload 方法呢?
除了你预先留好控制指令外,还可以利用 debug console 的 inject 命令。
如果让重启后的服务获取到老版本 VM 中的状态数据呢?
老版本的 VM 并没有立刻销毁,所以理论上你可以取到所有的数据。不过目前这个库并没有给出接口,如果你有这个需要,欢迎继续完善它 :) 。