我们目前的游戏第一次测试的时候笔记送匆忙,导致上线之后频繁更新。 比如BOSS战由于大区的人数和预期不一样导致的难度调整,或者是任务链或者数值调整,再加上一些BUG。

但是每次停服更新的话用户体验是比较伤的,所以后来就采取了一些措施来减少更新的停服时间。最后基本实现了不停服更新。

其实后来两次测试的服务器更新基本上是不停服的了,用户不太能感知到。即便能感知到也是极短的时间区间内,一般是几秒钟。今天有点空闲就把我们的做法分享一下吧。

负载均衡和去中心化

想要更新不停服,根本问题在于服务器切换的时间断内老服务不能停止,然后尽可能把新进用户转移到新服务器组里。 那么所有数据都必须可以自由转移,不需要固定绑在某一个或某一组服务器上。简单的说就是要去中心化,并且这是需要客户端做一些相应的支持的。

其实我们整个服务框架都是去中心化的全区全服的架构,只是为了策划需求分了大区而已,所有的大区实际上也都在一个大集群内。而这个去中心化的设计,整体上分好几层。

第一层:依赖DNS的负载均衡

第一层的负载均衡在客户端,为了简单起见直接使用了DNS。但是使用DNS也有一些问题,那就是DNS劫持。所以这要求客户端必须离线保存最近的DNS解析结果。 这样只要有一次DNS解析正确,那么顶多下一次被劫持的时候记录是老的,并没有走最近线路。

但是这个DNS解析结果并不是直接用,而是客户端会先向CDN拉取一个我们的登入认证服务器列表,然后这个登入认证服务器的地址可能是域名。

这是第一层负载均衡,由客户端随机挑选一个登入认证服务器进行连接。

特别重要的一点是,因为要做到无缝切换,所以客户端必须在一个登入服务器连接失败的时候自动换一个登入服务器重试。 这样即便登入服务器在维护中,只要保证有可用的即可。

第二层:登入服务器

第二层在登入服务器上,这里接受到客户端连接后悔检查客户端版本号和判定是否灰度之类的逻辑,然后入股需要的话向客户端发送更新信息。也就是说,更新流程在登入服务器上完成。

如果不需要更新,则可以根据某种策略选几个游戏服务器的地址给客户端,这里下发的也是多个地址。同样如果处于维护中不可用时,客户端也必须挨个试。

在选游戏服务器方面,理想情况下当然是选负载最低的,但是我们先还是用了简单的方案,直接随机。

A/B组切换

完成了第二层以后,其实不停服更新就比较简单了。我们的登入服务器是无状态的,然后再把一个大区内的服务器分A组和B组, 那么在A组服务时,登入服务器下发A组的游戏服务器地址,而如果需要更新的时候,新服务器发到B组,然后登入服务器重新加载配置,新的客户端下发B组即可。

这样要求所有能够执行AB组的服务器内的数据可以转移。对于玩家数据,其实就是在一个游戏服务器上被踢出,在另一个服务器上登入的流程。

我们完成AB组的服务器进程还有工会服务。所以我们的工会服务也是类似玩家数据的设计,即允许在一个服务器上踢出,另一个上面登入。

所以要完成AB组切换的话,相关的工具也需要准备好,即可以一次性把某一组配置切换掉。另外对发版本的流程也必须做一定的约束。基本流程是:

  1. 发布新版本客户端更新包(包括完整包和增量包)
  2. 发新的一组逻辑服务器并初始化
  3. 刷新逻辑服务器配置(这时候有些逻辑必须工会会切换到新服务器)
  4. 发布新服务器组配置和版本更新信息到登入服务器,然后reload。发布流程可能很慢,但是发布过程登入服务器不用停服,reload是很快的。
  5. 然后所有新登入用户就会切到新的一组服务器上了

强制切换通知

完成了上面的AB组切换以后,还有一个问题,就是老的有一批玩家还在老服务器上。 如果这批玩家触发需要重新登入的断线重连,或者重新登入的话才会分到新的一组服务器上,否则还在老的一组服务上。

对于这种情况,大部分情况其实不需要理会,等自然切换即可。但是有时候需要强制玩家切换怎么办呢? 也很简单,给游戏服务器设定维护模式,然后下一次有玩家发包的时候(最长也就心跳包的时间)通知客户端token失效,然后断开客户端的连接。 这时候客户端会走断线重连的流程,那么也就切换到新服务器了。

故障转移

有些服务是以接口的形式提供的,并不适合走AB组,这种服务一般不需要更新,但是总归会碰上需要更新的情况。 比如说聊天服务器,我们的聊天服务器是以频道的形式提供,频道根据Hash值然后分成slot分布在不同的进程上,类似redis cluster的设计。

这种服务本身有故障转移的功能,所以如果某些服务下线了,自动会转移到其他可用的进程上。比如刚才提到的聊天服务器,如果有个进程下线,那么该进程的数据和slots会自动转移到可用进程上。

那么这些服务器更新想要无缝切换,可以直接先下线一部分,再上线一部分这种方式来完成。切换几次就可以全部替换完了。

服务降级

还有些服务,可能不容易做成可以AB组切换,也不容易做自动化的故障转移。 比如排行榜服务器,并不容易做转移,因为数据量比较大,转移的时间比较长,而且这期间如果发生排名变化,会非常复杂。因为多个数据之间是互相关联的。

这种情况,大多数不是关键服务,并且也是更新频率不高的的服务,所以我们采取的方法是服务降级。即,更新期间只停掉这种类型的服务,然后其他的功能保持正常。

在我们这里就是,如果要发生更新排行榜,那么膜拜、竞技场会暂时不可用。客户端会收到*“维护中,XX功能赞不可用”*之类的消息,其他比如PVE副本、商场、邮件、聊天等等都是正常的。

然后等数据保存完毕并且更新完毕就可以重新开启了,我们发生这种更新的时候基本上可以控制时间在10-20分钟之间。

结尾

目前我们的不停服更新服务器的方案差不多就是这样了,我们这两次测试的服务器更新,AB组切换的更新大约执行了8、9次,服务降级的更新执行过2次,强制踢用户下线之执行过一次。 基本上用户都是无感知的。

这样就能减少更新的成本,特别是如果测试期间停服更新的话,即便是半夜,对用户留存还是蛮伤的。因为刚开始测试的时候都是些粉丝玩家,热情很高,停服2小时,热情就降一半了。

虽然说最好是能做到完全无缝升级,但是那样制作成本有点高。 比如有一些做热更新的方案,是使用动态链接库或者重载脚本层的,但是这种重载的时间会比较长一些,并且动态链接库的资源管理坑非常多。

另外我认为假定进程可能出故障的设计更能够处理异常情况,比如万一意外情况脚本或者动态链接库的宿主挂了,能够故障转移更合理一些。 Google的大数据系统就是假定所有的进程、服务器都有挂掉的可能,然后设计成某些物理机挂掉都不会影响正常服务,这才是比较好的设计。

所以还是要根据项目需要来做这个取舍吧,毕竟一个小型项目搞个超级重量级的方案也是成本过高,得不偿失不是?就像游戏服务器的聊天肯定不会做到QQ或者微信那么复杂是一样的。