skynet有兩種方法熱更新lua代碼,clearcache和inject,文章分別對這兩種方法做說明。
clearcache熱更新
講這個前,先說明下skynet代碼加載的事情。因為skynet的每個服務都是一個獨立的lua虛擬機,對於同一份lua代碼,N個服務就要加載lua文件N次,所以,skynet做了優化,代碼文件只需要加載一次到內存,其他服務復制這份內存就可以了,省了讀取lua文件和解析lua語法的過程。
clearcache 使用很簡單,啟動skynet,連接到其
控制台:
# nc 127.0.0.1 8000 Welcome to skynet console clearcache OK
但clearcache有個不可忽視的問題,每次clearcache后,不管代碼有沒有用到,skynet不會清理舊的內存。這會導致了多次clearcache后,skynet內存使用會越來越大
這是為什么?因為clearcache后,只有新起的服務會用到新代碼,舊的服務還引用着舊代碼。而skynet沒有做引用GC的復雜邏輯,在舊服務銷毀時,沒有清理用不到的舊代碼。
或許你會很好奇,clearcache 沒清的內存到底是啥?
這要從skynet代碼共享說起,skynet加載lua代碼時,對於一個代碼文件使用了一個新的vm加載,然后以文件名作為key將代碼索引到全局的vm中。這樣,當有服務需要代碼了,就從全局vm找到代碼,復制一份到服務。而clearcache,就是刪除這個全局的vm,然后再重建一個。這么做的好處是,執行clearcache后,不影響已有服務的運行。問題是,全局vm刪了,這個vm索引的所有代碼沒有清理,這樣,那些加載代碼用的vm沒做清理。
inject熱更新
inject命令相當於注入代碼到服務中,原理就是讓指定服務執行某個代碼文件,通過修改模塊及其函數的upvalue,完成對lua模塊代碼或變量的替換。這個命令我在前面的文章[
1]有詳細介紹。
inject用法很簡單,啟動skynet,連接到其控制台:
# nc 127.0.0.1 8000 Welcome to skynet console list :00000004 snlua cmaster :00000005 snlua cslave :00000007 snlua datacenterd :00000008 snlua service_mgr :0000000a snlua protoloader :0000000b snlua console :0000000c snlua debug_console 8000 :0000000d snlua simpledb OK inject :0000000d example/inject_simpledb.lua
inject命令的難點是,這個要注入的lua代碼該怎么寫。
下面直接改寫skynet自帶的example做說明:
# cat examples/simpledb.lua
local skynet = require "skynet" require "skynet.manager" local db = {} local command = {} -- 增加了這里 local function test(msg) print(msg) end -- 增加了這里 function command.do_test(msg) test(msg) end skynet.start(function() skynet.dispatch("lua", function(session, address, cmd, ...) local f = command[string.upper(cmd)] if f then skynet.ret(skynet.pack(f(...))) else error(string.format("Unknown command %s", tostring(cmd))) end end) -- 增加了這里 skynet.fork(function() while true do skynet.sleep(100) command.do_test("itest!") end end) skynet.register "SIMPLEDB" end)
假設以上的 command.do_test 就是我們要熱更改掉的函數。那用於inject的lua代碼如下:
# cat inject_test.lua
if not _P then print("hotfix fail, no _P define") return end print("hotfix begin") -- 用於獲取函數變量 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 command = _P.lua.command local upvs = get_up(command.do_test) local test = upvs.test command.do_test = function(msg) test('New ' .. msg) end print("hotfix end")
啟動控制台,執行inject后,就會看到類似下面的skynet的日志:
# ./skynet examples/config [:00000001] LAUNCH logger [:00000002] LAUNCH snlua bootstrap [:00000003] LAUNCH snlua launcher [:00000004] LAUNCH snlua cmaster [:00000005] LAUNCH snlua cslave [:00000006] LAUNCH harbor 1 16777221 [:00000007] LAUNCH snlua datacenterd [:00000008] LAUNCH snlua service_mgr [:00000009] LAUNCH snlua main [:0000000a] LAUNCH snlua protoloader [:0000000b] LAUNCH snlua console [:0000000c] LAUNCH snlua debug_console 8000 [:0000000d] LAUNCH snlua simpledb [:0000000e] LAUNCH snlua watchdog [:0000000f] LAUNCH snlua gate [:0000000f] Listen on 0.0.0.0:8888 Watchdog listen on 8888 [:00000009] KILL self [:00000002] KILL self itest! itest! itest! New itest! New itest!
最后語
通過前面的分析,我們知道了,clearcache和inject兩種方法都可以熱更代碼。clearcache比較簡單,但這種方法對於已有的服務是沒有效果的,只有在新的服務才生效。而inject可以熱更已有的服務,但不管是inject腳本的編寫,還是inject命令的執行,都相對比較繁瑣。所以要根據實際的需求,選擇適合的方法熱更lua代碼。
參考:
[1]
skynet 控制台管理使用技巧 沒有開花的樹
