前言

作为后端我们时常遇到服务的更新,而作为 http 服务的后端则一般是不用考虑这件事的,因为 http 服务的必须是无状态的,只需要在服务前加一个负载均衡就可以做到轻松的滚动更新,让用户无感知更新。但是我现在的工作的服务上包含的有状态的情况,但是更新又是必须的。

一、简单的构架介绍

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
+------------------------+                                                       +-------------+
|     +---------+        |                                                       |  client     |
|     |  room   |        |                                                     /->             |
|     |         |        |<-------                                        /ws--  +-------------+
|     +---------+        |        \---rpc call----       +------------+<--
|     +---------+        |                        \-------            |          +-------------+
|     |  room   |  game  |                               |    api     <----ws---->  client     |
|     |         |        |                          ----->            <\         |             |
|     +---------+        |         mq broadcast----/     +------------+ --\      +-------------+
|     +---------+        |     ----------/                                 ws
|     |         |        -----/                                              --\ +-------------+
|     |  room   |        |                                                      ->  client     |
|     +---------+        |                                                       |             |
+------------------------+                                                       +-------------+

这里不具体介绍我公司的游戏服务框架,用一个简单的模型来描述一下,apigame 都是需要支持多实例部署的,api 是连接客户端的实例,然后进入某个匹配房间后 api 会和 game 通信以对房间进行操作 game 则使用 mq 的订阅方式通知所有在对应房间的客户端进行推送消息,这里主要的问题在于用户进入房间后是有状态的,apigame 服务里都有着对应的 room 信息和游戏过程信息,这些都不好切换到 redis 这种地方去。

二、方案和思考

滚动更新需要的是:

  1. 服务必须是支持多实例运行的(用于新旧实例共存)。
  2. 服务健康检查是必须有的。
  3. 负载均衡必须支持滚动更新的流量切换。

2.1 去状态

迁移服务里的状态到 redis 这种地方,类似于 httpsession 方案,但是由于切换到 redis 之前的设计是在内存中快速访问,增加了故障点,并且也会增加用户操作时的对 redis 的操作,而且内存与 redis 的同步也很麻烦,而且需要修改大量的逻辑。

2.2 等待到状态结束

即让用户把这次游戏结束,再退出服务,这种方案有好处就是不用处理任何状态转移,对原有逻辑几乎没有什么改动,但是又回引入其他问题,那就是什么时候才能退出该服务,而且也不能无限等待,这里我选择这种方式。

三、实现方式

3.1 去状态

这个去状态实际上只是将一部分的复杂性转移到了另一个系统上,获取倒是很简单和 httpsession 一样的做法就行了,但是更新会很麻烦需要入侵到所有操作状态的地方,但是也就这么多了。

3.2 等待到状态结束

  1. 需要捕捉退出信号 SIGTERM, SIGINT
  2. 拒绝所有新连接,防止有负载均衡的流量跑到这个服务节点上。
  3. 并手动处理各个连接的退出和等待状态已经不需要时断开。
  4. 由于保持状态是两个服务,所以 game 服也要做相同的处理,并通知客户端。
  5. 由于这里是通过信号后等待所以需要让滚动更新管理器去等待。
  6. 例如 k8s 需要设置 spec.template.spec.terminationGracePeriodSeconds 为足够的长否则会直接发送 SIGKILL 信号。

参考