skynet 热更新 lua 代码

skynet开发太多数情况下只是写lua代码,很少用写c,这一定程度上提高了项目的开发效率。lua虽然没有C高效,但开发复杂业务却是非常便捷,而且lua支持热更。skynet有两种方法热更新lua代码,clearcache和inject,文章分别对这两种方法做说明。

clearcache热更新

讲这个前,先说明下skynet代码加载的事情。因为skynet的每个服务都是一个独立的lua虚拟机,对于同一份lua代码,N个服务就要加载lua文件N次,所以,skynet做了优化,代码文件只需要加载一次到内存,其他服务复制这份内存就可以了,省了读取lua文件和解析lua语法的过程。

clearcache 使用很简单,启动skynet,连接到其控制台:

# nc 127.0.0.1 8000
Welcome to skynet console
clearcache
OK

如果不了解skynet控制台,可以参考我的这篇文章[1]。

但clearcache有个不可忽视的问题,每次clearcache后,不管代码有没有用到,skynet不会清理旧的内存。这会导致了多次clearcache后,skynet内存使用会越来越大
这是为什么?因为clearcache后,只有新起的服务会用到新代码,旧的服务还引用着旧代码。而skynet没有做引用GC的复杂逻辑,在旧服务销毁时,没有清理用不到的旧代码。

或许你会很好奇,clearcache 没清的内存到底是啥
这要从skynet代码共享说起,skynet加载lua代码时,对于一个代码文件使用了一个新的vm加载,然后以文件名作为key将代码索引到全局的vm中。这样,当有服务需要代码了,就从全局vm找到代码,复制一份到服务。而clearcache,就是删除这个全局的vm,然后再重建一个。这么做的好处是,执行clearcache后,不影响已有服务的运行。问题是,全局vm删了,这个vm索引的所有代码没有清理,这样,那些加载代码用的vm没做清理。

inject热更新

inject命令相当于注入代码到服务中,原理就是让指定服务执行某个代码文件,通过修改模块及其函数的upvalue,完成对lua模块代码或变量的替换。这个命令我在前面的文章[1]有详细介绍。

inject用法很简单,启动skynet,连接到其控制台:


# nc 127.0.0.1 8000
Welcome to skynet console
list
:00000004       snlua cmaster
:00000005       snlua cslave
:00000007       snlua datacenterd
:00000008       snlua service_mgr
:0000000a       snlua protoloader
:0000000b       snlua console
:0000000c       snlua debug_console 8000
:0000000d       snlua simpledb
OK
inject :0000000d example/inject_simpledb.lua

inject命令的难点是,这个要注入的lua代码该怎么写。
下面直接改写skynet自带的example做说明:
# cat examples/simpledb.lua

local skynet = require "skynet"
require "skynet.manager" 
local db = {}
local command = {}
 
-- 增加了这里
local function test(msg)
        print(msg)
end
-- 增加了这里
function command.do_test(msg)
        test(msg)
end
 
skynet.start(function()
        skynet.dispatch("lua", function(session, address, cmd, ...)
                local f = command[string.upper(cmd)]
                if f then
                        skynet.ret(skynet.pack(f(...)))
                else
                        error(string.format("Unknown command %s", tostring(cmd)))
                end
        end)
        -- 增加了这里
        skynet.fork(function()
                while true do
                        skynet.sleep(100)
                        command.do_test("itest!")
                end
        end)
        skynet.register "SIMPLEDB"
end)

假设以上的 command.do_test 就是我们要热更改掉的函数。那用于inject的lua代码如下:
# cat inject_test.lua


if not _P then
        print("hotfix fail, no _P define")
        return
end
 
print("hotfix begin")
 
-- 用于获取函数变量
local function get_up(f)
        local u = {}
        if not f then
                return u
        end
        local i = 1
        while true do
                local name, value = debug.getupvalue(f, i)
                if name == nil then
                        return u
                end
                u[name] = value
                i = i + 1
        end
        return u
end
 
-- 获取原来的函数地址,及函数变量
local command = _P.lua.command
local upvs = get_up(command.do_test)
local test = upvs.test
 
command.do_test = function(msg)
    test('New ' .. msg)
end
 
print("hotfix end")

启动控制台,执行inject后,就会看到类似下面的skynet的日志:

# ./skynet examples/config
[:00000001] LAUNCH logger 
[:00000002] LAUNCH snlua bootstrap
[:00000003] LAUNCH snlua launcher
[:00000004] LAUNCH snlua cmaster
[:00000005] LAUNCH snlua cslave
[:00000006] LAUNCH harbor 1 16777221
[:00000007] LAUNCH snlua datacenterd
[:00000008] LAUNCH snlua service_mgr
[:00000009] LAUNCH snlua main
[:0000000a] LAUNCH snlua protoloader
[:0000000b] LAUNCH snlua console
[:0000000c] LAUNCH snlua debug_console 8000
[:0000000d] LAUNCH snlua simpledb
[:0000000e] LAUNCH snlua watchdog
[:0000000f] LAUNCH snlua gate
[:0000000f] Listen on 0.0.0.0:8888
Watchdog listen on      8888
[:00000009] KILL self
[:00000002] KILL self
itest!
itest!
itest!
New itest!
New itest!

最后语

通过前面的分析,我们知道了,clearcache和inject两种方法都可以热更代码。clearcache比较简单,但这种方法对于已有的服务是没有效果的,只有在新的服务才生效。而inject可以热更已有的服务,但不管是inject脚本的编写,还是inject命令的执行,都相对比较繁琐。所以要根据实际的需求,选择适合的方法热更lua代码。

好了,文章到这里就结束了。最后,还是那句话,有问题,欢迎反馈。

发表评论

电子邮件地址不会被公开。 必填项已用*标注