【OpenKruiseGame】云原生OpenKruiseGame实现游戏动态热更新
一、 简介
在OKG的概念里,一个游戏服(GameServer)中可以包含多个容器,每个容器功能作用不尽相同,各自对应不同的容器镜像。当然,一个游戏服也可以只包含一个容器。游戏服包含一个容器、还是包含多个容器对应着两种不同的架构思想。
单容器的游戏服更贴近虚拟机的运维管理方式。游戏服的单容器中存在多个进程,多个脚本文件或配置文件,游戏服引擎常驻进程通常会通过构建新的容器进行实现新版发布,而新的脚本、资源、或配置的更新往往依赖对象存储的挂载、或是自研程序的动态拉取。并且更新的情况由业务自行判断,整个过程以非云原生的方式进行。我们称这种游戏服为富容器。富容器热更新的问题在于:
- 无法对脚本/资源/配置文件进行云原生化的版本管理。由于容器镜像并没有发生变化,运维人员无法确认当前应用的版本。游戏上线后小版本的迭代十分频繁,当故障出现时,没有版本管理的系统将难以定位问题,这很大程度上提高了运维复杂度。
- 更新状态难以定位。即使对容器中的文件进行了更新替换,但执行重载命令时难以确定当前热更文件是否已经挂载完毕,这种更新成功与否的状态维护需要交给运维者额外管理,也一定程度上提高了运维复杂度。
- 无法灰度升级。在更新时,为了控制影响面,往往需要先更新低重要性的游戏服,确认无误后再灰度其余游戏服。但无论是对象存储挂载的方式还是程序拉取的方式很难做到灰度发布。一旦全量发布出现问题,故障影响面是非常大的。
- 在容器异常时,pod重建拉起旧版本的镜像,热更文件并未能持续化保留。
- 针对游戏服热更场景,更理想的做法是使用多容器的游戏服架构,将热更新的部分作为sidecar容器与main容器一同部署在同一个游戏服(GameServer)中,二者通过emptyDir共享热更文件。更新时只需更新sidecar容器即可。这样一来,游戏服的热更将以云原生的方式进行:
- sidecar容器镜像具有版本属性,解决了版本管理问题。
- Kubernetes容器更新成功后处于Ready状态,能够感知sidecar更新是否成功。
- OKG提供多种更新策略,可按照发布需求自行控制发布对象,完成灰度发布。
- 即使容器异常发生重启,热更文件随着镜像的固化而持续化保留了下来。
二、热更新的技术原理和使用场景
在原生kubernetes原生的workload中(例如deployment, statefulset等),应用的更新升级是通过更改资源对象中的image字段实现的,但原生的workload在更新image之后会重建pod,这样便无法实现热更新。
OKG的GameServerSet资源提供了原地升级的能力。无需重启pod即可更新游戏服,实现热更新。
如下图所示,蓝色部分为热更新部分,橘色部分是非热更部分。Game Script 容器从版本v1更新到版本v2后,不会重建整个Pod。橘色部分不会受到影响。Game Engien正常运行。

2.1 技术原理解析
在原生的kubernetes中,当更新工作负载使用的镜像时,此工作负载下的所有pod都会重新构建,开启新的pod生命周期,这种情况下无法实现热更新。OKG的GameServerSet在更新负载使用的镜像时,只会重启pod中的container,而不影响pod生命周期。
实际上,原地升级是应用了kubernetes本身的能力,在原生kubernetes中,如果修改具体某1个pod的镜像,pod的生命周期不会中断,而是重启pod中对应的容器。

在上面显示的例子中,我们修改了具体pod的镜像,从pod的RESTARTS和AGE来看,pod只重启了容器而生命周期在持续
OKG的原地升级正是利用了kubernetes的这一个能力
2.2 具体使用场景
2.2.1 场景1-游戏服原地升级热更新
在构建游戏服的时候可以使用富容器的形式,在负载pod中有2个或多个容器,其中main容器不变,而其他容器负责更新配置文件,脚本文件,资源数据等。结合main容器进程本身的热加载能力,在更新游戏服的过程,保证main容器进程不断。这样pod无需重启,以实现热更新。在OKG中通过直接修改GameServerSet的image配置就可以实现热更新
2.2.2 场景2-老游戏不变新游戏服热应用
游戏服有一些测试场景,需要旧游戏服务版本和新游戏服版本同时存在。更新时希望旧有的游戏服不变,而新扩容出的游戏服使用新版本。在OKG的GameServerSet中指定updateStrategy,这样可以在更新负载镜像之后现有的pod都不做变化,而只有在新扩容pod时和主动删除pod时在新建pod中使用新版本镜像。

apiVersion: game.kruise.io/v1alpha1
kind: GameServerSet
metadata:
name: minecraft-test
namespace: default
spec:
replicas: 3
updateStrategy: # 升级策略
type: OnDelete # 选择升级策略为"OnDelete",实现老游戏服不变,新游戏服升级新版本
gameServerTemplate:
spec:
containers:
- image: swr.cn-north-4.myhuaweicloud.com/cpaas/minecraft-demo:1.12.2
name: minecraft
2.2.3 场景3-分区分服场景的定向热更新
在一些PVE游戏中,如MMORPG,需要对已有的游戏服进行定向更新或分批次更新,在多个已有的游戏服中只更新其中一部分,而其他游戏服保持不变。在这种场景可以通过修改指定GameServer资源中的镜像配置来更新GameServer资源对应的游戏服。

...
spec:
deletionPriority: 0
opsState: None
updatePriority: 0
# 在下面新增containers字段
containers:
- name: minecraft # pod中游戏服容器name
image: swr.cn-north-4.myhuaweicloud.com/cpaas/minecraft-demo:1.13.0 # 新版本的镜像
...
三、 操作
3.1 准备
1.准备CCE集群

2.配置kubeconfig
3.安装helm
wget https://get.helm.sh/helm-v3.18.5-linux-amd64.tar.gz
tar -zxf ./helm-v3.18.5-linux-amd64.tar.gz
cp -a ./linux-amd64/helm /usr/local/bin/helm
3.2 安装OpenKruiseGame
使用helm在集群中安装OpenKruiseGame
# 添加helm源
helm repo add openkruise https://openkruise.github.io/charts/
# 安装OpenKruise
helm install kruise openkruise/kruise --version 1.8.0 --set manager.image.repository=openkruise-registry.cn-shanghai.cr.aliyuncs.com/openkruise/kruise-manager --set featureGates="PodProbeMarkerGate=true"
# 安装OpenKruiseGame
helm install kruise-game openkruise/kruise-game --version 1.0.0 --set image.repository=registry-cn-hangzhou.ack.aliyuncs.com/acs/kruise-game-manager
3.3 游戏服原地升级热更新
OKG的原地升级能力可以在不影响pod生命周期的情况下,即不重建pod的情况下更新游戏服。再结合游戏服应用本身的热加载能力,以实现热更新。
使用2048网页版作为示例,这个2048使用了nginx加载javascript脚本运行。在示例中将结合nginx进程的热加载能力,展示如何在不影响游戏服生命周期的前提下更新游戏版本。
游戏服使用两个容器,app-2048为主容器,sidecar为辅助容器。两个容器通过emptyDir共享文件目录。
sidecar启动时将存放热更文件的目录下文件/app/js 同步到共享目录下 /app/scripts/,同步后sidecar执行sleep不退出。
app-2048容器使用 /var/www/html/js 目录下的游戏脚本

apiVersion: game.kruise.io/v1alpha1
kind: GameServerSet
metadata:
name: gss-2048
namespace: default
spec:
replicas: 1
updateStrategy:
rollingUpdate:
podUpdatePolicy: InPlaceIfPossible # 使用原地升级策略
network:
networkType: HwCloud-CCE-ELB # 对接华为云CCE ELB
networkConf:
- name: PortProtocols
value: "80/TCP"
- name: kubernetes.io/elb.class # ELB实例的类型,以实际使用的ELB 类型为准
value: performance
- name: kubernetes.io/elb.id # ELB实例的ID, 以实际使用的ELB ID为准
value: 2f0f...1d78
gameServerTemplate:
spec:
containers:
- image: swr.cn-north-4.myhuaweicloud.com/cpaas/2048:v1.0 # main 容器,使用2048镜像
name: app-2048
volumeMounts:
- name: shared-dir
mountPath: /var/www/html/js
- image: swr.cn-north-4.myhuaweicloud.com/cpaas/2048-sidecar:v1.0 # sidecar容器,用于更新游戏服脚本文件
name: sidecar
args:
- bash
- -c
- rsync -aP /app/js/* /app/scripts/ && while true; do echo 11;sleep 2; done # 将网页脚本文件导入共享卷中
volumeMounts:
- name: shared-dir
mountPath: /app/scripts
volumes:
- name: shared-dir
emptyDir: {}
生成1个gameserver以及对应的1个pod


访问网络模型对外暴露的地址可以访问到游戏页面。
接下来对游戏服进行热更新,将游戏结束时显示的字样改为"_ Game Over!"。编辑GameServerSet将sidecar容器的镜像tag修改为v2.0

kubectl edit gameserverset gss-2048
...
- image: swr.cn-north-4.myhuaweicloud.com/cpaas/2048-sidecar:v2.0
name: sidecar
...
修改后过一段时间查看pod,可以看到重启次数为1,刚才经过一次重启但pod的AGE没有减少。

kubectl exec gss-2048-0 -c app-2048 -- /usr/sbin/nginx -s reload
避免浏览器缓存的影响,在无痕模式下访问游戏页面。可以看到游戏结束时显示的字样已经改变

游戏结束的提示字样已经更新
3.4 文件更新后的热重载
在3.1的示例中,对单个pod使用exec的方式进行重载。在实际生产中,批量管理pod的场景下,使用exec的重载操作过于繁琐,可以借助一些其他方法实现自动化热重载。下面提供一种重载方式以供参考。
3.4.1 通过inotify跟踪热更文件目录
inotify是Linux的文件监控系统框架,inotify可以监听文件目录下的变化,进而根据文件变化触发更新。
使用inotify需要在容器安装inotify-tools
# Ubuntu 操作系统
apt-get install inotify-tools
# CentOs 操作系统
yum -y install inotify-tools
依以上述2048游戏为例,在原镜像基础之上,app-2048容器监听 /var/www/html/js/ 目录,当发现文件变化时自动执行重载命令。
脚本如下所示,在容器启动时执行即可。
inotifywait -mrq --timefmt '%d/%m/%y %H:%M' --format '%T %w%f%e' -e modify,delete,create,attrib /var/www/html/js/ | while read file
do
/usr/sbin/nginx -s reload
echo "reload successfully"
done
将上述程序固化至镜像中,构建出新的镜像2048:v1.0-inotify,再次实验(其他字段不变),将sidecar镜像从v1.0替换到v2.0后,会发现已经不需要手动输入重载命令已完成全部热更过程。 完整的yaml如下
实验之前先删除集群中已经创建好的GameServerSet
kubectl delete gameserverset gss-2048
kind: GameServerSet
metadata:
name: gss-2048
namespace: default
spec:
replicas: 1
updateStrategy:
rollingUpdate:
podUpdatePolicy: InPlaceIfPossible
network:
networkType: HwCloud-CCE-ELB
networkConf:
- name: PortProtocols
value: "80/TCP"
- name: kubernetes.io/elb.class # ELB实例的类型
value: performance
- name: kubernetes.io/elb.id # ELB实例的ID
value: 2f0f...1d78
gameServerTemplate:
spec:
containers:
- image: swr.cn-north-4.myhuaweicloud.com/cpaas/2048:v1.0-inotify
name: app-2048
volumeMounts:
- name: shared-dir
mountPath: /var/www/html/js
- image: swr.cn-north-4.myhuaweicloud.com/cpaas/2048-sidecar:v1.0 #热更时替换成v2.0
name: sidecar
args:
- bash
- -c
- rsync -aP /app/js/* /app/scripts/ && while true; do echo 11;sleep 2; done
volumeMounts:
- name: shared-dir
mountPath: /app/scripts
volumes:
- name: shared-dir
emptyDir: {}
在热更新时直接修改GameServerSet中sidecar容器的镜像,之后无需手动执行重载命令就可以更新游戏服。
3.5 老游戏服不变新游戏服热应用
在更新游戏服镜像的时候,已有的游戏不更新,新建的游戏服更新。
实现此场景需要配置GameServerSet的更新策略字段
apiVersion: game.kruise.io/v1alpha1
kind: GameServerSet
metadata:
name: minecraft-test
namespace: default
spec:
replicas: 3
updateStrategy: # 升级策略
type: OnDelete # 选择升级策略为"OnDelete",实现老游戏服不变,新游戏服升级新版本
gameServerTemplate:
spec:
containers:
- image: swr.cn-north-4.myhuaweicloud.com/cpaas/minecraft-demo:1.12.2
name: minecraft
创建出了GameServerSet资源

已经创建出游戏服pod,可以看到pod中使用的镜像都是1.12.2版本
修改游戏服镜像
使用kubectl edit命令修改GameServerSet中配置的镜像
# 编辑GameServerSet,修改镜像版本。镜像tag改成"1.13.0"
kubectl edit gameserverset minecraft-test
...
spec:
gameServerTemplate:
spec:
containers:
- image: swr.cn-north-4.myhuaweicloud.com/cpaas/minecraft-demo:1.13.0
name: minecraft
...

已有的pod没有变化
对GameServerSet进行扩容,将游戏服的个数由3个扩容到5个
kubectl scale gameserverset minecraft-test --replicas=5

扩容之后,已有的pod没有变化,新扩容出的2个pod使用了"1.13.0"tag的新版本镜像
3.6 分区分服场景的定向热更新
定向热更新可以在已有的游戏服中指定更新某几个游戏服,同时其他游戏服持续运行不受影响
通过配置游戏服对应的GameServer资源,定向更新游戏服版本
创建GameServerSet
apiVersion: game.kruise.io/v1alpha1
kind: GameServerSet
metadata:
name: minecraft-demo
namespace: default
spec:
replicas: 3
updateStrategy:
type: RollingUpdate
rollingUpdate:
podUpdatePolicy: InPlaceIfPossible
network:
networkType: HwCloud-CCE-ELB
networkConf:
- name: PortProtocols
value: "25565/TCP"
- name: kubernetes.io/elb.class # ELB实例的类型
value: performance
- name: kubernetes.io/elb.id # ELB实例的ID
value: 2f0f...1d78
gameServerTemplate:
# reclaimPolicy设定了GameServer的回收策略,支持"Cascade"和"Delete"。
# "Cascade"表示pod删除时对应的GameServer一并删除,"Delete"表示只有在GameServerSet副本数减少时GameServer才会被删除。
# 为了保留在GameServer上的配置,要使用"Delete"。
reclaimPolicy: Delete
spec:
containers:
- image: swr.cn-north-4.myhuaweicloud.com/cpaas/minecraft-demo:1.12.2
name: minecraft
查看创建出的pod,可以看到创建出的pod都使用了"1.12.2"版本的镜像

修改minecraft-demo-1和minecraft-demo-2两个GameServer
kubectl edit gameserver minecraft-demo-1
...
spec:
deletionPriority: 0
opsState: None
updatePriority: 0
# 在下面新增containers字段
containers:
- name: minecraft # pod中游戏服容器name
image: swr.cn-north-4.myhuaweicloud.com/cpaas/minecraft-demo:1.13.0 # 新版本的镜像
...
# 修改完成后保存退出
# 以相同方法更新minecraft-demo-2
kubectl edit gameserver minecraft-demo-1
...
spec:
deletionPriority: 0
opsState: None
updatePriority: 0
# 在下面新增containers字段
containers:
- name: minecraft # pod中游戏服容器name
image: swr.cn-north-4.myhuaweicloud.com/cpaas/minecraft-demo:1.13.0 # 新版本的镜像
...
# 修改完成后保存退出

修改后查看pod镜像,可以看到第2个和第3个pod的镜像已经热更新,即为minecraft-demo-1和minecraft-demo-2两个pod

- 点赞
- 收藏
- 关注作者
评论(0)