skynet:熱更新 lua 代碼


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 控制台管理使用技巧 沒有開花的樹


免責聲明!

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



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