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

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

一、问题总览

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

# 问题 现象 根因
1 codetest_reload 热更不生效 修改代码后触发重载,返回值仍是旧版本 只清了 Lua 层缓存,没清 C 层 codecache
2 codetest_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.c L1221-1226
loadfile 被劫持(先查缓存) skynet/3rd/lua/lauxlib.c L1341-1371
cache.clear() 实现 skynet/3rd/lua/lauxlib.c L1392-1397
全局 require 被 skynet 替换 skynet/lualib/loader.lua L48
skynet 自定义 require skynet/lualib/skynet/require.lua L24-97
skynet.cache 暴露 skynet/lualib/skynet.lua L49

三、问题一: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 层缓存的工作原理

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 层缓存:

lua 复制代码
-- 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) 的模块级局部变量:

lua 复制代码
-- 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")。

lua 复制代码
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 服务中实现代码热更新,必须按以下顺序清除所有缓存:

lua 复制代码
-- ============================================
-- 代码热更新 - 完整流程
-- ============================================

-- 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 全局变量,需显式通过 ... 获取。

修复:

lua 复制代码
-- 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 的文件添加:

lua 复制代码
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

触发命令:

lua 复制代码
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 中添加本地名注册:

lua 复制代码
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 函数。

修复:正确写法:

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:

lua 复制代码
-- 改前
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 中解包:

lua 复制代码
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:

lua 复制代码
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:

lua 复制代码
-- 改前
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 中添加:

lua 复制代码
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 块内的模块级局部变量:

lua 复制代码
-- 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 添加自动恢复逻辑:

lua 复制代码
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 为 nil attempt 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 nil attempt to call a nil value (global '...') 命令语法错误 用 call .xxx "cmd", {args}
"forgot response session" 日志 warning dispatch 未返回结果 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 层 codecache skynet.cache.clear()
testModule 丢失 "not loaded" 报错 服务重启 自动恢复逻辑
相关推荐
andrsted1 小时前
练题簿在线免费刷题:将资料、Word 和 PDF导入小程序练习
学习·微信小程序·小程序·pdf·word·学习方法
新手小新1 小时前
通信工程师学习笔记5-通信系统
笔记·学习
我命由我123451 小时前
刚性防水与柔性防水
学习·职场和发展·产品运营·求职招聘·职场发展·学习方法·产品
dtq04241 小时前
C++3 - 分支语句if和string变量
开发语言·c++·学习
承渊政道1 小时前
Linux网络学习【深入理解应用层HTTP协议、Cookie与Session机制及HTTPS协议原理】
linux·网络·学习·http·cookie·session
LuminousCPP1 小时前
数据库学习笔记(二):MySQL 基础操作入门|在 CentOS Stream 10 上连接、建库与建表
数据库·经验分享·笔记·学习·mysql·centos
艾莉丝努力练剑2 小时前
【AI大模型接入SDK】ChatServer接口实现
c++·人工智能·学习·ai·架构·大模型·sdk
lhh_qrsly5 小时前
HTTP / HTTPS / MQTT / MQTTs(嵌入式学习版)
学习·http·https
xier_ran1 天前
【infra之路】AI 编译器学习笔记(十三):AI 编译器全景——TVM、XLA、IREE 与其他
人工智能·笔记·学习