如今,云原生技术在游戏行业的应用越来越广泛。笔者对此有浓厚兴趣,特地集中学习了一段时间。这篇文章既是记录学习过程的个人笔记,也是游戏服务器云原生的入门引导文章。读完这篇文章,可以对云原生在游戏行业的应用现状、游戏服云原生化的重难点及主流解决方案有初步了解。
首先要搞懂什么是云原生技术。云原生(Cloud Native)是指把程序部署到云平台而非传统机房上,同时在程序设计之初就以云的方式思考,充分利用云平台弹性和分布式的特点。
很多人把容器化等同于云原生技术,其实不然。应该说,容器化只是云原生中非常重要且必不可少的一环。其他概念还包括:微服务、DevOps、持续交付。而容器化是实现这些概念的基础,通过容器化,可以方便地完成微服务的细粒度拆分和部署,也可以通过YAML配置文件践行DevOps提倡的开发运维一体化的思维,还可以敏捷快速地修改程序并发布到线上。
我是从Docker官网和《深入浅出Docker》开始学习Docker的。这是典型的官方文档加指导书的学习模式,官方文档包含新手教程,照着教程把命令敲一遍就算入了门,在这过程中积累的问题再通过阅读书得到解答。
首先搞懂为什么要有Docker。最早的时候,直接在物理机上部署程序,为了保证程序平稳运行,需要购买明显超出性能需要的服务器,但是这样会造成明显的浪费。后来出现了虚拟机,在同一物理机上部署多态虚拟机,彼此隔离。但虚拟机会独占操作系统,过于重度。这时候出现了容器,它是共享宿主操作系统,因此比虚拟机更轻量,更节省资源,适合大量部署。其中Docker是一种应用最广泛的容器。
存在Windows容器和Linux容器,但不存在Mac容器。不过可以在Mac上运行Linux虚拟机,再在上面跑Linux容器,这也是Docker for Mac的实现方式。由于Docker for Mac的特殊实现,我在Mac上跑Agones时还出现了一些问题,后面再细说。
Docker作为容器的一种实现,它也要遵循OCI(开放容器计划)。这样的好处是多种不同的容器实现可以自由替换,其他依赖容器的软件(如Kubernetes)不用依赖特定类型的容器。
Docker通过镜像(Image)来实现容器的快速复制和部署。镜像内部包含一份精简版操作系统、程序代码及依赖库。通过build命令从Dockerfile文件构建镜像,并发布到仓库(Registery)中,以供其他人使用。
我学习Kubernetes主要是通过官网教程和《Kubernetes修炼手册》。这本书与《深入浅出Docker》的作者是同一个人。学习过程基本上是跟着官网敲命令,然后看书理解概念和原理。
首先需要搞懂为什么要有Kubernetes。虽然Docker和容器为程序提供了轻量级的运行时环境,但是多个容器之间的协同工作、复制、版本更新,这些功能容器本身并没有提供。官方的说法是,Kubernetes是谷歌推出的集群管理和服务编排系统,用于自动部署、扩展和管理容器化程序。集群管理比较好理解,如按节点部署应用程序、扩缩容都属于集群管理的功能;服务编排则类似于把足球场上不同位置的人成一个队伍,即组合多个容器以服务为单位对外提供功能访问。
Kuberntes和Docker一样偏运维向,因此初学起来比较枯燥,都是在敲命令,不像学别的开源软件可以一上来就可以写程序。对于DevOps接触少的人可能不太好适应。因此建议先快速搭建一个可运行的Kubernetes环境(例如通过minikube),等充分上手后再通过云服务(如谷歌云、阿里云)搭建完整的环境。
其次要弄懂Kubernetes和Docker之间的关系。最开始Docker很火,而Kubernetes刚推出,因此后者为了推广将默认容器实现设置为Docker。后来Kubernetes发展壮大后,谷歌想收购Docker,但Docker不同意。Docker的算盘是自己也想进军服务编排的领域,因此推出了Docker Swarm与Kubernetes竞争(事实上落败)。而站在Kubernetes的角度,它只想要一个干净版的容器,不想要Docker后来推出的那些与自己竞争的多余功能,因此推出了独立的CRI(容器运行时接口)。Docker迫于压力,被迫将其中兼容CRI的部分功能抽离出来,成为containerd。最终两方妥协的结果是,Kubernetes默认使用的容器成为了containerd。
最后要弄懂Kubernetes中的主要概念:Node(节点)、Pod、Deployment(部署)、Service(服务)。
Node代表一个机器,可以是物理机或者虚拟机。一个Kubernetes集群中包含一个或多个主节点(control plan)和多个从节点(data plan),主节点可以做主从互备。主节点负责管理和调度整个Kubernetes集群,从节点上部署应用程序容器。一个Node上可以运行一个或多个Pod。
Pod是Kubernetes中最小的管理单元。它是一组容器的集合,内部容器之间共享内存、磁盘等资源,并对外统一提供一个IP。一个Pod中可以包含一个或多个容器,不过推荐只包含一个。Pod的YAML配置文件是开发和运维共同关注的信息源,成为实现DevOps所需的桥梁。
Deployment代表部署,它是比Pod更高级别的抽象,提供了Pod的生命周期管理、版本管理、可扩展性、滚动更新等功能。Deployment通过ReplicaSet来提供自愈和扩缩容能力。ReplicaSet还可以实现版本升级和回滚。
Service代表服务,它是一组Pod对外提供的服务,对外提供稳定的域名、IP。Pod之间通过负载均衡分担流量。通过为Pod赋予Label,可以使Service将流量导入对应Label的Pod。通过将Label设置为版本号,可以实现版本的安全升级。不过Service的负载均衡功能会显著增加游戏服务器延时,因此不适用于游戏服务器,这点会在后面单独说明。
云原生技术最开始是在互联网行业大规模使用的。国外代表是谷歌、亚马逊,国内代表是阿里。Kubernetes最早也是谷歌对内部使用的服务编排技术的总结抽象和开源。后来,随着云平台底层的越来越完善,人们发现使用云原生技术不仅能提供更好的稳定性,还能节省服务器费用,简化开发上线流程,方便运维管理,可谓好处多多。近年来,这波云原生的浪潮也席卷到了游戏行业,但游戏行业有着自己的特点,因此原产于互联网行业的云原生技术会有一些水土不服,需要做定制化的修改。笔者对于游戏行业中几种主要的应用总结如下。
这是游戏行业云原生改造中难度最大,却也是改造后收益最大的一块。难度在于:不同于互联网应用的无状态,游戏服务器通常是有状态的,而且对延迟非常敏感,不同类型的游戏服务器架构差异又很大,很难像互联网那样提出一个普适各种场景的通用解决方案。通常来说,有明确对局时间的开房间类型游戏(如王者荣耀)更适合容器化。另外,游戏中的网关及中台等无状态服务也适合容器化。这块具体还会在后面单独详细说明。
云原生数据库不同于传统数据库,它的主要特点是可以弹性扩容,在流量出现急剧提升时可以快速增加节点以应对请求压力;另外搭建和管理方便,运维成本低。它的底层是通过Kubernetes的StatefulSet(有状态副本集)和Persistent Volume(持久卷)来实现的。
知名的云原生数据库国外有Amazon Aurora,国内有阿里云的PolarDB。《原神》就使用了PolarDB来实现高性能的海量数据查询。
游戏数据的查询和分析是游戏运营过程中重要的一环。传统上游戏公司会自建数据仓库来处理,而云原生的数据仓库提供了一种更优的替代方案,可以做到更低的成本、更方便的运维和更高的实时性。
较知名的云原生数据仓库,国内有阿里云的AnalyticDB,国外有Snowflake和Redshift。
云游戏是指在服务器运行游戏客户端,将游戏画面经过视频压缩后,传输到用户的显示设备上进行游戏。这里技术关键点有三个:一是需要有强大GPU和CPU性能的服务器来运行游戏客户端,二是需要将游戏客户端容器化以方便弹性伸缩,三是需要提供性能良好的视频压缩算法和网络传输协议。所以,云游戏也非常适合通过云原生的方式来部署。
函数计算用来处理计算密集型的任务,只需要提供函数计算的代码,就可以让调度系统自动分配和调度计算任务。它是一种serverless服务,使用者无需关心服务器的具体状况。
阿里云提供了函数计算的服务,莉莉丝的《剑与远征》项目使用它来做战斗校验,比原先自己做校验服务容器化获得了更低的弹性伸缩延迟。
游戏服务器上云与传统互联网行业有着不同的特点,因此容器化改造需要一方面保留Kubernetes中适用的部分,另一方面摒弃掉不适合的部分并替换成新的实现。具体来说,要解决的重点问题如下:
具体上云的实现逻辑,行业内并没有统一的框架或规范,游戏厂商各家有各家的实现。目前较知名的框架有Agones、KruiseGame,各自的侧重点也不尽相同。我在学习过程中调研过以下几个开源项目,分别做下介绍。
这个项目在2016年推出,是一个实验性质的项目,用于验证游戏服务器在Kubernetes上的管理。这个项目的发起人Mark Mandel来自谷歌,他同时也是Agones项目的发起人。可以说,Agones就是在这个项目的基础上发展和完善的。
这个项目的目的是通过一个简单的足球对战游戏,来验证游戏服在Kubernetes上的托管。架构上分为匹配服和游戏服,弹性扩容主要是针对后者。作者为这个项目写过四篇博文,详细介绍了项目目的和设计思路。同时,作者还在GDC 2017上做过专题演讲。
流程是客户端通过匹配服查找要连接到的游戏服,然后将游戏服的ip和端口返回给客户端,客户端连接到游戏服,开始比赛。这里为了延时考虑,客户端直连游戏服,而非像传统互联网应用一样通过Service做负载均衡。实现方法是将游戏服Pod的hostNetwork设置为true,这样Pod就可以直接使用宿主的网络名称空间,外部连接没有额外延迟。
由于各个Pod共用宿主机的网络空间,那么就可能出现端口冲突。解决办法是将已使用的端口记录到Redis中,新服务器启动时先向Redis获取已使用的端口并排除掉,然后在剩余的端口范围中随机,直到获取到可用的端口。
由于游戏服是有状态的,因此不能使用无状态的Deployment来管理,而是通过Pod加Label的方式自行实现scaler。由Label来区分服务器是何种类型(匹配服还是游戏服),并将同种类型的服务器安排在同一节点上。
在扩缩容上,这个项目优先考虑节点的扩缩容而非游戏服,因为节点数量与成本直接相关。为集群设置最大节点数和最小节点数,并为每个节点设置CPU缓冲区大小,避免耗尽节点的CPU。
为了避免集群碎片化,利用节点亲和性,让游戏服务器尽量集中到已有节点中。当某个节点使用的CPU降低到一定阈值时,封锁节点,不再允许调度新的Pod到这个节点上。当这个节点上的所有游戏服都退出后,再删除这个节点。
paddle-soccer在代码中与谷歌云强绑定,所以只能在谷歌云中使用。如果想在别的云平台使用,除非自己修改代码。
Agones在2017年推出,是谷歌和育碧联合发起的游戏服务器云原生解决方案。前面说到,它的发起人和paddle-soccer是同一人,但是它在后者的基础上做了进一步的封装和抽象,并引入育碧在游戏行业开发3A的经验,使得通用性更强、更容易使用。
与paddle-soccer类似,Agones对于游戏服务器采用了直连的方式,而非通过Service做负载均衡。
对于扩缩容这块,首先需要理解Agones的工作模型。游戏服通过Fleet来管理。Fleet上维护有所有可用的游戏服,这些游戏服可能处于Allocated状态或Ready状态。Allocated状态代表已投入使用的服务器,通过匹配服务可以分配到;而Ready状态代表已准备好,但还没投入使用的服务器。扩容和缩容都是针对Fleet。扩容时向Fleet添加新的状态为Ready的服务器,缩容时仅从Fleet删除状态为Ready的服务器,这样能保护正在使用的服务器不被异常关闭。如果确想关闭状态为Allocated的服务器,可以先由业务端调用SDK将服务器状态置为Ready。
另一个关键概念是FleetAutoScaler,它用来做弹性扩缩容。可以通过FleetAutoScaler设置Fleet的最大和最小副本数,以及缓冲区大小。缓冲区也就是Ready服务器的数量。通过这些设置,Fleet的服务器数量会控制在最大和最小值之间,而且扩缩容时会保持固定的缓冲区大小。
通过Fleet和FleetAutoScaler,这个模型维护一组预热的游戏服,可随时根据需要投入使用,避免硬启动带来的需求满足延迟。
Agones还提供了一套完善的SDK供业务端使用。SDK目前支持Go、C++等多种语言,以及Ready()、ShutDown()等查询或者设置游戏服状态的API。业务端调用SDK时,会向SDK服务器发送gRpc请求,SDK服务器再将请求转发到对应游戏服。
除此之外,Agones还提供健康检查的功能。当判断游戏服处于不健康状态时,处于Fleet管理下的游戏服会被删除并重新创建。
Agones可以在各种云平台和Minikube中使用。但是在Mac系统上跑Minikube时,有可能出现从外部连不上游戏服务器的问题,这个目前还没有太好的解决办法。
OpenKruiseGame(OKG)由阿里云于2022年推出,目前还是一个比较新的项目。它旨在帮助业务开发者简化游戏云原生化的过程,并提供了热更新、原地升级和定向管理等实用功能。
OKG属于OpenKruise的子项目,前者也依赖后者来构建。OpenKruise是阿里云推出的一个Kubernetes扩展功能集,它提供了Kubernetes原生所没有的一些功能,如:通用工作负载、原地升级、可配置扩缩容等。OpenKruise目前已在互联网行业得到广泛应用,如阿里巴巴、斗鱼、Boss直聘、小红书等。
与Agones不同,OKG的目标对象不仅包括PVP游戏,也包括PVE游戏,而不像Agones主要面向PVP游戏。这使得OKG在功能设计上与Agones有所不同,除了传统的弹性扩缩容,还包括热更新及定向运维管理等功能,这些功能在PVE游戏中更为需要。
与PVP游戏相比,PVE类型的游戏特点是用户状态保存的时间更长,不像有明确对局时间的开房间游戏(PVP)那样一局只有十几分钟,理论上PVE游戏中用户只要愿意可以一直在线,这个过程中状态需要一直维持。
为此,OKG首先添加了定向运维的功能。通过设置要定向管理(Reserve)的服务器id,删除对应id的游戏服,同时在创建新服时避免该id对应的游戏服生成。在缩容时,OKG优先删除要Reserve的游戏服,然后依次按照状态、优先级和服务器id等参数确定剩余游戏服的删除顺序。通过Reserve指定下线的功能,可以处理PVE游戏经常会遇到的合服场景。
其次,OKG提供了热更新的功能。很多语言(如Go)不具备语言级别的热更新能力,但可以通过容器的方式支持。OKG的热更新功能由底层的OpenKruise提供。实现方法是在每个游戏服Pod中部署Sidecar和Main两个容器,Sidecar放游戏脚本,Main放游戏引擎和守护进程。热更新时只更新Sidercar,Main不动。这样就无需对Pod整体重建,从而做到在玩家不中断游戏的前提下修改游戏逻辑。
与Agones类似,OKG使用CRD来自定义游戏服相关的工作负载,包括两种:GameServerSet与GameServer。GameServerSet是对一组游戏服管理的抽象,类比于Agones中的Fleet,它是对OpenKruise中的Advanced StatefulSet的进一步扩展,因此也具有后者特有的原地升级(热更新)功能。GameServer是对一个游戏服管理的抽象,也就是对游戏服定向运维动作的记录,因此删除GameServer也不会触发实际游戏服的删除。
截止本文写作时止,OKG刚刚迭代到0.4版本。作为一个新兴的项目,前面还有很长的路要走。
总的来说,由于游戏类型众多,服务器架构彼此差异大,很难找到一个像互联网那样容易标准化的云原生模型和解决方案。不管是Agones、OpenKruiseGame,还是游戏项目组自己实现的框架,都只适合特定游戏类型或者解决特定问题。不过,借助云原生技术的帮助,可以在成本控制、便捷运维、快速发布方面取得优势,这些优点是传统的部署方式所不具备的。相信随着云原生技术的发展,以及游戏业务和云原生技术的深度融合,游戏服务器的云原生化必将成为未来持续的趋势。