莉莉丝游戏「剑与远征」项目组主程:探讨容器和函数计算的“弹性”

作者:小莉慢品 2021-10-08

最近这些年,以容器和函数计算为代表的云原生技术,正用一种全新的方式改变着我们构建和部署应用。AFK(剑与远征)在上线之前也完成了战斗校验场景容器化的落地,容器为我们带了简易运维,弹性伸缩等好处。但在上线后,我们逐步发现容器在弹性伸缩时延上面的不足,转而将目光聚焦到了另外一种云原生技术:函数计算。本文结合了AFK的实践,来分享一下两种云原生技术在弹性计算的使用、原理和思考。

——拉德,AFK(剑与远征)主程

一、相关概念

什么是弹性伸缩

系统能够随请求的增减,实现系统扩容,实现高可用,在业务下降的时候,能实现系统缩容,减少成本。简而言之,是一种低成本的可扩展计算。


什么是冷启动

冷启动是指一个应用从无到可以对外服务的过程。在游戏交互领域,一般5秒以上的等待时间,玩家是无法接受的。

什么是容器技术

Docker几乎是容器的代名词,Docker提供了将应用程序的代码、运行时、系统工具、系统库和配置打包到一个实例中的标准方法。Kubernetes (以下简称K8S)是容器集群的管理系统,可以实现容器集群的自动化部署、自动修复,自动扩缩容、无缝应用升级等功能。


什么是函数计算

函数计算又称Faas,全称Function as a service,是 Serverless(云计算的方向之一) 的子集,也是整个 Serverless 的核心。Serverless ,又称 "无服务器",是开发者将服务器逻辑运行在无状态的计算容器中,完全由第三方管理的。Faas具备细粒度调用,实时伸缩,无需关心底层基础设施等特性。

什么是战斗校验

战斗校验是在服务器跑的一段战斗逻辑代码,用来验证玩家客户端上传的战斗是否有作弊的情况。

战斗校验的弹性需求

战斗校验一般需要逐帧计算,CPU消耗会非常高,通常1队v1队的战斗需要n毫秒,而5队v5队的战斗则需要相应5n毫秒。以AFK国服为例,每天跨天重置任务,同时也是整个战斗校验集群的业务高峰,而每天夜里,随着在线人数的减少,整个战斗校验集群进入业务低谷期。两者CPU消耗差异有10倍之多。

二、容器的弹性伸缩

容器是通过轮训监控的指标数据,根据算法执行调度实现自动伸缩,主要由应用层维度(Pod)和资源层维度(Node)两部分配合调度完成。


应用层维度

应用层维度从应用场景来看,分为水平扩容和垂直扩容,篇幅有限,本篇介绍主流的水平扩容:

1. HPA

Horizontal Pod Autoscaling,是Kubernetes中实现POD水平自动伸缩的功能。HPA组件会每隔30s从Metrics Server等监控组件获取CPU等监控指标,根据算法计算出期望副本数,通知Depolyment等组件执行扩缩容。

扩缩容算法:Desired Pods = ceil(sum(MetricValue) / Target)

举个例子:当前HPA设定CPU阀值:70%。当前有3个Pod,CPU都是90%。那期望的Pod数量=ceil(90%*3/70%) = 5,此时HPA会进行新建2个Pod,进行水平扩容。

HPA CPU

按CPU伸缩配置比较容易,但是有个缺陷,如果毛刺带来的负载大于100%,HPA的认知就会被限制,无法通过一次计算就得出还需要多少个POD,就需要多次调整。每次调整中间还需要间隔一个扩张冷却周期,默认3分钟。如下图,Pod想从1个变成8个,需要经过3次计算周期。


HPA QPS

所以HPA就有了Custom metrics扩展,常用的是QPS。通过配置单个Pod的QPS>10,当流量操作QPS阀值时候,集群就会自动扩容。QPS是一个潜在的约定,就是集群的每秒的请求消耗CPU比较平均,当特定时间点某种CPU消耗较多的请求大幅增加时候,QPS无法正确衡量。导致集群扩容的数量不能满足业务需求。

2. CronHPA

结合上文,我们会发现HPA的扩容调度有一定时延,业务遇到毛刺时候,HPA无法及时调整Pod到业务期望数量,造成部分应用不可用。这种业务抖动会给玩家带来非常差的体验。为了解决这个问题,我们引入了CronHPA,有点像Linux的Crontab,根据业务之前周期性的规律,在业务高峰前10分钟提前准备好资源,满足高负载的需求。CronHPA跟HPA不同,HPA是官方版本提供了实现方式,CronHPA基本要依赖于开源社区或者云厂商的实现方式。AFK所用的是阿里云的CronHPA,这里讲一下CronHPA的演进:

现在大部分厂商的CronHPA

目前大部分云厂商还是这种实现方式,包括之前的阿里云,CronHPA和HPA都是直接控制Deployment,来调度应用扩缩容的,因为二者无法彼此感知,所以CronHPA扩张上来的Pod很快会被HPA回收,两者并不兼容。如下图所示:


当时为了同时获得HPA和CronHPA的特性,AFK一直保持着双SVC的配置,分别支持HPA和CronHPA。每次和阿里云容器同学交流,我们总是敦促对方改进。

阿里云的新版CronHPA

终于阿里云的CronHPA终于在2020年3月开始兼容HPA了,算法也比较有意思,这里列一下实现方式:

① CronHPA控制的TargetRef从Deployment变成了HPA

② 扩容时候调整HPA的Min值= max(CronHPA Scale Up Replicas, Current replicas)

③ 缩容时候调整HPA的Min值= min(CronHPA Scale Down Replicas, Current Replicas)

我们可以看到,这个实现方式一直在调整的是HPA的Min值,而非当前的Replicas数量。这样一个设计有个很巧妙的地方,比如我们的HPA是min/max:10/99999,n点-n+4点(n点为最高峰)是业务高峰。我们可以配置成n-1点50分扩容到100, n点10分(不需要等到12点)缩容到10。我们发现尽管n点10分 HPA的Min已经调整成10了,但是当前的Pod数量还是按CPU计算的,并没有直接减少,而是随着业务的减少慢慢向HPA的Min靠拢。这样不需要保持整个n点-n+4点都维持在100的规模,极大减少了资源浪费。


在实际使用的时候,CronHPA非常适合有周期规律的应用场景,但是为了应对预估不足的流量,或者突发的流量,还是需要配置HPA作为最后的保障。

资源层维度

在资源层维度,目前主流方案是通过Cluster Autoscaler来进行节点的水平伸缩。当Pod不足时,Cluster Autoscaler安装集群配置的扩容实例规格,到云厂商的公共资源池去购买实例,初始化之后,注册到集群中,Kube Scheduler会调度新的Pod到这个节点上。链路越长,越容易出错,结果往往就是集群弹不起来.

这里列出一些限制:

  • 动态购买ECS:涉及的限制有库存、按量Ecs Quota、批量创建机器云盘限流等
  • 机器初始,安装K8S:涉及的限制有Yum源、Metadata Server、Ram Role、内网Open Api等
  • 选择terway网络(一种和vpc打通的网络方式):涉及的限制有Eni数量Quota、Eni并发创建限流、Ecs规格所支持的Eni网卡数、Ram Role等。

AFK被坑过两次:

  • 一次我们所需的扩容的六代机型,严重低于阿里云公共资源池的水位线,导致Cluster Autoscaler无法购买到机器,集群扩容失败。因为集群扩容是按配置的机型顺序来扩容的,建议配置一些旧机型,保证水位线的充足。
  • CentOS社区Yum源权限变动,流入到下游的阿里云,导致弹性购买的机器无法初始化(403错误),也就无法加入到K8S集群,集群扩容失败。阿里就此发了复盘报告,从阿里侧杜绝这个问题。

容器的冷启动链路

除了链路太长给弹性带来不稳定性外,整个资源层扩容时间,也是在2-3分钟左右。为了解决伸缩时延过长的问题,社区发布了一个新的组件,Virtual Kubelet,通过它,K8S的节点可以用其他服务来伪装。比如阿里云的ECI,底层使用基于Kata的安全沙箱容器,对容器运行环境进行深度优化,提供比虚拟机更快的启动速度,整个资源层扩容时间可以缩减到30秒以内。


三、函数计算的弹性

上文提到Faas是serverless的核心,我们首先纠正两个经常误解的名词:

  • Serverless并不是说不需要服务器了,可能叫server-free相对好理解一点,只是说程序员可以不用关心server,关注业务实现就可以了。就好比用Erlang的同学,可以不用像C++那样手工分配和释放内存了,而是交给了GC,但是内存还是在那。
  • 函数计算实际上跑的是个代码组合单元,而不是代码里面的单个函数。这里的function可以理解为功能,或者main这样的功能函数入口可能更为妥当。


函数计算的调度

相较于K8S复杂的逻辑,函数计算对开发者来说是非常友好的,整个调用流程如下:


如图所示,调度系统在感知有请求过来之后:

  • 如果有闲置的实例,则直接返回空闲实例,交给API Server处理请求。路径:1->2->3->4.a->5->6->7->8->9
  • 如果没有闲置实例,调度系统就会到FAAS维护的ECS池中去取ECS,这个时间很短,可以忽略。经过实例初始化之后,交给API Server处理请求。路径:1->2->3→4.b1->4.b2->5->6->7->8->9。同时FAAS的ECS池会异步地去云厂商的公共资源池补货。

函数计算将复杂性下沉到了基础设施,通过监控更加细粒度的Function执行情况。客户端请求服务,如果有实例空闲,就直接返回实例,用来计算。如果所有实例忙碌,就会非常快速的创建新的实例来承压(没有算法,就是这么暴力)。

函数计算的冷启动链路

函数计算的实例的冷启动链路也非常简单,如下:


链路介绍:

  • 获取ECS:函数计算自己维护了一个ECS池,所以获取ECS时间,基本可以忽略。同时ECS池会到公共资源池异步补货,以此保证水位线。
  • 下载代码:从OSS下载用户代码并解压,一般代码包相对镜像小太多,时间在毫秒级别。
  • 启动容器:Docker的容器镜像已经打到ECS的镜像里面了,所以这部分只需要将用户代码放到容器固定目录,创建和开始容器, 在百毫秒级别。
  • 启动服务:运行环境的初始化,比如nodejsserver的启动,大概百毫秒级别。
  • 服务加载依赖:主要是一些脚本语言把库载入内存的实际,如果库比较小,这个时间也在毫秒级别。
  • 执行处理逻辑:这个就是运行时间,时间按照处理逻辑来,AFK在百毫秒到秒级别。

忽略业务逻辑执行逻辑的情况下,FAAS从调度,到获取ECS,到服务启动,基本在1秒+左右。

四、实际业务使用

谈到弹性,我们离不开负载均衡,服务负载不均的时候,则会导致业务出现部分不可用。接下来我们以AFK为例,来看下容器和函数计算在业务使用上的差异:

负载配置

容器的SVC一般需要挂载SLB对外提供服务,负载均衡依赖于SLB的轮询等机制。轮询机制无法感知Pod的实际负载,可能会引起负载不均。下面列举一下AFK在实际使用中,遇到过一些负载不均的情况:

问题:一段战斗校验代码,在极小概率下,算出了非常大的坐标,引起了逻辑死循环,导致卡死的进程无法正常响应SLB过来的请求,SLB无法感知Pod,依然会持续发送新的请求,新的请求也会持续失败。

解决办法:引入了一个超时报警机制,在业务层做监控,记录超时的数据,本地复现修复。

问题:之前AFK是一个Node上起的多个Pod,当时用的是Node作为SLB的服务器单元,因为每个Node上起的Pod数量不一致,会导致每个Pod处理的请求数量不一致,负载不均。

解决办法:将Pod直接作为SLB的服务器单元,挂载到SLB后面。

问题:也是一个Node起多个Pod引起的问题,有些Node上起了2个Pod,导致Node的CPU超过50%,Linux的系统调度会增加额外开销,导致Pod性能下降,处理相同请求的负载变高。

解决办法:尽量让Pod均匀分布在Node上面,已保证Node的CPU基本一致。或者一个Node上部署一个Pod。

问题:机型不一致,导致的负载不均。五代机和六代机混用,六代机的性能比五代机提升15%左右,混用会导致分配至五代机上的Pod负载偏高。

解决办法:在集群扩容中,将六代机型放在首位,尽量保证机型不混用。

函数计算的话,开发人员就无需关注负载,调度系统会合理安排每个请求,保证及时有实例来处理请求。对于上面提到的死循环问题, 函数计算也贴心的提供了超时杀进程机制,避免死循环引起的费用问题。

伸缩配置

容器需要配置HPA和CronHPA,如下:

  • HPA配置 :CPU>70%时候,进行扩容
  • CronHPA :  n-1点50分 从10扩容到100,n点10分 从100缩容到10,满足每天n点的业务高峰(结合上文,此处调整的是HPA的Min值,而非当前实际Pod数量)

函数计算不需要进行伸缩相关配置,依然是由调度系统根据请求,实时取出空闲实例或者创建新的实例,来处理请求。

对比可以发现,函数计算无需关心负载和伸缩相关配置,极大的减轻了开发人员的负担,使得开发人员更加关注业务本身。

五、容器和函数计算,该怎么选择?

可以互相弥补

函数计算因为其极致弹性,往往可以很好的契合我们的弹性业务需求。但是,函数计算目前不是一款社区产品,本身由云厂商实现,是个黑盒,我们也不能登录到实例上去看现场,要严重依赖打点和tracing来定位问题。容器呢,尽管弹性不如函数计算,但是对于我们来说却比较自由。二者在使用上,都无需对代码做修改,所以AFK最终的方案是容器为主,函数计算的结合方式,在容器请求异常之后,到函数计算这边重试(当然也建议部分请求互为主备,主动切一些流量到函数计算,只有到达一定量级,云厂商才会有热情的support)。这样保证大部分请求都落在容器上,方便我们定位问题。在一些突发流量或者流量估计不足的情况下,系统又能急速扩张,满足业务需求。两种云产品相结合,也同时增加了容错性,毕竟这两款产品的弹性都依赖一定复杂的链路。

甚至互相结合

目前一款叫Knative的社区产品正在公测,Knative的定位为基于 K8S的函数计算解决方案,结合了容器和函数计算。通过引入Queue-Proxy的模式已经实现了缩容到0的需求,引入KPA算法,可以把应用层扩容带入到了10秒以内。Knative团队一直在努力,在解决探活,臃肿等问题后,应用层的扩容有望达到秒级,达到用户无感。


原文:https://mp.weixin.qq.com/s/Yb3PJABUgZFQWkquYT5CBQ

最新评论
暂无评论
参与评论

商务合作 查看更多

编辑推荐 查看更多