skynet-inject热更原理

inject是一个用于动态加载 Lua 代码文件并执行其中定义的函数的功能。可以在运行时动态加载 Lua 代码文件,然后调用其中定义的函数,通过修改模块及其函数的upvalue,实现代码的动态注入和执行。热更实现的大概思路:通过向指定服务注入cmd实现热更
参考wiki https://blog.codingnow.com/2016/11/lua_update.html

关键源码分析

-- debug_console.lua

local function adjust_address(address)
    local prefix = address:sub(1,1)
    if prefix == '.' then
        return assert(skynet.localname(address), "Not a valid name")
    elseif prefix ~= ':' then
        address = assert(tonumber("0x" .. address), "Need an address") | (skynet.harbor(skynet.self()) << 24)
    end
    return address
end


function COMMAND.inject(address, filename, ...)
    address = adjust_address(address)
    local f = io.open(filename, "rb")
    if not f then
        return "Can't open " .. filename
    end
    local source = f:read "*a"
    f:close()
    local ok, output = skynet.call(address, "debug", "RUN", source, filename, ...)
    if ok == false then
        error(output)
    end
    return output
end

adjust_address: 用于调整传入的地址参数,对传入的地址参数进行转换,以保证将热更脚本注入到正确的服务地址
io.open(filename, “rb”): 以二进制只读的方式读取文件内容
source: 存放读取到的热更脚本内容
skynet.call(address, “debug”, “RUN”, …): 通过skynet的debug消息类型,将读取的热更脚本内容注入到目标服务地址(debug.RUN内部实现)

-- skynet.debug.lua

function dbgcmd.RUN(source, filename, ...)
    local inject = require "skynet.inject"
    local args = table.pack(...)
    local ok, output = inject(skynet, source, filename, args, export.dispatch, skynet.register_protocol)
    collectgarbage "collect"
    skynet.ret(skynet.pack(ok, table.concat(output, "\n")))
end
-- skynet.inject.lua

return function(skynet, source, filename, args, ...)
    if filename then
        filename = "@" .. filename
    else
        filename = "=(load)"
    end
    local output = {}

    local function print(...)
        local value = { ... }
        for k,v in ipairs(value) do
            value[k] = tostring(v)
        end
        table.insert(output, table.concat(value, "\t"))
    end
    local u = {}
    local unique = {}
    local funcs = { ... }
    for k, func in ipairs(funcs) do
        getupvaluetable(u, func, unique)
    end
    local p = {}
    local proto = u.proto
    if proto then
        for k,v in pairs(proto) do
            local name, dispatch = v.name, v.dispatch
            if name and dispatch and not p[name] then
                local pp = {}
                p[name] = pp
                getupvaluetable(pp, dispatch, unique)
            end
        end
    end
    local env = setmetatable( { print = print , _U = u, _P = p}, { __index = _ENV })
    local func, err = load(source, filename, "bt", env)
    if not func then
        return false, { err }
    end
    local ok, err = skynet.pcall(func, table.unpack(args, 1, args.n))
    if not ok then
        table.insert(output, err)
        return false, output
    end

    return true, output
end

这两段核心代码实现了一个动态加载和执行 Lua 代码的功能,通过构建环境表、加载代码并执行,最终返回执行结果。这个函数是实现动态注入功能的关键部分,能够实现在 Skynet 框架中动态加载并执行 Lua代码,完成注入热更脚本

热更方式

  1. 可以直接在debug控制台通过调用inject将指定代码注入到指定地址实现,但对于同类型的服务需要手动依次执行,效率较低
  2. 可通过实现sh脚本获取到指定服务地址(例如hotfix服),然后在框架中的hotfix服内实现对框架类型服务的热更lua脚本
  3. 实现sh脚本,用于启动debug控制台,执行热更逻辑
  4. 添加hotcfg,用于配置需要热更的服务与要执行lua脚本路径,并定义lua脚本
  5. 根据框架架构(单点 or 集群),在hotfix服务中实现获取指定服务组的addr
  6. 通过配置的hotcfg实现将lua热更脚本依次注入到目标服务中

拓扑图

方案1:
【基于skyent的热更思考】-LMLPHP

方案2:
【基于skyent的热更思考】-LMLPHP

注意事项

  1. 要注意新代码与原有代码的兼容性,避免因为接口变更或依赖关系导致运行时错误
  2. 要注意避免对全局环境造成非预期的影响,尽量将修改限制在局部代码范围内
  3. 需要加入足够的异常处理机制,以在热更新过程中出现异常情况时能够及时捕获并处理,以保证系统的稳定性
  4. 在进行热更新前最好制定好回滚策略,以便在更新失败时能够及时恢复到原有的稳定状态
  5. 尽量避免热更新过程对系统性能造成过大影响,可以在适当的时机进行优化,减少更新对系统性能的影响
  6. 在进行热更新时建议增加详细的日志记录和监控机制,以便随时监测更新过程中的各种信息并进行分析
03-26 16:42