关于skynet做服务热更新

为什么要做热更新

那当然是因为产品需要更新功能,但是服务功能模块又可以有用户在使用,又不想停止服务又想更新功能,就有了需要热更新的需求。
无状态的服务非常好更新,例如查询服务,直接重启服务也可以,只是出现短暂的服务不可用。更新难的是有状态的服务,不能直接重启,重启就可能出现数据丢失了。

热更方式

  • 在原本的lua虚拟机中更新
    这种方式通过重新加载读取_G.package.loaded中的文件,去覆盖旧_env中的数据,要处理好table被引用、全局变量、闭包的upvalue,而且还会出现本身旧状态数据不适应新版本处理函数的情况,比如一个函数检测一个状态的值等不等于一个枚举值,旧版本枚举值等于1,新版本等于2,但是旧版本状态数据是1,这个时候你热更还需要抉择状态数据是用旧的还是要改成新的,而且有些状态数据是运行时玩家之间的API交互产生的结果,比如A玩家击杀了B玩家,B玩家状态是挂了,但是标记挂了的枚举在新版本改了,此时的热更肯定不符合预期,所以这种热更方式只适合更新bug。少量更改人为可预见行为,才使用。

比如skynet提供的inject注入方式。

  • 旧服务顶替新服务的方式
    这种方式处理起来不复杂而且稳定可控性强,有点类似重启服务然后更新,但是我们可以做到无缝衔接,新服务到岗之后再停止旧服务,传统的进程服务,可能就是启动新服务,然后把旧服务流量切换到新服务上面,等服务没有流量以后再关掉旧服务。
    skynet可以直接在进程内就实现这个需求,skynet服务lua沙盒就像一个一个微小的进程,我们在内部实现一个服务做出服务网关,记录所以可热更服务的地址,服务通过向网关注册时的名字联系,发消息都通过网关,然后我们关闭掉skynet.codecache,服务更新之后网关记录新服务地址,通知旧服务下岗,做到无缝热更。

不过还需要考虑新服务上线,旧服务还不能下岗的情况,比如一把游戏还没有结束。

新服务顶替旧服务实现方案的一些思考

所有方案都是利用关闭掉skynet的code.cache来实现业务代码的热更,过程中抽象三种角色。

  • 客户端
    所有需要RPC调用某个可热更服务的服务。

  • 热更服务管理员
    需要在恰当的时间进行切流,比如新服务启动了,也有可能新服务启动了,暂时不切流(比如一把游戏还没有结束)。

  • 可热更服务
    新服务启动了,旧服务要考虑在什么时候销毁。

  • 方案一
    热更服务管理员做网关,客户端发消息都通过热更服务管理员转发,热更服务管理员还负责启动新服务和通知旧服务关闭,这也是我在工作中实现并使用的方案。不过有些值得诟病的缺点,比如经过转发的消息从1v1变成了1v1v1,消息从2次打包解包变成3次打包解包,微微增加一点系统开销,还有就是没有提供AB面切流的实现,不方便给有状态的服务进行更新。

  • 方案二
    热更服务管理员做配置中心客户端发消息先尝试调用本地记录地址,如果没有或者失败(服务已经退出)或者下岗,询问配置中心要新的地址,可热更服务启动需要向配置中心注册,新的可热更新服务注册后,配置中心通知旧的可热更服务可以下岗了,旧的可热更服务根据自身情况决定什么时候退出。如果是有状态的需要维持到状态结束,这个可以通过可热更服务去控制,根据自身情况反馈客户端。比如一局游戏还没结束,正常处理请求,结束后用户想开始下一把,此时通知客户端我要下岗了,此方案就能解决消息多一次打解包,有状态服务的通讯保持。此方案没有什么诟病的地方有点类似redis切片集群方案,一个key不在这个redis节点,通知客户端move到目标节点地址,可以考虑配置中心通知旧服务下岗时接收一下新服务地址,到时候客户发消息过来,如果要move就可以直接切换地址,不需要向配置中心要了。

    方案二实现
    实现过程中,发现方案二有2个问题

    • 问题一
      给可热更服务发消息必须用call,不能用send,因为可热更服务需要反馈自身状态。稍微有点影响性能,但是比方案一还是要好。
    • 问题二
      客户缓存的旧地址长时间不访问,过了一段时间再访问,地址的目标服务可能已经更替了,不过这种情况要uint32自增的id走了一个轮回,如果机器上是4字节的话,也就是已经新建了16777215(0x00ffffff)个服务,才有可能重用服务id,虽然很难出现,但是还是要处理这种情况,这里考虑🤔加个版本号加模块名的校验。(测试的时候改下skynet源码的服务id分配,比如id到100就轮转),实现并增加了测试代码hot_module3
  • 方案三
    热更服务管理员还是做配置中心,客户端发消息用本地记录地址,如果没有客户端只要询问一次配置中心地址,然后通过sub/pub机制更新新服务地址,此时就会有一个问题,客户拥有新,旧地址,需要考虑需不需要继续和旧地址通信,此时从方案二的服务切流变成客户切流。
    方案三实现
    方案三的好处就是解决了方案二每次使用call的问题。

    • 客户端自己决定什么时候切换到新服务(一般就是状态结束了切换)
    • 服务 服务就是看自己什么时候退出(一般就是没有用户状态了退出)

关闭codecache的性能影响测试

skynet.codecache开启状态是把加载(require )过得lua代码文件缓存到内存,下次其他服务再加载的时候就直接用缓存就行,不需要再去读取代码文件了。
我们利用关闭掉codecache来实现热更只会稍微影响新服务启动的速度。
code_cache_test

cache情况下启动1万个服务需要2.65s
关闭cache情况下启动1万个服务需要3.69s
通过测试,新服务的启动速度下降28%,影响不大。

总结

通过多方案的实现分析,最终确定方案三在性能和灵活性上面最具优势,后续决定在方案三的基础上补充,一键检测热更功能(有变动才热更)。


关于skynet做服务热更新
https://huahua132.github.io/2023/05/22/think/reload/
作者
huahua132
发布于
2023年5月22日
许可协议