首先提出个人观点:热更新不应该作为常规的版本更新手段,只应该作为bug fix的一种临时措施。
skynet实现热更新有以下方式:
upvalue其实是lua中的一种隐式类型,例:
local a = {}
function foo()
return a
enda是foo的一个upvalue,(个人理解,upvalue更像是一个get函数指向的instance)。
lua提供了以下函数来修改upvalue:
debug.upvalueiddebug.upvaluejoin
skynet改写了lua的辅助API(luaL_loadfilex),代码缓存改为永久保存机制:
一旦加载过一份脚本,那么到进程结束前,它占据的内存永远不会释放(也不会被加载多次)。
skynet提供了以下lua接口用来处理缓存:
-
clearlocal cache = require "skynet.codecache" cache.clear()
新建一个新的缓存,不会释放旧的缓存(注意:它的内存是不断增长的,因为没有释放老的内存,慎用这个函数)。
-
modecache.mode(mode)
-
mode当mode为空时,返回当前的mode,默认为"ON"mode 说明 ON (默认)缓存加载的lua代码文件的 OFF 禁止lua代码文件复用,即使在别的服务中曾经加载过同名文件 EXIST - 如果自己或其他服务或加载过同名文件,复用之前的lua代码文件拷贝;
- 如果是新加载的文件则不进行缓存
修改缓存模式
-
3rd/lua/lauxlib.c
#define CACHE_OFF 0 // 缓存关闭(直接加载)
#define CACHE_EXIST 1 // 缓存已存在(更新旧缓存)
#define CACHE_ON 2 // 缓存开启(替换虚拟机)
// 全局静态变量,定义了缓存模式
static int cache_key = 0;
// 设置缓存模式
static int cache_mode(lua_State *L) {
static const char * lst[] = {
"OFF",
"EXIST",
"ON",
NULL,
};
if (lua_isnoneornil(L,1)) {
int t = lua_rawgetp(L, LUA_REGISTRYINDEX, &cache_key); // 读取lua的缓存模式定义值
int r = lua_tointeger(L, -1);
if (t == LUA_TNUMBER) {
if (r < 0 || r >= CACHE_ON) {
r = CACHE_ON;
}
} else {
r = CACHE_ON;
}
lua_pushstring(L, lst[r]);
return 1;
}
int t = luaL_checkoption(L, 1, "OFF" , lst);
lua_pushinteger(L, t);
lua_rawsetp(L, LUA_REGISTRYINDEX, &cache_key); // 设置缓存模式
return 0;
}
// 加载文件;L:lua虚拟机, filename:文件地址, mode:文件格式(bin/text)
LUALIB_API int luaL_loadfilex (lua_State *L, const char *filename,
const char *mode) {
int level = cache_level(L);
if (level == CACHE_OFF) { // OFF,直接加载,禁止复用
return luaL_loadfilex_(L, filename, mode);
}
const void * proto = load(filename);
if (proto) {
lua_clonefunction(L, proto); // 复用
return LUA_OK;
}
if (level == CACHE_EXIST) { // EXIST,如果以前加载过,复用它;如果是新文件,直接加载,并且不缓存;
return luaL_loadfilex_(L, filename, mode);
}
lua_State * eL = luaL_newstate(); // ON,遇到新文件,直接新建虚拟机,并缓存文件
if (eL == NULL) {
lua_pushliteral(L, "New state failed");
return LUA_ERRMEM;
}
int err = luaL_loadfilex_(eL, filename, mode);
if (err != LUA_OK) {
size_t sz = 0;
const char * msg = lua_tolstring(eL, -1, &sz);
lua_pushlstring(L, msg, sz);
lua_close(eL);
return err;
}
lua_sharefunction(eL, -1); // 把加载过的函数共享出去
proto = lua_topointer(eL, -1);
const void * oldv = save(filename, proto);
if (oldv) { // 如果已经有老的同名函数,为了防止覆盖已有的代码,关闭当前lua虚拟机
lua_close(eL);
lua_clonefunction(L, oldv);
} else { // 这一份新加载的缓存会一直保留到进程结束
lua_clonefunction(L, proto);
/* Never close it. notice: memory leak */
}
return LUA_OK;
}
// 清空缓存(旧的内存并没被清掉,只是用新的替换它)
static void
clearcache() {
if (CC.L == NULL)
return;
SPIN_LOCK(&CC)
lua_close(CC.L);
CC.L = luaL_newstate();
SPIN_UNLOCK(&CC)
}
// 提供给lua的接口
LUAMOD_API int luaopen_cache(lua_State *L) {
luaL_Reg l[] = {
{ "clear", cache_clear }, // 清空缓存
{ "mode", cache_mode }, // 修改缓存模式
{ NULL, NULL },
};
luaL_newlib(L,l);
lua_getglobal(L, "loadfile");
lua_setfield(L, -2, "loadfile");
return 1;
}- 当在同一个skynet进程中开启了大量lua VM时,缓存机制可以节省不少内存,并且提高了VM启动速度。
每个snlua服务会启动一个单独的lua VM,对于同一份lua文件,N个服务就要加载N次到内存。
skynet对此做了优化,每个lua文件只加载一次到内存,保存lua文件--内存映射表,下一个服务加载的时候copy一份内存即可,提高了VM的启动速度。
注意:clearcache只能用于新服务的更新,对已有服务不能更新;新服务用新代码,旧服务用旧代码。
service/debug_console.lua
-- 控制台命令函数
function COMMAND.help()
return {
...
clearcache = "clear lua code cache", -- 清理缓存
...
}
end
-- 清空缓存命令
function COMMAND.clearcache()
codecache.clear() -- 调用的是:3rd/lua/lauxlib.c的clearcache函数
end下面介绍如何利用clearcache命令来更新服务:
-
新建热更新代码文件
examples/hot.lua和examples/main_hot.lua-- main_hot.lua package.path = "./examples/?.lua;" .. package.path local skynet = require "skynet" skynet.start(function() skynet.newservice("debug_console",8000) print("test hotpatch...") while true do skynet.newservice("hot") skynet.sleep(300) end end)
-- hot.lua local skynet = require "skynet" skynet.start(function() print("hello") end)
-
新建配置文件
examples/config.hotthread = 8 logger = nil harbor = 0 start = "main_hot" luaservice ="./service/?.lua;./test/?.lua;./examples/?.lua"
-
启动skynet
he@SD-20210816HMLO:/mnt/e/skynet$ ./skynet examples/config.hot [:00000002] LAUNCH snlua bootstrap [:00000003] LAUNCH snlua launcher [:00000004] LAUNCH snlua cdummy [:00000005] LAUNCH harbor 0 4 [:00000006] LAUNCH snlua datacenterd [:00000007] LAUNCH snlua service_mgr [:00000008] LAUNCH snlua main_hot [:00000009] LAUNCH snlua debug_console 8000 [:00000009] Start debug console at 127.0.0.1:8000 test hotpatch... [:0000000a] LAUNCH snlua hot hello -
修改lua文件
examples/hot.lua为以下内容-- hot.lua local skynet = require "skynet" skynet.start(function() print("world") -- 修改了这里 end)
-
使用telnet登录控制台
he@SD-20210816HMLO:/mnt/e/skynet$ telnet 127.0.0.1 8000 Trying 127.0.0.1... Connected to 127.0.0.1. Escape character is '^]'. Welcome to skynet console -
在控制台输入
clearcacheclearcache <CMD OK>
-
控制台输出如下
[:00000015] LAUNCH snlua hot hello [:00000016] LAUNCH snlua hot world
inject(注入更新),将新代码注入到已有的服务里,让老服务执行新的代码,可以更新旧服务。
由于skynet修改了lua的缓存机制,所以lua的require热更新方法不再适用。
inject的实现原理(个人理解):其实就是把老代码片段和新代码片段合并在一起,从而替换掉一部分代码,让老的get函数去关联新的upvalue。
lua代码中与inject相关的全局变量:
| LUA全局变量 | 说明 |
|---|---|
_P |
消息分发函数... |
_U |
自定义接口函数, 全局变量... |
-
如果要使用全局变量
_P,热更的代码至少需要自己手动实现消息派发功能,模板如下:skynet.start(function() ... skynet.dispatch("lua", function(session, address, cmd, ...) -- 消息派发逻辑 ... end) end)
-
如果要使用全变量
_U,则把需要热更的函数定义为全局函数;
-
service/debug_console.lua-- debug命令 function COMMAND.help() return { ... inject = "inject address luascript.lua", -- 注入新代码 ... } end -- 跳到COMMAND.inject -- 注入新代码的命令 function COMMAND.inject(address, filename, ...) address = adjust_address(address) local f = io.open(filename, "rb") if not f then return "Can't open " .. filename end local source = f:read "*a" f:close() local ok, output = skynet.call(address, "debug", "RUN", source, filename, ...) if ok == false then error(output) end return output end -- 跳到lualib/skynet/debug.lua/dbgcmd.RUN
-
lualib/skynet/debug.lua-- 注入代码 function dbgcmd.RUN(source, filename, ...) local inject = require "skynet.inject" local args = table.pack(...) local ok, output = inject(skynet, source, filename, args, export.dispatch, skynet.register_protocol) -- 跳到 lualib/skynet/inject.lua/inject函数 collectgarbage "collect" skynet.ret(skynet.pack(ok, table.concat(output, "\n"))) end
-
lualib/skynet/inject.lua-- inject函数,加载新的lua代码并注入; skynet:skynet库, source:代码块, filename:lua文件地址, args:参数 return function(skynet, source, filename, args, ...) if filename then filename = "@" .. filename else filename = "=(load)" end local output = {} local function print(...) local value = { ... } for k,v in ipairs(value) do value[k] = tostring(v) end table.insert(output, table.concat(value, "\t")) end local u = {} local unique = {} local funcs = { ... } for k, func in ipairs(funcs) do -- 获取接口的函数原型 getupvaluetable(u, func, unique) end local p = {} local proto = u.proto -- 遍历所有的消息分发函数 if proto then for k,v in pairs(proto) do local name, dispatch = v.name, v.dispatch if name and dispatch and not p[name] then local pp = {} p[name] = pp getupvaluetable(pp, dispatch, unique) end end end local env = setmetatable( { print = print , _U = u, _P = p}, { __index = _ENV }) local func, err = load(source, filename, "bt", env) -- 加载新的lua代码 if not func then return false, { err } end local ok, err = skynet.pcall(func, table.unpack(args, 1, args.n)) if not ok then table.insert(output, err) return false, output end return true, output end
- 通过控制台输入
inject命令,触发COMMAND.inject函数 COMMAND.inject调用dbgcmd.RUN函数跳转到lua脚本lualib/skynet/inject.lua里面
-
通过dispatch函数热更
-
新建热更新文件
examples/main_hot_inject.lua和examples/hot_inject.lua-- examples/main_hot_inject.lua package.path = "./examples/?.lua;" .. package.path local skynet = require "skynet" local myclass = {} local function hello() print("hello") end function myclass.dispatch() -- todo end function myclass.say() hello() end skynet.start(function() skynet.newservice("debug_console",8000) skynet.dispatch("lua", function(session, address, cmd, ...) myclass.dispatch() end) while true do myclass.say() skynet.sleep(100) end end)
-- examples/hot_inject.lua print ("hot inject start") if not _P then print("hotfix fail, no _P define") return end 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 myclass = _P.lua.myclass local up = get_up(myclass.say) local hello = up.hello myclass.say = function() print("world") end print ("hot inject end")
-
新建配置文件
examples/config.hot_inject
thread = 8 logger = nil harbor = 0 start = "main_hot_inject" luaservice ="./service/?.lua;./test/?.lua;./examples/?.lua"
- 启动skynet
he@SD-20210816HMLO:/mnt/e/skynet$ ./skynet examples/config.hot_inject [:00000002] LAUNCH snlua bootstrap [:00000003] LAUNCH snlua launcher [:00000004] LAUNCH snlua cdummy [:00000005] LAUNCH harbor 0 4 [:00000006] LAUNCH snlua datacenterd [:00000007] LAUNCH snlua service_mgr [:00000008] LAUNCH snlua main_hot_inject [:00000009] LAUNCH snlua debug_console 8000 [:00000009] Start debug console at 127.0.0.1:8000 hello
-
启动控制台并注入lua脚本
he@SD-20210816HMLO:/mnt/e/skynet$ telnet 127.0.0.1 8000 Trying 127.0.0.1... Connected to 127.0.0.1. Escape character is '^]'. Welcome to skynet console list :00000004 snlua cdummy :00000006 snlua datacenterd :00000007 snlua service_mgr :00000008 snlua main_hot_inject :00000009 snlua debug_console 8000 <CMD OK> inject 8 ./examples/hot_inject.lua hot inject start hot inject end <CMD OK>
-
观察主服务窗口
[:00000009] 127.0.0.1:65254 connected world
-
-
通过全局函数
-
新建热更新文件
examples/main_hot_inject.lua和examples/hot_inject.lua-- examples/main_hot_inject.lua package.path = "./examples/?.lua;" .. package.path local skynet = require "skynet" local myclass = {} function hello() -- 注意这里是全局变量,不是local print("hello") end function myclass.dispatch() -- todo end function myclass.say() hello() end skynet.start(function () skynet.newservice("debug_console",8000) while true do myclass.say() skynet.sleep(100) end end)
-- examples/hot_inject.lua print ("hot inject start") if not _U then return end _U._ENV.hello = function () _G.print("u world") end print ("hot inject end")
-
新建配置文件
examples/config.hot_injectthread = 8 logger = nil harbor = 0 start = "main_hot_inject" luaservice ="./service/?.lua;./test/?.lua;./examples/?.lua"
-
启动skynet
he@SD-20210816HMLO:/mnt/e/skynet$ ./skynet examples/config.hot_inject [:00000002] LAUNCH snlua bootstrap [:00000003] LAUNCH snlua launcher [:00000004] LAUNCH snlua cdummy [:00000005] LAUNCH harbor 0 4 [:00000006] LAUNCH snlua datacenterd [:00000007] LAUNCH snlua service_mgr [:00000008] LAUNCH snlua main_hot_inject [:00000009] LAUNCH snlua debug_console 8000 [:00000009] Start debug console at 127.0.0.1:8000 hello
-
启动控制台并注入lua脚本
he@SD-20210816HMLO:/mnt/e/skynet$ telnet 127.0.0.1 8000 Trying 127.0.0.1... Connected to 127.0.0.1. Escape character is '^]'. Welcome to skynet console inject 8 ./examples/hot_inject.lua hot inject start hot inject end <CMD OK>
-
观察主服务窗口
[:00000009] 127.0.0.1:52042 connected hello hello u world
-
snax.hotfix可以用于热更新(被热更的对象只能是全局变量)
例,热更新函数:
-
创建配置文件
examples/config.snax,内容如下:thread = 8 logger = nil harbor = 0 start = "main_snax" bootstrap = "snlua bootstrap" -- The service for bootstrap luaservice = "./service/?.lua;./test/?.lua;./examples/?.lua" lualoader = "lualib/loader.lua" cpath = "./cservice/?.so" snax = "./examples/?.lua"
-
创建入口文件
examples/main_snax.lua,内容如下:local skynet = require "skynet" local snax = require "skynet.snax" skynet.start(function () local s = snax.newservice("snhello", "hello") if not s then print("create snax server fail") return end s.post.hello("hello") -- 无响应请求 print("post hello") local r = s.req.work("coding") -- 有响应请求 print("req work with result:", r) -- 热更新 snax.hotfix(s, [[ function response.work(arg) -- 响应obj.req.work return "finish "..arg end ]]) local r = s.req.work("watch movie") print("req work with hotfix result:", r) end)
-
实现snax服务文件
examples/snhello.lua,内容如下:local skynet = require "skynet" local snax = require "skynet.snax" function init(...) -- 初始化函数 print("snax start with arg:", ...) end function exit(...) -- 退出函数 print("snax exit with arg:", ...) end function accept.hello(...) -- 响应obj.post.hello print("how are you") end function response.work(arg) -- 响应obj.req.work return "do "..arg end
-
启动控制台并注入lua脚本
he@SD-20210816HMLO:/mnt/e/skynet$ ./skynet ./examples/config.snax [:00000002] LAUNCH snlua bootstrap [:00000003] LAUNCH snlua launcher [:00000004] LAUNCH snlua cdummy [:00000005] LAUNCH harbor 0 4 [:00000006] LAUNCH snlua datacenterd [:00000007] LAUNCH snlua service_mgr [:00000008] LAUNCH snlua main_snax [:00000009] LAUNCH snlua snaxd snhello snax start with arg: hello post hello how are you req work with result: do coding req work with hotfix result: finish watch movie [:00000002] KILL self
TODO
skynet的缓存机制是可以关闭的,当关闭skynet缓存机制之后,就可以利用lua本身的特性来实现热更新。
注意:关闭skynet缓存机制意味着性能可能会下降,慎重选择
TODO