笨鸟先飞的橘猫头像

skynet热更新实现问题与排错记录

热更新实现问题与排错记录

一、问题总览

在实现代码热更新功能的过程中,遇到了两个核心问题,根源都指向 skynet 框架的双层缓存设计。

#问题现象根因
1codetest_reload 热更不生效修改代码后触发重载,返回值仍是旧版本只清了 Lua 层缓存,没清 C 层 codecache
2codetest_get 间歇性失败有时返回 nil, "not loaded"testModule 是局部变量,服务重启后丢失

二、skynet 的双层缓存架构

skynet 中有两层独立的缓存,只清其中一层无法完成代码热更新。

┌─────────────────────────────────────────────────────────────┐
│                    Lua 层缓存(per lua_State)                │
│                                                             │
│  package.loaded["test_module"] = table: 0x...               │
│       ▲                                                     │
│       │ 同一张表引用                                          │
│  loaded (skynet/require.lua 局部变量)                         │
│                                                             │
│  清除方式: package.loaded["xxx"] = nil                       │
├─────────────────────────────────────────────────────────────┤
│                    C 层缓存(per process)                    │
│                                                             │
│  struct codecache {                                         │
│      struct spinlock lock;                                  │
│      lua_State *L;  ← 独立 lua_State,存放所有已编译 proto   │
│  };                                                         │
│                                                             │
│  key = 文件路径, value = 编译后的字节码 proto                  │
│  清除方式: skynet.cache.clear() 或 codecache.clear()         │
└─────────────────────────────────────────────────────────────┘

关键代码位置

组件文件行号
codecache 结构体定义skynet/3rd/lua/lauxlib.cL1221-1226
loadfile 被劫持(先查缓存)skynet/3rd/lua/lauxlib.cL1341-1371
cache.clear() 实现skynet/3rd/lua/lauxlib.cL1392-1397
全局 require 被 skynet 替换skynet/lualib/loader.luaL48
skynet 自定义 requireskynet/lualib/skynet/require.luaL24-97
skynet.cache 暴露skynet/lualib/skynet.luaL49

三、问题一:codetest_reload 热更不生效

3.1 现象

修改 test_module.lua(version: 5 → 6),触发 codetest_reload,日志显示:

[13:17:22] codetest_load done: version=5, value=hello_v5     ← 首次加载
[13:17:48] BEFORE clear: package.loaded['test_module'] = table: 0x...
[13:17:48] AFTER clear: package.loaded['test_module'] = nil  ← Lua层已清除
[13:17:48] codetest_reload: resolved file=./chatroom/lualib/test_module.lua
[13:17:48] codetest_reload done: version=5, value=hello_v5   ← ❌ 仍是旧版本!

3.2 调用链分析

hotfix_service.lua
  └→ require("test_module")                      (全局 require,已被 skynet 替换)
       └→ skynet.require("test_module")           (skynet/require.lua)
            ├─ loaded["test_module"] → nil        ✓ package.loaded 已清除
            ├─ loadfile("./chatroom/.../test_module.lua")
            │    └→ luaL_loadfilex()              (lauxlib.c,被劫持的 loadfile)
            │         ├─ load_proto(filename) → 命中 C 层缓存!
            │         └─ 返回旧的编译字节码 ❌     根本不会读磁盘文件
            └─ 执行旧字节码 → 得到旧版本模块

3.3 C 层缓存的工作原理

// lauxlib.c - luaL_loadfilex 简化逻辑
LUALIB_API int luaL_loadfilex(lua_State *L, const char *filename, ...) {
    int level = cache_level(L);            // 默认 CACHE_ON = 2

    if (level == CACHE_OFF) {
        return luaL_loadfilex_(L, ...);    // 不缓存,直接读磁盘
    }

    proto = load_proto(filename);          // ⬅ 先查 codecache
    if (proto) {
        lua_clonefunction(L, proto);       // ⬅ 命中!直接返回,不读磁盘!
        return LUA_OK;
    }

    // 未命中才读磁盘,然后 save_proto 存入缓存
    eL = newState(L);
    err = luaL_loadfilex_(eL, filename, ...);
    save_proto(L, filename, proto);
    ...
}

3.4 修复方案

在 require 重载之前,必须同时清除 C 层缓存:

-- 1. 清除 Lua 层缓存
package.loaded["test_module"] = nil

-- 2. 清除 C 层代码缓存(关键!)
skynet.cache.clear()

-- 3. 重新加载
local newMod = require("test_module")

3.5 修复的文件

chatroom/service/hotfix_service.lua — handle_codetest_reload 中新增 skynet.cache.clear()

chatroom/lualib/res_update.lua — CheckReloadCodes 中新增 skynet.cache.clear()(生产级热更路径同样受影响)


四、问题二:codetest_get 间歇性返回 “not loaded”

4.1 现象

call .hotfix_service "codetest_get"
→ 1   nil
→ 2   not loaded, call codetest_load first

热更功能"修了几次"后会突然不可用。

4.2 根因

testModule 是 [hotfix_service.lua L110](file:///home/djh/skynet-master/chatroom/service/hotfix_service.lua#L110) 的模块级局部变量:

-- if mode == "controller" then 块内部
local testModule        -- ← 服务启动时初始化为 nil

每次修改 hotfix_service.lua 本身(如加调试日志、修复 bug),skynet 会重启这个 service,testModule 就回到 nil。此时直接调用 codetest_get 或 codetest_reload 都会因为没有先 codetest_load 而失败。

服务启动 → testModule = nil
    │
    ├─ codetest_load → testModule = require("test_module")  ✅ 有值
    │
    ├─ 修改 hotfix_service.lua → 服务重启 → testModule = nil
    │
    └─ codetest_get → nil → "not loaded"  ❌

4.3 修复方案

给 handle_codetest_get 和 handle_codetest_reload 添加自动恢复逻辑:检测到 testModule 为空时自动执行 require("test_module")。

local function handle_codetest_get()
    if not testModule then
        -- 服务重启后 testModule 丢失,自动恢复
        local ok, mod = pcall(require, "test_module")
        if not ok then
            return nil, "not loaded, ..."
        end
        testModule = mod
    end
    return { version = testModule.getVersion(), ... }
end

五、总结:代码热更新的正确步骤

在一个 skynet 服务中实现代码热更新,必须按以下顺序清除所有缓存:

-- ============================================
-- 代码热更新 - 完整流程
-- ============================================

-- Step 1: 清除 C 层代码缓存(process 级别,所有 lua_State 共享)
--   lauxlib.c 的 struct codecache,以文件路径为 key 缓存编译后的 proto
--   loadfile 命中此缓存会跳过磁盘读取,直接返回旧的字节码
skynet.cache.clear()

-- Step 2: 清除 Lua 层模块缓存(per lua_State)
--   package.loaded 和 skynet/require.lua 的 loaded 是同一张表
package.loaded["module_name"] = nil

-- Step 3: 重新 require
local newMod = require("module_name")

注意:Step 1 和 Step 2 的顺序可以互换,但两者缺一不可。


六、启动与集成阶段问题

6.1 Lua 5.3 全局 arg 变量缺失

现象:

./chatroom/service/hotfix_service.lua:7: attempt to index a nil value (global 'arg')

根因:hotfix_service.lua 使用 arg[1] 获取启动参数(判断 controller/agent 模式),但 Lua 5.3 不再自动暴露 arg 全局变量,需显式通过 ... 获取。

修复:

-- hotfix_service.lua 文件顶部添加
local arg = {...}

6.2 logger 组件 API 不匹配

现象:

./chatroom/service/hotfix_service.lua:10: attempt to call a nil value (field 'infof')

根因:hotfix_service.lua 调用 log.infof("..."),但 logger.lua 只提供了 info/warn/error/debug 四个基础方法,没有 xxxf 格式化变体。

修复:在 [logger.lua](file:///home/djh/skynet-master/chatroom/lualib/logger.lua) 中添加 infof/warnf/errorf/debugf 适配层,自动从 [source] 格式串提取日志来源。


6.3 inspect 模块缺失

现象:

attempt to call a nil value (global 'inspect')

根因:多个热更新相关文件(hotfix_service.lua、controller.lua、test_hotfix.lua 等)使用了 inspect() 函数但未 require。inspect 是一个 Lua table 格式化调试工具。

修复:

  1. 新建 [inspect.lua](file:///home/djh/skynet-master/chatroom/lualib/inspect.lua)(递归 table 打印,防循环引用,深度限制 4 层)
  2. 所有使用处添加 require "inspect"

6.4 cluster 模块路径错误

现象:

module 'cluster' not found

根因:controller.lua 中写了 require "cluster",skynet 的 cluster 模块实际路径是 skynet.cluster。

修复:改为 require "skynet.cluster"。


6.5 skynet.register 未导入

现象:

attempt to call a nil value (field 'register')

根因:skynet.register 定义在 skynet.manager 模块中,不是 skynet 默认导出的 API。多个测试文件(test_hotfix.lua、test_code_hotfix.lua、test_code_swap_service.lua)调用了 skynet.register() 但未 require manager。

修复:所有使用 skynet.register 的文件添加:

require "skynet.manager"

6.6 测试模块 require 路径问题

现象:

module 'test_module' not found:
    no file './chatroom/lualib/test_module.lua'
    ...

根因:test_module.lua 最初放在 chatroom/service/,但 skynet 的 package.path 不包含此目录。搜索路径为:./?.lua → ./chatroom/lualib/?.lua → ./chatroom/etc/?.lua → skynet 内置路径。

修复:将文件移至 chatroom/lualib/test_module.lua。

额外发现:存在两个同名文件 chatroom/test_module.lua(旧版本)和 chatroom/lualib/test_module.lua(新版本)。package.searchpath 按路径顺序匹配,可能加载到旧文件。删除 chatroom/test_module.lua。


七、调试控制台交互问题

7.1 “Not a valid name” 错误

现象:

./skynet/service/debug_console.lua:227: Not a valid name

触发命令:

call .hotfix_service "broadcast_update", {"path"}
call .test_hotfix "get"

根因:debug_console 的 call 命令使用 . 前缀查找服务,这对应 skynet 的本地名(skynet.name(".xxx", handle) 注册)。而 skynet.register("xxx") 注册的是全局名,debug_console 不会自动解析。

skynet.register("hotfix_service")    → 全局名 "@hotfix_service"
skynet.name(".hotfix_service", ...)  → 本地名  ".hotfix_service"
                                          ↑ debug_console 只认识这个

修复:所有需要通过 debug_console 调用的服务,同时在 skynet.start 中添加本地名注册:

skynet.start(function()
    skynet.register("service_name")                -- 全局名(集群通信用)
    skynet.name(".service_name", skynet.self())   -- 本地名(debug_console 用)
end)

影响的文件:hotfix_service.lua、test_hotfix.lua、test_code_hotfix.lua。


7.2 broadcast_update 命令调用失败

现象:

[string "debug console"]:1: attempt to call a nil value (global 'broadcast_update')

根因:命令格式错误。debug_console 的 call 语法是:

call <服务名> "<命令>", {参数}

而不是直接执行一个 Lua 函数。

修复:正确写法:

call .hotfix_service "broadcast_update", {"chatroom/etc/test_config.lua"}

7.3 “Maybe forgot response session” 警告

现象:

[:0000000b] Maybe forgot response session 6 from :0000000c : @./chatroom/service/hotfix_service.lua:111

根因:debug_console 使用 skynet.call(带 session)发送命令,期望收到 skynet.ret 回应。但 hotfix_service.lua 的 dispatch handler 没有调用 skynet.ret。同时 clusteragent 跨节点转发消息时也会带上 session。

修复:在 dispatch handler 中捕获 session,有 session 时调用 skynet.ret:

-- 改前
skynet.dispatch("lua", function(_, _, cmd, ...)
    ...
    local ok, ret = pcall(f, ...)
end)

-- 改后
skynet.dispatch("lua", function(session, source, cmd, ...)
    ...
    local ok, ret = pcall(f, ...)
    if session ~= 0 then
        skynet.ret(skynet.pack(ret))   -- 把结果返回给调用方
    end
end)

关键点:session == 0 的是 cluster.send 异步消息,不需要返回。session ~= 0 的是 skynet.call/cluster.call,必须返回。


7.4 reload_config 参数类型错误

现象:

reload_config failed: bad argument #1 to 'dofile' (string expected, got table)

根因:debug_console 调用 call .hotfix_service "reload_config", {"chatroom/etc/test_config.lua"} 时,{...} 作为整体打包成 table 传给 handler。

修复:在 handle_reload_config 中解包:

local function handle_reload_config(configPath)
    if type(configPath) == "table" then
        configPath = configPath[1]
    end
    ...
end

八、集群跨节点问题

8.1 services 表按 addr 键导致 Agent 覆盖

现象:Controller 日志显示 SendAllAndSelf: sent to 1 services,但明明注册了两个 Agent(logic_agent 和 battle_agent)。battle 节点收到广播而 logic 节点没收到。

根因:[controller.lua](file:///home/djh/skynet-master/chatroom/lualib/controller.lua) 的 services 表以服务地址 addr 为 key:

services[addr] = { name = ..., node = ..., ... }

跨节点时,clusteragent 会将不同远程 Agent 的地址都映射到同一个本地 handle(如 14)。第二个 Agent 注册时 services[14] 覆盖了第一个。

logic_agent 注册 → services[14] = { name="logic_agent" }
battle_agent 注册 → services[14] = { name="battle_agent" }  ← 覆盖!

修复:改用唯一的 name 作为 key:

-- 改前
services[addr] = { name, addr, node, ... }    -- addr 跨节点不唯一

-- 改后  
services[name] = { name, addr, node, ... }    -- name 全局唯一

所有操作(Send、SendAll、UnregService、Heartbeat、CleanupTimeout、FindService)同步改为按 name 操作。


8.2 config_manager 未初始化

现象:

call .hotfix_service "getconfig"
→ 空表(只有 version=0)

根因:config_manager.Init() 只在 test_hotfix.lua(单进程模式专用服务)中调用。集群模式下 Controller 启动时未调用,currentConfig 和 configVersion 仍是默认值。

修复:在 hotfix_service.lua Controller 的 skynet.start 中添加:

config_manager.Init({
    version = 1,
    max_connections = 100,
    feature_a = true,
    feature_b = false,
})

九、状态管理问题

9.1 testModule 局部变量在服务重启后丢失

现象:

codetest_get 有时返回 nil, "not loaded"——似乎热更功能"修了几次"后突然不可用。

根因:

testModule 是 hotfix_service.lua Controller 块内的模块级局部变量:

-- if mode == "controller" then 块内部
local testModule        -- ← 服务启动时初始化为 nil

每次修改 hotfix_service.lua 本身(加调试日志、修复 bug),skynet 会重启这个 service。服务重启后 testModule 回到 nil:

服务启动 → testModule = nil
    │
    ├─ codetest_load → testModule = require("test_module")  ✅
    │
    ├─ 修改 hotfix_service.lua → 服务重启 → testModule = nil
    │
    └─ codetest_get → testModule 为 nil → "not loaded"  ❌

修复:handle_codetest_get 和 handle_codetest_reload 添加自动恢复逻辑:

local function handle_codetest_get()
    if not testModule then
        local ok, mod = pcall(require, "test_module")
        if not ok then return nil, "not loaded" end
        testModule = mod
    end
    return { version = testModule.getVersion(), ... }
end

十、问题速查表

问题现象关键词根因类别修复点
arg 为 nilattempt to index a nil value (global 'arg')Lua 5.3 兼容性加 local arg = {...}
log.infof 不存在attempt to call a nil value (field 'infof')API 不匹配logger.lua 加适配层
inspect 不存在attempt to call a nil value (global 'inspect')依赖缺失新建 inspect.lua + require
cluster 找不到module 'cluster' not found路径错误require "skynet.cluster"
skynet.register 不存在attempt to call a nil value (field 'register')模块未导入require "skynet.manager"
“Not a valid name”debug_console 报错全局名 vs 本地名skynet.name(".xxx", ...)
broadcast_update nilattempt to call a nil value (global '...')命令语法错误用 call .xxx "cmd", {args}
“forgot response session”日志 warningdispatch 未返回结果skynet.ret(skynet.pack(ret))
reload_config 类型错误string expected, got table参数打包方式type 检查 + 解包
Agent 覆盖只有一个节点收到广播addr 不唯一services 改用 name 键
config_manager 空getconfig 返回空表未初始化Controller 加 Init()
test_module 找不到module 'test_module' not found文件路径移到 lualib/ 目录
同名文件冲突加载旧版 test_module多份文件删除旧文件
require 热更无效reload 后返回旧版本C 层 codecacheskynet.cache.clear()
testModule 丢失“not loaded” 报错服务重启自动恢复逻辑

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/weixin_40453090/article/details/168410715

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--