skynet源碼分析之熱更新


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")


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM