Skynet搭建的棋牌服务器实现部分Lua业务热更新

热更新最大的难点是状态同步,所以无状态的热更新是最容易实现的,本篇幅的热更新方案的约束条件就是不涉及状态同步的模块。

挑选出业务内善变的模块做热更新方案,从而提高开发效率,本篇幅是总结一下我工作当中的棋牌服务器是如何实现部分Lua业务进行平滑热更新的。

棋牌服务器是采用云风的开源服务器框架Skynet搭建的,具体的业务用Lua脚本开发。

目前项目实现了agent(客户端代理)和roomkeeper(游戏房间管理)2个大模块的热更新,每个agent对应一条连接的客户端,代理接收客户端的所有请求,大厅相关的业务拓展几乎是在这个模块上开发的,比如邮件、签到、充值等等;而roomkeeper则是管理玩家游戏的虚拟房间。

热更新方案:

单独开启一个服务通过忙等待的方式定时检测热更新配置文件是否被修改过,如果文件被修改,那么重新加载该配置文件,根据最新的配置内容来决定更新agent还是roomkeeper模块,亦或者是都更新。如果有模块需要更新,那么先调用skynet.clearcache(为什么调用clearcache请参考云风的这篇博客《在不同的 lua vm 间共享 Proto》),然后向相关服务发送热更新请求,相关服务在接收到请求后以合适的策略进行热更新。

agent服务存储的玩家状态不在热更新方案范畴,业务逻辑的相关处理模块会加入热更新。

agent模块热更新是通过agent池向所有agent服务发送hotupdate请求的方式,agent服务接收到消息后,在skynet.queue执行队列里获得执行权时,把需要更新的业务模块通过package.loaded[module_name] = nil措施清理掉缓存,然后重新require,这样就完成了热更新。

在skynet框架中,如果agent服务在处理某请求过程当中发生了阻塞调用(比如skynet.call/cluster.call),那么当前coroutine会被挂起,并且该agent服务照样可以处理其他请求。如果阻塞调用的下一行调用了一个全局方法globalFunc,假设服务这时收到热更新请求,而热更新的内容包括删除globalFunc方法,那么当被挂起的coroutine被唤醒继续执行下一行代码(也就是调用globalFunc方法),因为globalFunc方法在热更新的时候删除了,找不到就会抛出异常。

为了避免这类问题,我们引用了skynet.queue,把agent服务接收的消息的响应处理函数都压入skynet.queue执行队列,这样每次调用它都能得到一个新的临界区,即上一个函数处理没有返回,下一个函数也是不可能执行的,这样就能保证消息处理函数执行的时序性。

roomkeeper模块热更新则是使用一种灰度平滑切换的方式进行。我们采用的是用一个数组来管理两个room服务对象(每个room对象类似于一个酒店,游戏虚拟房间由room对象创建,类似于在就酒店开房),常规情况下,其中一个room服务对外工作,另一个则处于空闲状态,当roomkeeper服务接收到hotupdate请求时,首先把空闲状态的room服务杀死,通过package.loaded[module_name] = nil措施清理掉模块缓存,再新创建一个room服务加入到这个数组中(因为模块代码已经更新,并且缓存里的旧模块也清理掉了,新创建的服务肯定是用最新的模块代码加载的),并且在room服务里预生成一批roomobject对象(roomobject是游戏虚拟房间对象,房间解散了就释放),然后把新创建的room服务切换为工作状态,而原本工作状态的room服务则设置为关闭状态,玩家需要房间进行游戏都是从处于工作状态的room服务里获取roomobject,如果room服务预生成的那批roomobject不够用,则新建一批roomobject,然后分配一个空闲的roomobject给玩家打牌。

roomkeeper热更新处理流程是通过定时器周期性检测所有room服务是否更新完毕,处于关闭状态的room服务如果有roomobject还有牌局正在进行,不能直接杀死room服务,必须在room服务所管理的所有roomobject里都没有真实玩家的情况下才能杀死重新生成(因为前面切换为最新模块的room服务已经处于工作状态了,所有后面切换完成的room服务会将关闭状态修改为空闲状态)。

待两个room服务都完成了从旧模块切换为新模块的过渡工作,热更新就完成了。

 

你可能感兴趣的:(Skynet搭建的棋牌服务器实现部分Lua业务热更新)