skynet源码分析之热更新

摘要:
skynet启动后,用nc命令就可以进入控制台,如图。1--service/debug_console.lua2functionCOMMAND.list()3returnskynet.call4end2.clearcache更新方法clearcache用于新建服务的热更新,比如agent,对已有的服务不能热更新。skynet对此做了优化,每个Lua文件只加载一次到内存,保存Lua文件-内存映射表,下一个服务加载的时候copy一份内存即可,提高了VM的启动速度。

skynet有两种方法支持热更新lua代码:clearcache和inject,在介绍skynet热更新机制之前,先介绍skynet控制台,参考官方wikihttps://github.com/cloudwu/skynet/wiki/DebugConsole

1. skynet控制台

想要使用skynet控制台,需启动debug_console服务skynet.newservice("debug_console", ip, port),指定一个地址。skynet启动后,用nc命令就可以进入控制台,如图。

skynet源码分析之热更新第1张

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     localok, 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 functionCOMMAND.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语法的过程)。参考官方wikihttps://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   structcodecache {
3           struct spinlock lock;
4           lua_State *L;
5 };
6   static structcodecache 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       returnLUA_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     returnLUA_OK;
29   }

load接口,从全局状态机CC的注册表里获取指定文件对应的内存块(可能不存在)

1 //3rd/lua/lauxlib.c
2 static const void *
3  load(const char *key) {
4    if (CC.L ==NULL)
5      returnNULL;
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    returnresult;
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     returnresult;
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 functionCOMMAND.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     returnoutput
15 end
16 
17 --lualib/skynet/debug.lua
18 functiondbgcmd.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 functiontest(...) 6 print(...) 7 skynet.ret(skynet.pack("OK")) 8 end 9 10 functionCMD.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 functionget_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             returnu
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")

免责声明:文章转载自《skynet源码分析之热更新》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇RAxML安装Win10系统的SurfacePro4如何重装系统-4 如何再次备份和还原系统下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

Vuex 模块化与项目实例 (2.0)

Vuex 强调使用单一状态树,即在一个项目里只有一个 store,这个 store 集中管理了项目中所有的数据以及对数据的操作行为。但是这样带来的问题是 store 可能会非常臃肿庞大不易维护,所以就需要对状态树进行模块化的拆分。 首先贴出一个逻辑比较复杂的H5项目:DEMO & 源码 该项目主要包括 banner、feeds、profile 三个...

句柄类与继承

前一小节《容器与继承》http://blog.csdn.net/thefutureisour/article/details/7744790提到过: 对于容器,假设定义为基类类型,那么则不能通过容器訪问派生类新增的成员;假设定义为派生类类型,一般不能用它承载基类的对象,即使利用类型转化强行承载,则基类对象能够訪问没有意义的派生类成员,这样做是非常危...

Android-Native-Server 启动和注册详细分析

Android-Native-Server 启动和注册详细分析 以mediaService为实例来讲解: mediaService的启动入口 是一个 传统的 main()函数 源码位置E:src_androidandroid_4.1.1_r1android_4.1.1_r1frameworksavmediamediaservermain_mediaserv...

内存映射大文件

对于一些小文件,用普通的文件流就可以很好的解决,可是对于超大文件,比如2G或者更多,文件流就不行了,所以要使用API的内存映射的相关方法,即使是内存映射,也不能一次映射全部文件的大小,所以必须采取分块映射,每次处理一小部分。 先来看几个函数 CreateFile :打开文件 GetFileSize : 获取文件尺寸 CreateFileMapping :创...

springboot报错_Cannot deserialize instance of `java.util.ArrayList` out of START_OBJECT token

一、问题描述: springboot框架,前台通过ajax像后台controller传递参数。 前台代码: $.ajax({ type: "POST",//方法类型 contentType:'application/json', dataType: "json",//预期服务器返回的数据类型 url:...

使用antd Table + mobx 处理数组 出现的一系列问题

在store中定义了一个数组: @observable list = [...] 若是在table组件中直接使用list: <Table className={styles.table} columns={this.columns} dataSource={list}  /> 这时就会提示以下错误...