灰度更新

在手游的框架中实践灰度更新的流程

Louis Huang

6 minute read

灰度更新

灰度更新,又称红蓝部署 (Blue-Green deploy) 他的核心思想是在同一个服务集群中,通过版本的不同,同时部署两套或多套服务组件。然后逐渐的将访问流量从旧版本切换到新版本上,如果在流量转移的过程中遇到了任何问题,可以随时回滚,保证整个系统可以持续提供服务。

红蓝部署有很多的实现方式,最常见的就是使用负载均衡的机制,将不同版本号的应用服务归类到不同的负载均衡节点上,通过动态的重载负载均衡器的配置,可以让流量分布到新加入的高版本号的节点,或相反的操作。这里有一遍解释的非常详细的文章:https://martinfowler.com/bliki/BlueGreenDeployment.html

红蓝更新示意图

通过灰度更新的方式,我们可以保证在不停机的情况下,持续的将我们的更新内容滚动的发布给用户,可以说灰度更新是整个项目可以做到持续交付、自动化发布的基石。本文主要针对在手游环境下,实践灰度更新发现的一些问题以及总结的经验进行讨论。

游戏服务设计

看起来使用灰度更新的机制非常轻松,并且有很多好处,可惜不是所有的事情都这么简单,游戏服务有其特殊性导致了我们的灰度更新实现起来会有难度。

通过第一段的介绍,大家会发现一个问题,如果我们想要服务器可以被安全的灰度,就需要保证这些服务是无状态的,也就是说,当一个用户使用蓝色版本的服务器时,服务器上不会存储这个用户的任何临时状态数据,这样他的下一个请求被路由到任意新的红色版本服务器上,都不会有任何问题。

大多数的 Web 应用都可以实现这个无状态的要求,基本上 Web 服务器使用多进程的方式(Apache、Nginx)处理用户请求,每个 HTTP 请求都是通过某种负载均衡的机制交给一个独立的进程来处理,很自然的就要求服务单元内部不能有临时状态,但是游戏服务器就很难做到这点。

游戏的业务逻辑比较复杂,各种玩法可能会要求在客户端的一个请求下,访问到这个玩家身上不同系统的各种数据,例如一个简单地装备强化的请求,就有可能要关心用户的排名数据,当前服务器的强化概率设定,用户背包中强化消耗品的数量,等等。同时复杂的逻辑也会产生大量的中间状态,如果要求游戏服务器不持有状态,这就会极大的增加游戏服务器的编写难度,任何不小心的疏漏都会产生逻辑的问题,并且这些问题还非常难以发现,因为在开发调试的环境下往往只有一台游戏服务器,所有的请求都不会转移到其他的节点上,所以没有任何问题。

就算是我们定义了非常完备的数据访问、存储的中间层,保证所有的数据都来自外部的数据服务,所有的中间状态也都已经存储到了外部的数据服务中,这样的无状态要求也会导致对于数据库的压力大增,就算是我们使用了 redis 、memcached 这样的内存数据库来记录这些临时数据,也会对其产生大量的压力,因为玩家的任意一个请求就可能导致这个用户的所有数据的读入和写入。

如果我们想要允许游戏服务器保持用户状态,那么我们就要保证,在任意的时刻一个玩家的数据只能被一个游戏服务器所拥有

一个玩家的数据如果正在被某个游戏服务器节点持有,那么他的所有请求都必须发送到这个节点上,否则用户的数据就会同时被多个服务节点持有造成他的数据版本不一致。只有一个玩家的数据在任何游戏服务节点上都没有被持有的情况下,他的请求才能路由到任意的游戏服务节点上。

灰度更新具体实现

在直接面向玩家的负载均衡器中,我们有设置一个当前的默认的游戏服务器版本号,并且我们会收集所有相同版本号的游戏服务器放入一个负载均衡池中,通过这个设置我们可以将所有玩家的请求转发到和默认版本号相等的服务器上进行处理,并且我们的游戏服务器在接受了一个玩家之后会在中心的内存数据库中记录这个唯一用户和当前服务器之间的映射关系。负载均衡器会将这个用户之后的所有请求都发送到这个服务器进行处理。

当用户长时间不在线之后,游戏服务器为了降低内存压力,会将这个用户的数据存盘并销毁所有状态,这个时候游戏服务器也会去维护在内存数据库中的用户唯一ID和服务器之间的映射,将其删除。等同于游戏服务器放弃了这个用户的控制权。这样这个用户再次进行请求的时候,会根据负载均衡的方式转发到任意空闲的游戏服务器进行处理。

游戏客户端连接服务器

如上图所示,当一个用户被负载均衡器分配到了 ID 为 1.0.1-1 的游戏服务器上之后,游戏服务器会将用户的唯一 ID 和自己服务器 ID 的映射关系写入一个 redis 服务器,这样以后这个用户的其他请求都会持续的路由到这个游戏服务器上。直到这个游戏服务器放弃了这个用户的数据,并且在 redis 中清除了这个映射关系,这个用户才会自由的被分配到其他游戏服务器上。

特权测试

往往我们会希望在新版本的服务部署之后,可以让某些特权玩家(通常是公司内容测试人员)优先与其他用户进入新版本的服务器,这样我们可以在面向大众之前进行简单的测试,并决定是否要将用户导向新的服务器。

我们在公司内部通过自己的 DNS 服务器修改服务器列表获取域名的解析,让特权机器可以获取到不一样的服务器列表,这样这些用户可以要求负载均衡将自己分配到具体某个版本的服务上,从而优先体验到新的服务器,并且所有的外部环境和数据都是和正式用户一致。

当特权测试通过之后,我们会将外部的默认版本号提升到新服的版本号,这样所有在老版本服务器上没有状态的用户,都会被分配到新服务器上。

客户端同步

在我们游戏运营的过程中,发现了一个更加复杂的需求,就是客户端热更新。客户端热更之后,往往需要服务器也有匹配的版本,这样才能正常的进行游戏,反之依然。于是我们就会要求游戏客户端版本和服务器版本同步。

这样我们对整个灰度更新的流程就增加了一个需求:一旦发布了需要同步更新的服务器或之后,我们会要求客户端必须也是热更到了最新版本之后才能进入。同时一旦热更过的客户端也不应该进入旧版本的服务器

为了满足这个需求我们在客户端和服务器上各做了相应的机制: - 客户端:当用户因为被服务器踢出游戏或是选择更换大区、帐号的行为时一定要重新走热更新流程 - 服务器:增加快速下线模式,当这个模式被打开时,客户端只要网络断开开始重连,就将用户踢出游戏,让其重新登录。

这样,当我们决定某次更新需要服务器客户端同步时,会将所有旧版本的服务器的快速下线模式打开。对于正在游戏的玩家来说,没有任何影响,他们可以在旧版本的服务器上正常游玩,但是一旦这个玩家断线或是选择重新登录,就会检测并安装新的热更包,同时也会被旧服务器踢出,进入新的服务器。

已知的缺陷

  • 当需要客户端服务器同步更新时,没有办法回滚已经放出的服务器,因为客户端已经更新到了最新的补丁包
  • 灰度更新时,原则上需要开启一倍的游戏服务器,因为你无法控制用户何时从旧服务器上下线并转移到新服务器上
comments powered by Disqus