• 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,"	"))
    12             socket.write(id, "
    ")
    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, "
    ")
    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, "
    ")))
    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")
  • 相关阅读:
    python 编码与解码
    python 写文件
    python 文件读写
    python 异常处理
    python 断言
    C++的可移植性和跨平台开发
    Python中subprocess学习
    Python 的web自动化测试
    CookieJar和HTTPCookieProcessor
    python3爬虫
  • 原文地址:https://www.cnblogs.com/RainRill/p/8940673.html
Copyright © 2020-2023  润新知