基于skyent的热更思考
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代码,完成注入热更脚本
热更方式
- 可以直接在debug控制台通过调用inject将指定代码注入到指定地址实现,但对于同类型的服务需要手动依次执行,效率较低
- 可通过实现sh脚本获取到指定服务地址(例如hotfix服),然后在框架中的hotfix服内实现对框架类型服务的热更lua脚本
- 实现sh脚本,用于启动debug控制台,执行热更逻辑
- 添加hotcfg,用于配置需要热更的服务与要执行lua脚本路径,并定义lua脚本
- 根据框架架构(单点 or 集群),在hotfix服务中实现获取指定服务组的addr
- 通过配置的hotcfg实现将lua热更脚本依次注入到目标服务中
拓扑图
方案1:
方案2:
注意事项
- 要注意新代码与原有代码的兼容性,避免因为接口变更或依赖关系导致运行时错误
- 要注意避免对全局环境造成非预期的影响,尽量将修改限制在局部代码范围内
- 需要加入足够的异常处理机制,以在热更新过程中出现异常情况时能够及时捕获并处理,以保证系统的稳定性
- 在进行热更新前最好制定好回滚策略,以便在更新失败时能够及时恢复到原有的稳定状态
- 尽量避免热更新过程对系统性能造成过大影响,可以在适当的时机进行优化,减少更新对系统性能的影响
- 在进行热更新时建议增加详细的日志记录和监控机制,以便随时监测更新过程中的各种信息并进行分析