关于skynet_fly热更新实现

实现思路

可以先看看这篇关于skynet做服务热更新
这篇可以说是最初的实现版本,最新的完整实现

这里我主要阐述各方案比较之后觉得较好的方案三实现,我在skynet_fly也决定使用方案三,接下来进入正题。

主要思想围绕新服务替换旧服务的方案,通过讲述如何热更rpc调用服务切换旧服务退出这4个点展开。
可以结合代码看文档,这样应该会更清晰,表达能力有限♥(ˆ◡ˆԅ)

  • 如何热更
    通过设置关闭skynet.codecache,这样launch新服务的时候加载的就是新代码了。不过我不想写每个服务的时候都去做这个操作,我就抽象了一个服务,我在skynet_fly封装了hot_container.lua服务 路径service/hot_container.lua。我叫它热更服务容器
    这个服务启动的时候要传递MODULE_NAME,也就是要加载的可热更模板的代码实现。
    hot_container会加载MODULE_NAME名称的lua代码文件,MODULE_NAME需要返回一个CMD命令表,也就是skynet lua消息的命令,必须实现start,exit命令。这样就是实现了可热更容器的抽象。
    hot_container是可热更容器的意思。通常我们热更模块实现代码命名为xxx_m.lua,我希望这成为skynet_fly的命令规范,并且放在命名为module的文件夹下。

  • rpc调用
    skynet的rpc调用通过服务id或者服务别名调用。可热更服务不能简单的通过服务别名调用,因为可能出现旧服务和新服务同时存在的情况,我们需要一个管理记录可热更服务的服务,如此就诞生了contriner_mgr.lua服务 路径service/contriner_mgr.lua,我叫它容器管理员
    以及给需要rpc调用的客户配套的lua模块代码contriner_client.lua 路径lualib/contriner_client.lua,我叫它容器客户端
    容器管理员提供了启动热更服务关闭热更服务关闭所有热更服务查询热更模块id监听热更模块取消监听模块,5个命令,容器管理员服务是不可以热更的。

  • 启动热更服务
    load_modules MODULE_NAME,通过传入module_name,contriner_mgr会加载loadmodsfile配置的文件,拿到此模板的配置,然后启动服务,启动完毕再通知旧服务exit,接下给监听的服务下发新的服务id和版本号。

  • 关闭热更服务
    kill_module MODULE_NAME,比如我们产品可能有些功能不要了,就可以关闭该服务了。

  • 关闭所有热更服务
    kill_all,停服时使用,需要做停服的数据收尾处理。

  • 查询热更模块id
    query MODULE_NAME,查询模块服务id,在contriner_client中首次加载使用查询之后挂载监听。

  • 监听热更模块
    watch MODULE_NAME,监听模块。

  • 取消监听模块,
    unwatch MODULE_NAME,取消监听。

容器客户端封装了对了热更管理员的交互我们在使用时感觉不到热更管理员♥(ˆ◡ˆԅ)的存在。
容器客户端是使用面向对象的方式实现的,为了方便每个rpc调用对象有自己的服务切换处理
提供了 newmod_sendmod_callbalance_sendbalance_callbroadcast方法。

  • new
    创建一个rpc调用客户端,传入想要联系的MODULE_NAME和新服务切换的检查函数。
    MODULE_NAME服务地址本地没有的情况下会去query 容器管理员,并且watch监听。

  • mod_send
    通过取模自己服务id映射一个热更服务id,然后对应id skynet.send消息。

  • mod_call
    同mod_call,不过是调用call。

  • balance_send
    简单轮询负载均衡,send消息

  • balance_call
    简单轮询负载均衡。call消息

  • broadcast
    广播 send消息

以上发消息函数调用之前都会检测是否切服

contriner_client还重写skynet.eixt 函数,在退出前注销监听

  • 服务切换
    服务的实现就是通过contriner_client中的new函数传入is_can_switch函数,每次发消息都会调用这个函数。

  • 旧服务退出
    容器管理员launch新服务后会发送exit消息给旧服务,旧服务自己决定什么时候退出。

监听实现思路参考了skynet.sharedata实现,看源码一度怀疑监听机制会有次序bug,觉得在query和monitor之间插入一个load处理的话就暂时收不到新的配置了。不过后面看着看着想通了。
传入对象与新对象不同的话,会直接返回新对象。
在skynet_fly中我有引入一个版本号来处理这种情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function CMD.monitor(name, obj)
local v = assert(pool[name])
if obj ~= v.obj then
sharedata.host.incref(v.obj)
return v.obj
end

local n = pool_count[name].n + 1
if n > pool_count[name].threshold then
n = n - check_watch(v.watch)
pool_count[name].threshold = n * 2
end
pool_count[name].n = n

table.insert(v.watch, skynet.response())

return NORET
end

如何使用

可以参考查看examples下的hot_module5示例,它是一个简单的数字炸弹游戏,热更是使用可以分为2部讲解,如何启动如何rpc调用

  • 如何启动
    • 第一步启动容器管理员
    • 第二步编写load_mods.lua
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      return {
      service_m = {
      launch_seq = 1,
      launch_num = 1,
      mod_args = nil,
      default_arg = {
      player_num = 2,
      min_num = 1,
      max_num = 100,
      }
      },

      agent_m = {
      launch_seq = 2,
      launch_num = 2,
      mod_args = {
      {
      player_id = 10001,
      nickname = "张三",
      },
      {
      player_id = 10004,
      nickname = "李四",
      hello = {a = 1,b = 2}
      },
      }
      }
      }
      这是load_mods.lua的一个编写示例,像service_m、和agent_m是需要启动的热更模板。
      每个模块有4个参数可以配置。
  • launch_seq 首次启动顺序,从小到大。
  • launch_num 启动数量。
  • default_arg 默认传入配置,热更模板内的start函数接收,当mod_args中没有对应服务下标的配置时启用。
  • mod_args 是一个配置数组,为了应对相同服务可能配置不通的情况。

有了mod_config之后,我们在main.lua中通过mod_config.lua启动。

1
2
3
4
5
6
7
8
9
local skynet = require "skynet"
local contriner_launcher = require "contriner_launcher"

skynet.start(function()
skynet.error("start hot_module5!!!>>>>>>>>>>>>>>>>>")
contriner_launcher.run()

skynet.exit()
end)
  • 如何rpc调用
    rpc调用使用非常简单易用。

    1
    2
    3
    4
    5
    6
    7
    8
    local service_client = contriner_client:new("service_m") --新建一个联系service_m的对象
    --因为service_m可能启动多个,使用模除以的方式去映射一个服务id
    service_client:mod_send(cmd,...)
    service_client:mod_call(cmd,...)
    service_client:mod_call(cmd,...)

    local agent_client = contriner_client:new("agent_m") --新建一个联系agent_m的对象
    agent_client:mod_send(cmd,...)

    contriner_client会帮我们管理好对应服务的id。

  • 功能改动
    在实现mysql连接模块的时候发现同一模板多个服务可以连接不同的数据库,当rpc客户端想要联系指定的数据库时并没有很好办法,于是就扩展了instance_name,使module_name下多了一个二级目录,扩展后的contriner_clientmod_call_by_name等等。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    local game_client = contriner_client:new("mysql_m","game")
    local ret = game_client:mod_call_by_name("query","show tables;")
    log.error("ret :",ret)

    local sql_str = "insert into user(id,name) values('1','skynet_fly');"
    log.info("game insert:",game_client:balance_call_by_name("query",sql_str))

    local sql_str = "select * from user where name = 'skynet_fly';"
    log.info("game select:",game_client:balance_call_by_name("query",sql_str))

    local hall_client = contriner_client:new("mysql_m","hall")
    log.info("hall select:",hall_client:balance_call_by_name("query",sql_str))
    log.error("mysql_test_m over!!!")

    对应的启动配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51

    mysql_m = {
    launch_seq = 4,
    launch_num = 4,
    mod_args = {
    {
    instance_name = "game",
    db_conf = {
    host = '127.0.0.1',
    port = '3306',
    max_packet_size = 1048576,
    user = 'root',
    password = '123456',
    database = 'gamedb',
    }
    },
    {
    instance_name = "game",
    db_conf = {
    host = '127.0.0.1',
    port = '3306',
    max_packet_size = 1048576,
    user = 'root',
    password = '123456',
    database = 'gamedb',
    }
    },
    {
    instance_name = "hall",
    db_conf = {
    host = '127.0.0.1',
    port = '3306',
    max_packet_size = 1048576,
    user = 'root',
    password = '123456',
    database = 'halldb',
    }
    },
    {
    instance_name = "hall",
    db_conf = {
    host = '127.0.0.1',
    port = '3306',
    max_packet_size = 1048576,
    user = 'root',
    password = '123456',
    database = 'halldb',
    }
    },
    }
    },

    这样就可以配置rpc调用不同的数据库了。
    之后在实现examples/digitalbomb数字炸弹时发现需要指定玩家id去映射服务,还有外部需要拿服务ID的需求,于是又扩展了对应函数。

总结

这里就讲解完了,如果有不明白或者觉得不好的地方,欢迎提issues
下一遍我准备写一下skynet_fly的配置脚本工具生成。
skynetfly源码地址


关于skynet_fly热更新实现
https://huahua132.github.io/2023/06/30/skynet_fly_ss/关于skynet_fly热更新实现/
作者
huahua132
发布于
2023年6月30日
许可协议