技术头条 - 一个快速在微博传播文章的方式     搜索本站
您现在的位置首页 --> 系统架构 --> 开发笔记 : 热更新

开发笔记 : 热更新

浏览:2283次  出处信息

    这几天我的工作是设计未来游戏服务器的热更新系统。

    这部分的工作,我曾经在过去的一个项目中尝试过 。这些工作,在当时一段时间与广州网易其他项目交流时,也对网易其他项目的设计产生过一些影响,之后,也在实战中,各个项目组逐步发展出许多热更新的系统来。

    我最近对之前所用到的一些方案,如修改 lua module 的加载策略,增加一些间接层,来达到热更新代码的系统设计做了一些思考。感觉在处理热更新这个问题时,还不够严谨。经过两天的思考,我按我的构思实现了新系统的雏形。

    在函数式编程语言中,热更新通常比较容易实现。erlang , lisp 都把热升级做为核心特性之一。函数副作用越小的语言,越容易做热升级:你只需要简单的把新写的函数替换回去就好了。

    对于纯粹的请求回应式的服务,做热升级也相对容易。比如大多数 web server ,采用 REST 风格的协议设计的网站,重启服务器,通常对用户都是透明的。中间状态保存在数据库中,改写了代码,把旧的服务停掉,启动新版的服务,很少有出错的。

    按我们现在的游戏服务器设计,大多数服务也会遵循这个结构,所以许多底层的子模块简单重启进程就可以了。但和游戏逻辑相关的一些东西却可能要考虑更多东西。

    我想把我们的系统分布设计实现,先实现最简单的热更新功能,再逐步完善。如果一开始就指望系统的任何一个部分都可以不停机更新掉老版本的代码是不太现实的,考虑的太多,容易使系统变的过于复杂不可靠。

    那么第一步,我们要实现的就仅仅是游戏逻辑有关的代码热更新。而不考虑更新服务器框架有关的模块。我想把这部分称为热修复,而不是热升级。主要用来解决运行时,不停机去修复一些 bug ;而不是在不停机的状态下,更新新版本。在这个阶段,也不考虑可以更新服务间的通讯协议,只考虑更处理这些协议的代码逻辑。

    做以上限制后,热更新系统实现起来就非常简单了。

    首先,我们的每个服务就是一个包分发器。主干代码在启动时,将一组组函数按模块加载入框架。框架分发消息包,经由分发器调用消息处理函数处理。热更新其实也是由一个消息来触发的。由于我们用 lua coroutine 的方式来分发每个消息处理包,在大多数情况下,收到热更新请求时的那一刻,之前的消息包都已经处理完毕了。只需要把消息处理函数的分发器重置,重新把新版的消息处理函数塞进去就可以了。但是,也有一些例外。

    我们的系统按功能拆分出了许多服务模块,大量依赖 RPC 来协同工作。所以在一个特定的消息包处理中,如果它通过 RPC 调用了另一个模块中的方法,那么这个消息处理函数是被框架挂起的。直接忽略掉这个处理函数的运行是可能有副作用的(如果没有副作用,我们可以简单的把函数参数记录下来,用这些参数重新调用新版函数即可)。那么更合理的方案是等待所有这些挂起的函数运行完毕,再启动热更新流程。

    实现起来并不难,在收到热更新请求时,替换掉消息分发器,截流住所有新的服务请求,记录在队列中。过滤出那些之前的 RPC 请求的返回包,交给框架处理。检测到所有旧请求完成后,切换回正常的消息分发器,并用新版本代码更新所有消息处理函数。

    当然这只是很理想的状况,是有可能因为同时更新几个服务,而服务间相互有 RPC 请求造成死锁。我的观点是,暂时先不考虑这些非常状况,等遇到问题了再一个个解决,让系统逐渐演化。这并非是不能一开始就设计出没有问题的系统,而是把系统的复杂度维持在一个可控范围内的举措。而且把热更新的设计目的限制在热修复 bug 的范畴,我认为是可以接受的。

    关于 timer 这种特殊的相应函数,我更倾向于直接停止所有 timer callback 回调,让重新启动的新版服务的初始化过程重新注册新的 timer ,这样比较干净。

    第二个重要点在于状态数据的继承。

    我们的模块在初始化时,往往会构造出一些环境表。消息处理过程是可以带状态的,状态信息就储存在这些状态表中。比如前面提到的 agent 的关注列表。

    所以我们应该避免直接在初始化代码中创建 table 出来,而应该调用框架提供的 API 创建这些表,并给它们起上名字。通过这些名字,可以在热更新时直接继承这些表,而不用重新创建。

    日后的需求可能是更多样的,因为版本更新后,原来的数据结构发生的改变,当发生数据结构改变时,我们需要在热更新的初始化阶段更新这些数据结构。

    对于这类问题,我的看法还是一样。先不必在设计实现热更新系统时包含入这些考量,而在真正遇到这个需求时,再仔细考虑如何演化这个系统。有更为实际的需求,比空想更容易评估怎样做的更好。

    第三个问题是逻辑层编写时,由于代码规模的需要,分拆出来的子模块代码。

    这些子模块可能和消息分发模式无关,单纯解决某些子问题,又不适合拆分成独立服务。

    我这次不希望去修改 lua 的模块加载机制。因为从 lua 5.1 到 5.2 的发展路线来看, lua 5.1 的模块加载机制本身就被过度设计了。我想我会单独为我们的系统设计一套简单的适应热更新的模块机制,而不是直接用 lua 基础库里的 require 。从 API 层面就把不可更新的模块和可更新的模块区分开。不可热更新的模块,比如系统框架等,用 lua 基础库里的 require 机制;可以热更新的则使用自己的。


    今天花了半天时间把以上想法实现,明天尝试合并入主干代码。

QQ技术交流群:445447336,欢迎加入!
扫一扫订阅我的微信号:IT技术博客大学习
© 2009 - 2024 by blogread.cn 微博:@IT技术博客大学习

京ICP备15002552号-1