skynet有兩種方法支持熱更新lua代碼:clearcache和inject,在介紹skynet熱更新機制之前,先介紹skynet控制台,參考官方wiki https://github.com/cloudwu/skynet/wiki/DebugConsole
1. skynet控制台
想要使用skynet控制台,需啟動debug_console服務skynet.newservice("debug_console", ip, port),指定一個地址。skynet啟動后,用nc命令就可以進入控制台,如圖。
debug_console服務啟動后,監聽外部連接(第3行)。
第15行,當打開控制台連接建立后,fork一個協程在console_main_loop里處理這個tcp連接的通信交互
第6-13行,使用特定的print,數據不是輸出到屏幕上,而是通過socket.write發送給控制台
第24-28行,獲取控制台發來的數據,然后調用docmd
第35-52行,解析出相應指令,執行完后,通過print發送給控制台
1 -- service/debug_console.lua 2 skynet.start(function() 3 local listen_socket = socket.listen (ip, port) 4 skynet.error("Start debug console at " .. ip .. ":" .. port) 5 socket.start(listen_socket , function(id, addr) 6 local function print(...) 7 local t = { ... } 8 for k,v in ipairs(t) do 9 t[k] = tostring(v) 10 end 11 socket.write(id, table.concat(t,"\t")) 12 socket.write(id, "\n") 13 end 14 socket.start(id) 15 skynet.fork(console_main_loop, id , print) 16 end) 17 end) 18 19 local function console_main_loop(stdin, print) 20 print("Welcome to skynet console") 21 skynet.error(stdin, "connected") 22 local ok, err = pcall(function() 23 while true do 24 local cmdline = socket.readline(stdin, "\n") 25 ... 26 if cmdline ~= "" then 27 docmd(cmdline, print, stdin) 28 end 29 end 30 end) 31 ... 32 end 33 34 local function docmd(cmdline, print, fd) 35 local split = split_cmdline(cmdline) 36 local command = split[1] 37 local cmd = COMMAND[command] 38 local ok, list 39 if cmd then 40 ok, list = pcall(cmd, table.unpack(split,2)) 41 else 42 ... 43 end 44 45 if ok then 46 ... 47 print(list) 48 print("<CMD OK>") 49 else 50 print(list) 51 print("<CMD Error>") 52 end 53 end
比如,在控制台輸入"list",最終會調用到COMMAND.list(),獲取當前服務信息,然后返回給控制台。於是就有了上面截圖的信息。
1 -- service/debug_console.lua 2 function COMMAND.list() 3 return skynet.call(".launcher", "lua", "LIST") 4 end
2. clearcache更新方法
clearcache用於新建服務的熱更新,比如agent,對已有的服務不能熱更新。使用方法很簡單:在控制台輸入"clearcache"即可,下面分析其原理:
每個snlua服務會啟動一個單獨的lua VM,對於同一份Lua文件,N個服務就要加載N次到內存。skynet對此做了優化,每個Lua文件只加載一次到內存,保存Lua文件-內存映射表,下一個服務加載的時候copy一份內存即可,提高了VM的啟動速度(省掉讀取Lua文件和解析Lua語法的過程)。參考官方wiki https://github.com/cloudwu/skynet/wiki/CodeCache
第2-6行,全局的Lua狀態機,以Lua文件名為key,內存指針為value,保存在狀態機的注冊表里,位於棧上有效偽索引LUA_REGISTERYINDEX處。
第8行,修改了官方的luaL_loadfilex接口:
第11-15行,調用load從全局狀態機的注冊表里獲取文件名對應的內存塊,調用lua_clonefunction拷貝一份后即可返回
第16-18行,第一次加載文件到內存里
第19-26行,調用save保存文件名-內存塊的映射,如果有舊的內存塊,返回舊的,否則返回剛加載的內存塊
1 // 3rd/lua/lauxlib.c 2 struct codecache { 3 struct spinlock lock; 4 lua_State *L; 5 }; 6 static struct codecache CC; 7 8 LUALIB_API int luaL_loadfilex (lua_State *L, const char *filename, 9 const char *mode) { 10 ... 11 const void * proto = load(filename); 12 if (proto) { 13 lua_clonefunction(L, proto); 14 return LUA_OK; 15 } 16 lua_State * eL = luaL_newstate(); 17 int err = luaL_loadfilex_(eL, filename, mode); 18 proto = lua_topointer(eL, -1); 19 const void * oldv = save(filename, proto); 20 if (oldv) { 21 lua_close(eL); 22 lua_clonefunction(L, oldv); 23 } else { 24 lua_clonefunction(L, proto); 25 /* Never close it. notice: memory leak */ 26 } 27 28 return LUA_OK; 29 }
load接口,從全局狀態機CC的注冊表里獲取指定文件對應的內存塊(可能不存在)
1 // 3rd/lua/lauxlib.c 2 static const void * 3 load(const char *key) { 4 if (CC.L == NULL) 5 return NULL; 6 SPIN_LOCK(&CC) 7 lua_State *L = CC.L; 8 lua_pushstring(L, key); 9 lua_rawget(L, LUA_REGISTRYINDEX); 10 const void * result = lua_touserdata(L, -1); 11 lua_pop(L, 1); 12 SPIN_UNLOCK(&CC) 13 14 return result; 15 }
save接口,先獲取舊的內存塊(12-15行),如果有則直接返回,否則把新內存塊加載到注冊表中(17-19行)
1 static const void * 2 save(const char *key, const void * proto) { 3 lua_State *L; 4 const void * result = NULL; 5 6 SPIN_LOCK(&CC) 7 if (CC.L == NULL) { 8 init(); 9 L = CC.L; 10 } else { 11 L = CC.L; 12 lua_pushstring(L, key); 13 lua_pushvalue(L, -1); 14 lua_rawget(L, LUA_REGISTRYINDEX); 15 result = lua_touserdata(L, -1); /* stack: key oldvalue */ 16 if (result == NULL) { 17 lua_pop(L,1); 18 lua_pushlightuserdata(L, (void *)proto); 19 lua_rawset(L, LUA_REGISTRYINDEX); 20 } else { 21 lua_pop(L,2); 22 } 23 } 24 SPIN_UNLOCK(&CC) 25 return result; 26 }
clearcache的原理就是刪除這個全局的狀態機,這樣新服務就可以用最新的Lua文件(load接口返回NULL),且不影響已有服務的運行。此時,新服務運行新的代碼,舊服務運行舊的代碼。
在控制台輸入"clearcache"后,最終調用到c中的clearcache,刪除舊的全局VM,然后新建一個(19-20行)。
1 -- service/debug_console.lua 2 function COMMAND.clearcache() 3 codecache.clear() 4 end 5 6 // 3rd/lua/lauxlib.c 7 static int 8 cache_clear(lua_State *L) { 9 (void)(L); 10 clearcache(); 11 return 0; 12 } 13 14 static void 15 clearcache() { 16 if (CC.L == NULL) 17 return; 18 SPIN_LOCK(&CC) 19 lua_close(CC.L); 20 CC.L = luaL_newstate(); 21 SPIN_UNLOCK(&CC) 22 }
3. inject更新方法
inject譯為“注入”,即將新代碼注入到已有的服務里,讓服務執行新的代碼,可以熱更已開啟的服務,使用方法簡單,在控制台輸入"inject address xxx.lua"即可,難點在於lua代碼的編寫,建議只做一些簡單的熱更。其實現原理是:給服務發送消息,讓其執行新代碼,新代碼修改已有的函數原型(包括upvalues),完成對函數的更新。
第10行,給指定服務發送"DEBUG"類型消息
第20行,最終調用inject接口注入代碼修改函數原型(包括閉包)。注:只需修改服務的register_protocol接口以及消息分發接口
1 -- service/debug.lua 2 function COMMAND.inject(address, filename) 3 address = adjust_address(address) 4 local f = io.open(filename, "rb") 5 if not f then 6 return "Can't open " .. filename 7 end 8 local source = f:read "*a" 9 f:close() 10 local ok, output = skynet.call(address, "debug", "RUN", source, filename) 11 if ok == false then 12 error(output) 13 end 14 return output 15 end 16 17 -- lualib/skynet/debug.lua 18 function dbgcmd.RUN(source, filename) 19 local inject = require "skynet.inject" 20 local ok, output = inject(skynet, source, filename , export.dispatch, skynet.register_protocol) 21 collectgarbage "collect" 22 skynet.ret(skynet.pack(ok, table.concat(output, "\n"))) 23 end
inject的處理過程:
第7-9行,獲取接口的函數原型(包括閉包),保存在u里
第11-21行,遍歷所有的消息分發函數(每種消息類型對應一個函數),通過getupvaluetable接口保存函數原型(包括閉包)
第22-23行,執行新的Lua代碼,通過env里的_U,_P獲取原有的函數原型
1 -- lualib/skynet/inject.lua 2 return function(skynet, source, filename , ...) 3 local output = {} 4 local u = {} 5 local unique = {} 6 local funcs = { ... } 7 for k, func in ipairs(funcs) do 8 getupvaluetable(u, func, unique) 9 end 10 local p = {} 11 local proto = u.proto 12 if proto then 13 for k,v in pairs(proto) do 14 local name, dispatch = v.name, v.dispatch 15 if name and dispatch and not p[name] then 16 local pp = {} 17 p[name] = pp 18 getupvaluetable(pp, dispatch, unique) 19 end 20 end 21 end 22 local env = setmetatable( { print = print , _U = u, _P = p}, { __index = _ENV }) 23 local func, err = load(source, filename, "bt", env) 24 ... 25 26 return true, output 27 end
示例:比如啟動了一個test服務
-- test.lua
1 local skynet = require "skynet" 2 3 local CMD = {} 4 5 local function test(...) 6 print(...) 7 skynet.ret(skynet.pack("OK")) 8 end 9 10 function CMD.ping(msg) 11 test(msg) 12 end 13 14 skynet.dispatch("lua", function(session, source, cmd, ...) 15 local f = CMD[cmd] 16 if f then 17 f(...) 18 end 19 end) 20 21 skynet.start(function() 22 end)
在控制台輸入"inject address inject_test.lua"熱更test服務,
第23行,通過全局環境變量_P獲取lua類型消息分發函數里的接口CMD
第24行,獲取CMD.ping接口的所有閉包
第25行,得到test的函數原型
第27-30行,更新接口,完成熱更。
1 -- inject_test.lua 2 print("hotfix begin") 3 4 if not _P then 5 print("hotfix faild, _P not define") 6 return 7 end 8 9 local function get_upvalues(f) 10 local u = {} 11 if not f then return u end 12 local i = 1 13 while true do 14 local name, value = debug.getupvalue(f, i) 15 if name == nil then 16 return u 17 end 18 u[name] = value 19 i = i + 1 20 end 21 end 22 23 local CMD = _P.lua.CMD 24 local upvalues = get_upvalues(CMD.ping) 25 local test = upvalues.test 26 27 CMD.ping = function(msg) 28 local postfix = "aaa" 29 test(msg .. postfix) 30 end 31 32 print("hotfix end")