原创 杜俊超 网易游戏运维平台 2019-07-19
杜俊超
13 年加入网易,担任游戏运维工程师,负责梦幻西游的运维工作至今,期间负责过《终结者 2:审判日》、《决战!平安京》等游戏海外版的运维工作,期间也有负责几款出海游戏的运维工作,目前在负责组内运维规范化、自动化建设,以及游戏公有云架构设计的工作。
7 月 13 日,网易游戏学院受邀 AWS,作为主题演讲分享的重磅嘉宾,参与了中国区 AWS Game Tech Day- AWS 游戏开发者大会广州站分享,网易游戏资深运维工程师杜俊超代表网易团队带来了《网易游戏海外 AWS 动态伸缩实践》分享,技术内容丰富,引起现场热烈讨论,与会人员收获满载。
AWS 作为网易游戏海外上云的方案之一,为网易游戏在全球运营过程中提供极大便利,网易游戏学院也积极与行业同行,与全球游戏热爱者自由交流,与广大游戏热爱者共同分享实践经验。
会上,杜俊超分享了网易游戏在游戏出海运营过程中积累的服务器动态伸缩实践经验,这次分享以全球化的开放视野,与广大游戏热爱者共探云技术的实践运用。
以下为分享实录:
大家好,很荣幸受到 AWS 的邀请来到这里跟大家交流,今天想跟大家分享的是最近一年来我们的游戏在海外 AWS 上进行动态伸缩的一些实践。
出海佳绩
我们看到,几款目前比较火的游戏,海外市场都是其重要的收入来源。尤其是荒野,可以说是重度依赖海外市场,尤其是日本。
这么大的海外体量下, 支撑它的除了庞大的用户玩家群体,还有一系列的海外 AWS 的基础设施。
与国内发行相比,游戏在海外发行上,从运维的角度来看,具有很多根本性的差异。
在国内,我们使用的是自建的机房,部署的是传统的物理机,但是游戏出海,作为发行商,我们不可能在每个发行地都自建机房和基础设施,这样太花时间和成本了,所以才会考虑引入公有云,使用公有云上虚拟化的资源。
我们都知道,与传统物理机相比,云上的虚拟机是可以快速的拉起和销毁的,正是基于这个特性,我们才可以实现服务器弹性的伸缩,来节约成本。
我们之前已经有同事在前面几次开发者大会上,给大家分享过,我们的游戏在海外发行时遇到的性能、安全性、和全球通服的挑战, 今天我想跟大家分享的是动态伸缩方面的内容。
适合动态伸缩的游戏类型
从上面列举的网易出海成功的游戏例子来看,他们都有几个共同的特点:
第一:品质够好:)(打了个广告)
第二:存在人数峰谷周期:
这些游戏都是每天都有 1-2 个在线人数高峰,我们的目标是在人数增长的时候自动扩容,在人数逐渐下降的时候自动缩容。
第三:竞技类大世界服
这点很重要,并不是说所有游戏都适合动态伸缩,像 MMORPG,它的游戏架构通常是玩家登录页面看到的一个逻辑服,背后就是一个物理服,这个逻辑服的所有玩家都在同一个物理服上面,哪怕人数很少,也很难在线进行缩容,往往这台服挂了,上面全部玩家都会掉线。
而竞技类大世界服的架构是一个大区背后有很多物理服,通过开房间、开战斗局的方式把一小撮玩家分配到一个服上,缩容的时候只要把空闲的服务器屏蔽,不要让它分配到新的战斗,然后等上面的玩家都结束战斗之后,就可以下线了,而扩容则把服务器加入可分配队列里就可以。
比如这里截取了一个游戏的美服, 在某个时刻的游戏战斗服的负载分布情况。可以看到,这些战斗服的 cpu 负载大致是平均的,但是前面有几台会稍微繁忙些,后面有几台十分空闲的,这是因为游戏代码的调度策略是在负载总体平均的基础上,尽量让玩家落到集中几台,然后让另外几台空闲出来,有点像铜壶滴漏的样子。这样就可以屏蔽空闲的机器,等玩家战斗完,关机下线掉。
方案调研:AutoScaling
分析完适合做动态伸缩的游戏类型后。接下来,我们就开始做方案选型。我们了解到, AWS 在 EC2 的服务下面,就有一个 AutoScaling 的功能提供。
AutoScaling:常规玩法
它的使用也非常简单,一开始我需要创建一份启动配置,这个配置实际上是一个 EC2 的实例模板,上面定义好我扩容的时候要创建的实例的机型,使用的操作系统镜像,和落到哪个 VPC 上,等等这些信息。
接下来我创建一个 AutoScaling Group,绑上刚刚建立的启动模板,并且定义伸缩的时机,比方说我可以用实例的 cpu 使用率作为 metric,给这个 AutoScaling Group 设置伸缩的阈值。还可以在这个 AutoScaling Group 之前,挂一个 ELB,让整个 AutoScaling Group 作为一个整体对外服务。
最后,有了前面两个东西,整个 AutoScaling 就可以跑起来了。
AutoScaling:常规玩法问题
可以说,这套玩法,适用于很多无状态的 web 业务。但是对于游戏来说还不行,对游戏而言,上面这套玩法至少有这么些问题。
第一,它的 Scaling 阈值是按聚合平均值计算,比如我选择的是 cpu 这个指标,它计算的是整个 AutoScaling Group 里所有实例的 cpu 负载平均值。刚刚前面也介绍了,我们的游戏战斗服的调度策略不是绝对平均的,导致服务器的负载也不是完全平均的,如果阈值采用平均值计算的话会有偏差。
第二,上面那套常规的配置,无法按业务指标设定策略,默认只提供 cpu、网络流量这些指标。
第三,默认情况下,缩容时会立刻把实例终止掉,对于服务器上还有玩家的话, 这种情况是不可接受的。
AutoScaling:进阶玩法
针对上面的问题, AutoScaling Group 提供了一些进阶的玩法来解决。
除了默认的指标按聚合平均值计算外,它还允许用户自己设立 CloudWatch Alarm,然后这个 alarm 选取的指标,可以自定义聚合方式。
比如我这里的聚合方式设为取最小值,然后最小值低于某个阈值的时候触发缩容。
另外,这个 CloudWatch Alarm 除了基于数值指标以外,还可以基于日志来建立。它的过程是首先在服务器上安装一个 CloudWatch agent,收集业务日志,发送到 CloudWatch, 然后在 CloudWatch 配置日志 alarm,比如当日志里出现某个关键字就触发一个 alarm,最后以这个 alarm 来触发 autoscale 事件。
针对缩容默认就立刻终止实例的问题,AWS 提供了生命周期钩子的解决方案。钩子可以设在启动或者终止实例的时候,通过钩子, 可以在终止实例前,调用 lambda 或者发一个 SNS 消息, 甚至调用 Step Funtions 完成一系列的任务,比如轮询游戏服的状态,看还有没有玩家在战斗,最后通过改变实例的状态,让 AutoScaling 继续终止实例。
AutoScaling:进阶玩法问题
有了上面的进阶玩法之后,似乎可以满足游戏的伸缩需求,但是我们在深入调研、试用的过程中,还是发现了一些问题:
第一,当我想聚合多台机器指标的时候会失败,CloudWatch 限制了聚合不能超过 10 个机器。
另外,alarm 建立的时候,实例的范围就已经确定, 后续对这个 alarm 增减机器,需要用 api 显式的修改 alarm 的 metric 范围,没有一种动态的类似打标签的形式来标注我的机器应该纳入到这个 alarm,十分不便。
第二,如果是提交自定义业务指标,或者提交日志的话,我需要在机器上安装一个 agent,需要确保这个 agent 在正常工作,这是一个比较麻烦的事情。
第三,配合钩子使用,整套流程下来,牵涉到的 AWS 服务过多,遇到问题的话排查困难
第四,我们使用的云服务器不止 AWS,我需要一套统一的工具链实现我们的伸缩需求
基于上述原因,我们最后没有采用 AutoScaling Group 的方案来实现我们的伸缩需求。
方案调研:Gamelift
然后,我们还了解到 AWS 的另一个服务:Gamelift
它与前面介绍的 AutoScaling Group 不同,AutoScaling Group 是一个 EC2 下的附加功能,而 Gamelift ,我们从名字来看就知道,它是针对游戏设计的,是为游戏量身定做的一套托管、部署的解决方案。
从官网的宣传来看,Gamelift 具有众多的特性,而其中最吸引我们的是它在游戏服务器伸缩功能上的实现。
Gamelift:工作原理
我们来看看它的工作原理:
首先我们需要把服务器代码打包,上传并托管到 Gamelift,
然后声明我们要什么样的机器实例和操作系统来运行我们的代码,这些机器实例会被放到一个叫 Fleet(队组)的抽象资源里管理起来。
当队组里的服务器启动的时候, 需要通过 Gamelift 的 Server SDK 注册自己,提交自己的 ip、port,可用会话数等信息;
客户端启动的时候,先连接我们集中或就近部署的 Game Service,这个节点通常是一个登录服,登录服通过 Gamelift 的 SDK,提交玩家的信息,比如玩家等级,玩家的延迟等等;
然后由 GameLift 的匹配服务,根据这些信息选出最合适的可用的游戏会话给客户端;
最后,客户端根据返回的游戏会话,直连这个游戏会话所代表的游戏服,完成整个游戏进入过程。
也就是说,GameLift 服务掌握了整个游戏的游戏会话连接数信息,有了这些连接数信息, 它就可以设定什么时候需要伸缩。
GameLift:自动扩展
具体来说,Gamelift 支持两种伸缩的设定方式,一个是基于目标跟踪,另一个是基于规则的策略
GameLift:自动扩展-基于目标跟踪
基于目标跟踪的意思是,把可用游戏会话百分比作为决定是否伸缩的唯一指标。
通过设定这个百分比,可以确保一直有一定数量的服务器资源来应对新增的玩家请求。
如果这个百分比设得较大,那么在人数波谷的时间段,就会预留过多的备用资源;设得较小又可能会在人数快速上升的时间段因为服务器来不及启动而导致玩家排队。
GameLift:自动扩展-基于规则
而基于规则的自动扩展则对扩展操作提供了更多的精细控制。
它的每个规则是这样子的:如果某个指标保持为大于或小于一个阈值持续一段时间,那么就调整 Fleet 的容量,比如加 10,或者加 10%,又或者设定容量绝对值到 50 这样。
与基于目标跟踪相比,基于规则的好处在于考虑了当前状态的持续时间,不会因为突发的毛刺点触发伸缩,并且可以为一个队组配置多个规则,来应对各种情况。
GameLift:伸缩的其他功能
GameLift 的伸缩还有其他的功能:
冷却时间:在更改了容量以响应自动扩展策略之后, GameLift 将等待十分钟,然后再响应该相同策略的触发器。
游戏会话保护:如果实例的任何服务器进程正在托管受保护的游戏会话,则无法终止此实例。比如你在机器上面 debug 的时候,不希望服务器被突然关掉。
扩展限制:可以设置队组的最小和最大限制,控制总体实例使用情况,防止你的策略设置得不恰当, 会一直无限的扩容造成浪费。
启用/禁用自动扩展:在队组级别手动暂时启用或禁用自动扩展。
GameLift:调研结论
经过上面的调研,我们知道 GameLIft 的设计确实更贴合游戏,因为它把玩家请求的分配都接管过去了。
但正如介绍 AutoScaling Group 时所说的,安装和维护一个 CloudWatch agent 对我们来说都觉得比较麻烦,那像 GameLift 这种,需要在服务端代码植入 SDK 的服务,产品就更难接受了,而且这样会导致国内与海外版本的代码差异很大,所以我们最终也没有考虑 GameLift 的伸缩方案。
我们的方案
那我们最后到底是怎样实现游戏服的动态伸缩的呢?我们最终,借鉴了 AutoScaling 与 GameLift 的设计思想,自建了一个伸缩策略调度平台,在需要伸缩机器的时候,利用 EC2 Api 来开关服
伸缩策略调度平台:架构
来看看我们的架构:
首先,我们的游戏服上会有一个自研的类似 CloudWatch agent 的通用 agent 来上报负载数据到监控系统;另外一些游戏业务的指标,它可能没有上报到监控系统,但是可以作为伸缩依据的,那么它就会以接口的形式开放给我们的平台去采集。
橙色部分就是我们的平台,里面会有一个策略模块,定期地从监控系统或者游戏项目提供的指标接口采集机器的负载数据;然后会有一个记录游戏服开关机状态的 consul 集群。
策略模块结合负载数据和定义好的策略,就能在适当的时机触发伸缩动作,并且从 consul 集群里找到具体要操作的机器,然后让执行模块去启动实际的伸缩流程。
用 EC2 api 去开机和关机就是执行模块发起的,不过它不是直接调用 ec2 的 api,而是通过调用我们的混合云管理系统封装好的公有云 api 来实现。
伸缩策略调度平台:指标采集
指标采集方面,像 cpu/ 内存这种基础的主机指标会由 agent 上报并按 CMDB 的群组结构进行汇聚:
另外一类的业务指标,需要游戏项目提供采集接口,比如有些项目会采集空闲的进程数和繁忙的进程数这些指标:
伸缩策略调度平台:策略配置
策略配置方面,参考了一些 Cloudwatch 与 Gamelift 的设计思路:
比方说我们也会设置冷却时间,然后我们也支持多组匹配条件,并且每一组匹配条件里,还可以设多个命中规则,比 Gamelift 更加复杂。
我们每次伸缩的量,也设计得更加灵活,除了可以设置固定的值以外,还能通过指标的运算得到;
我们还支持使用游戏群组的负载聚合值来判断,然后还能使用多个指标的运算值去比对阈值,而不仅仅是单个指标的,这点跟 Cloudwatch alarm 是一致的。
伸缩策略调度平台:伸缩效果
下图是我们一个游戏的在线人数与机器数的曲线图。蓝色这条线是在线人数,绿色柱状条是战斗服的数量。
伸缩之前,战斗服的数量是一直不变的,动态伸缩之后,可以看到战斗服的数量会随着人数的变化而变化:
我们还给每个服粗略统计了下与动态伸缩之前相比所节省的费用,不同地区的服务器会有不同程度的节省幅度,一般来说,人数波峰与波谷差值越大的,节省越明显:
伸缩策略调度平台:优化方向
下面是我们平台的一些优化方向:
我们了解到去年年底 EC2 AutoScaling 新增了具备机器学习的预测性扩展。可以通过设定扩展计划,在每日和每周的负载高峰时间来临前,主动地扩展。这点我们也希望可以做到我们的平台上。
第二是扩展计划。这个跟第一点不太一样,第一点是通过机器学习来预测,而这个是指人为地制定一些扩展的计划。比如策划要在周末放一个内容,或者有一些导量计划马上要上了,这个时候就可以提前设定一个扩容的计划。
第三,是基于请求变化速率来决策。跟 GameLift 的规则设置一样 , 目前我们的指标规则是采用当前数值的,但其实这个值的提示意义不是特别大,比如当前集群负载达到了 90%,达到了我们设定的扩容阈值,但有可能这个负载会一直保持,甚至下滑,这个时候其实没有必要扩容,所以真正有意义的是趋势指标和速率指标。
伸缩策略调度平台:遇到的问题
上面看到的伸缩效果,节省的费用,都是很美好的东西。接下来我想分享一下我们在做这个 AWS 的动态伸缩中遇到的问题。
问题1:容量
我们游戏服用的是高配的核数比较多的机器,然后在扩容开机的过程中,偶尔会遇到因为可用区资源不足而开不起来的情况。
因为我们的服务器在缩容的时候只是关机而已,而开机是不支持从另一个可用区开起来的,所以我们只能做了个容错,在这种情况下,我们换一个配置接近的实例类型来开, 比如原机器是 m4.10xlarge 的话, 如果因为容量不足开不起来,平台会换成 m5.12xlarge 来开机这样。
其实更好的做法是缩容的时候直接 terminate 实例,然后扩容的时候随机从一个可用区创建实例,但是这样会牵涉很多机器初始化,CMDB 变更的问题,所以当时我们没有往这个方向去迭代。
问题2:乒乓效应
采用高配实例类型的机器还有另一个问题。因为机器的配置比较高,在一些人数不多,使用机器不是很多的地区,单台机器的伸缩,都很容易引起总体负载的波动。可能刚扩容完一台机器,集群整体负载就下降到缩容阈值以下了,或者刚缩容了一台机器,集群整体负载很快又上升到扩容阈值以上。
反映到图上来看, 就是在人数没有很大波动的情况下,服务器时不时扩一下缩一下,就像乒乓球一样弹来弹去。
要解决这个问题, 我们很自然地想到了要把伸缩的粒度切细,把原来承载很多进程的高配多核服务器切成多个少核的低配服务器,但是在实践的过程中我们又遇到了另一个问题。
问题3:cpu指令集
有项目在把战斗服切成低配实例之后,开服后在加入集群的过程中一直等待超时。我们运维跟服务端程序一起排查了很久之后, 才发现,原来是游戏引擎会利用 cpu 的相关指令集来获取时间戳,而刚好这个低配的实例的 cpu, 不支持那个指令集。
当时我们用到的是 m4 的机器,从官网来看,这个系列的机型有两种 cpu。然后我们发现,我们原来使用的高配的 m410xlarge 机器用的是版本旧一点的 cpu 型号,低配的 2x 和 4x 机器用的是版本新一点的 cpu 型号,而这个型号不支持引擎用的那种指令集,但是到了 m5 的 cpu, 又支持了。
后来,程序就改了代码,用另外一种方式来获取时间戳,解决了这个问题。
问题4:实例故障问题
当把高配实例切成多个低配实例之后,因为服务器总数多了,我们渐渐感觉,故障的次数也变多,甚至有一次,一个项目在一个地区同时挂了 4 台机器。我们就这个 case 提了 support,support 查证后告诉我们,这 4 台实例恰好都落到同一个有硬件问题的宿主了。
那么想尽量降低这个集中死机的风险的话,可以把实例尽量均匀的分摊到各个可用区,让多个实例落到同一个宿主的几率降低。
问题5:实例漂移问题
关于实例死机的问题,还想多说一点。一般我们遇到这些底层硬件问题而实例被重启的情况,往往是认为实例起来后漂移了宿主,就完事了。
但有一次我们发现实例被重启后, 接下来才收到一个关于底层硬件损坏的预警。这时候我们就疑惑:到底这个硬件降级的预警是不是发送延迟了,还是我的实例原地重启了之后才检查到硬件降级了才发出的。
于是我们提了 case 去问 support,support 回复我们说,宿主因为故障重启了,接下来才被降级,然后发出了 retirement 的告警邮件,这时候我们的实例仍然处在原来宿主之上,需要我们再手动的 stop 和 start 一次它才会漂移:
这个例子提醒我们以后遇到死机重启的情况,一定要注意实例到底漂移了宿主没有。
实践心得
总结一下我们的实践心得:
第一,尽量做到随机可用区创建实例。既可以降低实例集中在一台宿主的风险,也可以提高实例开机的成功率。
第二,机型变更留意 cpu 指令集,还有时钟源、网卡队列等差异
第三,实例 failover 要确认是否已经漂移了宿主,防止后面又被重启
最后,既然实例不可避免地会死机重启,我们应当做好故障自愈
故障自愈
我们实现故障自愈流程,主要是依赖了 CloudWatch 的报警,和我们的运维系统打通。
首先我们会为每个实例添加一个 system 层面的状态检查失败的 CloudWatch 报警,这个报警会通过 SNS topic 发送到我们的报警系统,报警系统再去调用作业平台的 api,运行项目自定义的自愈流程:
同时,我们还会在 CloudWatch 的 Event Rules 里添加 AutoRecovery 的结果报警,报警同样会通过 SNS topic 发送到我们的报警系统:
展望未来
最后想提出我们对于未来的展望:
第一,提供预测与更多的判断策略,目标是让判断更加精准;
第二,希望机器的创建与下线,减少与 CMDB 的关联,减少初始化的步骤,目标是让实例可以在随机的可用区创建;
第三,探索在容器上的应用,利用容器的调度功能来实现伸缩。
以上是我所有的分享,希望能对大家有帮助,感谢大家!
往期精彩
﹀
﹀
﹀
深入理解实时计算中的 Watermark
IPv6 支持度报告和 IPv6 环境下 DNS 相关测试
MongoDB 4.0 事务实现快速上手
(三)深入 Openflowplugin 源码集群模式的 Master 选举
(二)深入 Openflowplugin 源码 Switch 生命周期对象
(一)深入 Openflowplugin 源码分析 Openflow 握手过程