Serverless – 扩缩容:如何应对流量的波峰波谷?
今天我要和你分享的主题是 Serverless 函数计算的核心特性:动态扩缩容。
在介绍扩缩容的实现原理之前,我们首先来看这样 2 个真实的场景。
场景 1:汽车日志的准实时流推荐服务,白天车流量比较大的时候,访问量较大,夜晚随着车流量的降低,需要处理的量就比较小了。
场景 2:针对线上服务的监控程序,以 10 秒请求一次重要接口的方式进行轮训服务,确保服务的可用性。
这两个场景的区别,就在于量级是否平稳。试想一下,如果用 PaaS、K8s 服务的思路实现场景 1 的功能,就需要按白天最大请求量的规格来申请资源,确保服务能实时处理完。那么,夜间大部分的资源就是空闲的,利用率很低。对于场景 2 来说,访问量比较平稳,PaaS 服务的确是比较合理的应用,资源的使用也相对合理。
Serverless 由于具备弹性扩缩容的能力,可以完美地解决场景 1 的问题,而针对这种比较轻量的场景,函数计算就是不二的选择了。因为它可以将实例缩容为 0,并根据请求量级自动扩缩容,从而有效提升资源的利用率。
可以说,极致的动态扩缩容机制是 Serverless 函数计算的那顶皇冠,戴上它,才能彰显和 PaaS 平台的不同之处。
那么,这个“不同之处”背后的实现机制是什么样的呢?
今天,我会带你深入了解不同形态下的自动扩缩容架构原理,从 K8s 的 HPA 开始分析,再去聊聊目前开源 Serverless 明星框架 Knative 的 KPA,看看它的实现和 HPA 有什么不同。最后,我们再提取出自动扩缩容的核心思想,让你能够理解自己要设计一个系统的时候该怎么做。
基本框架
开源的 Serverless 函数计算引擎核心,基本上是通过 K8s 的 HPA 来做,而云厂商一般有封装好的各种底座服务,通常可以基于底座的产品来封装,例如容器服务、容器镜像服务、云服务器等。但我们也不排除云厂商基于开源框架做二次开发的可能性。
云厂商容器调度服务的发展历史中,通常有两种调度形态,一种是基于节点 Node 调度的方式,一种是基于容器实例的方式(我们通常把这种称为 Serverless 的,即不感知 Node 维护的方式),你可以简单地认为,前者是拿了一整套房子过来,自己维护整套房子和隔间的运行,而后者只关心隔间的运行情况。
由于云厂商的函数计算通常是基于容器服务的底座来进行的,接下来,我就从 Node 和 Pod 两个形态,分别来跟你讲一讲它们的调度方式。
Node 维度
首先,我们来看 Node 调度形态下,函数计算框架是如何进行动态扩缩容的。
如上图所示,这是一张基于容器 PaaS 服务和云服务 Node 的扩缩容调度示意图,我来解释一下它的各个部分都有着什么样的作用。
Shceduler:调度模块负责将请求打到指定的函数实例上(Pod),同时负责为集群中的 Node 标记状态,记录在 etcd 中;
Local-controller:Node 上的本地控制器,负责管理 Node 上所有函数实例的生命周期,以 DeamonSet 的形式存在;
AutoScaler:自动扩缩容模块,会定期检测集群中 Node 和 Pod 的使用情况,同时根据自定义策略进行扩缩容。扩容时,往往会向底层的 PaaS 资源进行申请,例如百度智能云的 CCE、阿里云的 ACK/ASK 等;
Pod :也就是上图标识的 Cold 和 Warm 两种状态的容器,Cold 表示该 Pod 未被使用,Warm 表示正在被使用或者处于等待回收的状态;
Node:包含闲置、占用两种状态,如果一个 Node 上所有 Pod 都是“Cold”,那么该 Node 为闲置状态,否则为占用。
当系统运转时,AutoScaler 会定期检查集群中所有 Node,如果检测到 Node 处于一个需要扩容的状态,则根据策略进行扩容。当然,这个策略完全可以由你自己定义,通常可以按照“占用除以总数的比例是否处于一个合理区间”来进行判定。
在这种形态下, Pod 通常会作为函数实例的通用形态。代码以及不同运行时往往也是以挂载的形式注入到容器内。Autoscaler 也会在轮询时根据 Warm Pod 的闲置时间将其重置为 Cold Pod,从而让整个集群资源达到一个较高复用能力的水平。
在缩容时,理想情况下 AutoScaler 可以将 Node 缩容到 0,但通常为了应对突发的流量请求,也会预留一部分 Buffer 存在。
你会发现,Node 的调度方式对于函数计算调度引擎来说更加的灵活。但“权限越大责任越大”,Node 维度的方案,除了需要管理 Node 的调度之外,还需要处理 Node 中 Pod 的安全隔离和使用。这种形态下,我们可以通过“空 Pod”的方式来提前占用,预加载一部分 Pod 出来,加快“真实 Pod”的创建速度。
Pod 维度
可能你也猜到了,以 Pod 为扩缩容单元的好处就在于它能够更加细粒度地控制函数实例的数量。下面,我们就从扩缩容的“鼻祖”HPA(HorizontalPodAutoscaler)开始,了解 Pod 维度的扩缩容动作流程。
开始之前,你可以先思考这样一个问题,HPA 既然就是用来负责自动扩缩容的,那么它是否可以直接用于 Serverless 呢?先不要急着回答,我们先看 HPA 的扩缩容机制。
常规扩缩容
在 K8s 官网有这样一段关于自身扩缩容控制器 HPA 的定义。
The HorizontalPodAutoscaler is implemented as a Kubernetes API resource and a controller. The resource determines the behavior of the controller. The horizontal pod autoscaling controller, running within the Kubernetes control plane, periodically adjusts the desired scale of its target (for example, a Deployment) to match observed metrics such as average CPU utilization, average memory utilization, or any other custom metric you specify.
不用非要看懂,这里说的,大体可以理解为这个扩缩容控制器做了两件事:
第一件事,定期从 k8s 控制面获取资源的各项指标数据(CPU 利用率、内存使用率等);
第二件事,根据这些指标数据将资源数量控制在一个目标范围内。
HPA架构图(官方)
从 K8s 的官方文档中,你基本可以看到 HPA 的雏形:HPA 通过控制 Deployment 或者 RC 来实现对 Pod 实际数量的控制。官方给出的 HPA 架构图可能还不太好理解,我们把指标(metric)数据的收集过程加上再看看:
HPA的工作流程
具体的工作流程是什么样呢?你可以对照着示意图来理解我下面说的流程。
在 K8s 中,不同的 Metric 会由对应的 Metrics Server 持续采集(Heapster 或自定义 Metrics Server),HPA 会定期通过 Metrics Server 的 API 或者聚合的 API Server 获取到这些 Metric 指标数据(CPU 和 内存使用情况),从而根据你自己定义的扩缩容规则计算出 Pod 的期望个数,最后,根据 Pod 当前的实际数量对 RC/Deployment 做出调整,使 Pod 达到期望的数量。
讲到这里,你应该能很快地回答出来,HPA 形态的扩缩容还不能直接用于 Serverless。
因为 Serverless 语义下的动态扩缩容是可以让服务缩容到 0 的,但 HPA 不能。HPA 的机制是通过监测 Pod 的 Metrics 指标来完成 Deployment 的扩缩容,如果 Deployment 的副本数缩容到 0,流量没有了,指标为 0,那 HPA 也无能为力了。
极致扩缩容
正因为如此,从 0 到 1 的扩缩容过程,是需要额外的机制来加持的。由于目前社区和各大云厂商都非常重视 Knative,部分的传统企业在转型过程中也选择 Knative 作为私有化部署方案的底座首选。所以,我这里同样选择以 Knative 的 KPA 来做说明。
Knative 的扩缩容主要包括三个方面:流量指标的收集、实例数量的调整、从 0 到 1 的过程。
流量指标的收集
扩缩容最重要的一步,就是收集我们所需要的指标。我们可以对照着流程图来理解一下。
在 Knative 中,Revision 代表一个不变的、某一时刻的代码和 Configuration 的快照。每个 Revision 会引用一个特定的容器镜像和运行它所需要的任何特定对象(例如环境变量和卷),再通过 Deployment 控制函数实例的副本数,而我所说的实例,就是图中的 User Pod 了。
和我们之前介绍的 Node 维度扩缩容机制中的函数实例不同,这里,你可以看到每个函数实例(User Pod)中都有两个容器:Queue Proxy 和 User Container。其中,User Container,我相信你一定猜到了,是部署和启动我们自身业务代码的容器,那么 Queue Proxy 呢?
实际上,在每个函数实例被创建时,都会被以 Sidecar 的方式将 Queue Proxy 注入,也就是在原来的业务逻辑上再新加一个抽象层。Queue Proxy 作为每一个 User Pod 的流量入口,负责限流和流量统计的工作。每隔一定时间,AutoScaler 都会收集 Queue Proxy 统计的流量数量,作为后续扩缩容的重要依据。
通俗地来讲,其实就是在自身业务代码的容器旁边,又加了一个双胞胎 Queue Proxy,双胞胎加入的方法,就是 Sidecar。后续不管在这个房子的(User Pod)兄弟干了什么,都会让这个双胞胎去记录。在最外层的 AutoScaler,则会去找这位双胞胎收集所有的信息,为后续的扩缩容做准备。
实例数量的调整
当收集到流量的指标后,AutoScaler 就需要根据指标调整实例的数量了。Autoscaler 会通过改变实例的 Deployment 来决定实例最终的个数,以便确定扩容出来多少个 Pod。
简单的算法,就是按照将当前总的并发数平均划分到期望数量的实例上,使其符合设定的并发值。
这里简单举个例子,比如当前总并发数为 100,设定的并发值为 10,那么最终调整出的实例数量就是:100/10=10 个。当然,扩缩容的实例数量还会考虑到系统的当前的负载和调度周期等因素。
从 0 到 1
看到这,可能你会有个疑问:我们能够确定的,是当 Revision 存在实例时流量接收的情况,那么如果 Revision 实例缩容到了 0,流量又是怎么被接收的呢?是直接丢弃,还是暂存在某个地方呢?
为了解决这一问题,Knative 专门引入了一个叫做 Activator 的组件来做流量暂存和代理负载。
我们还是来看这张图。事实上,当 AutoScaler 将函数实例缩容为 0 时,会控制 Activator 作为实例为 0 时的流量接收入口,也就是图中红线的部分。Activator 在收到流量后,会将请求和信息暂时缓存,并主动告知 AutoScaler 进行扩容,直至成功扩容出函数实例,Activator 才会将暂存的流量转发到新生成的函数实例上。
这里带来一个问题:流量什么时候会切换到 Activator 或者 Knative 的 Pod 上呢?我们会在流量转发一节中仔细讲此类问题。当然,你也可以提前关注ServerlessServices(SKS)和 TBC (target-burst-capacity)等关键组件和参数的相关设置。
扩缩容模型设计思路
到这里,我们已经从 Node 和 Pod 两个不同的维度学习了 Serverless 比较典型的扩缩容机制方案了。那么,思考一下,如果让你来设计一个扩缩容系统,该怎么去实现呢?
Node 和 Pod 的方案一定给了你不小的启发。下面,我将从指标、决策、数量三个核心点,来给你讲一总体的设计思路。
指标
无论是 Node 纬度还是 Pod 纬度,我们会发现最终系统都是根据一些监控的指标来作为扩缩容的依据,同时,这些指标的采集任务往往都是通过定时轮询的方式来完成。
比如,Node 扩缩容案例中用的是 Node 处于空闲或占用的数量,HPA 中采用 CPU 利用率、内存使用率来作为 Pod 扩缩容的依据,而 Knative 是通过 Collector 统计 Pod 的并发情况来进行扩缩容判断。
所以,指标的选取是扩缩容的首要因素,你需要按照你的平台特性,来确定需要收集哪些指标,并且通过某种手段将它们收集上来为你所用。
决策
拿到指标之后,最后都会通过一个专门负责扩缩容的模块(如 AutoScaler)来根据这些指标进行决策。决策的过程,就是以这些指标作为输入,最终输出是否扩缩容、扩容多少或者缩容多少。
决策过程完全可以由你自己定,比如按照比例、固定数量,按照操作后的指标平均值等等。
你也可以根据系统调度情况,设置不同的调度方式,比如针对流量突然上涨采取及时扩容,针对流量的平滑升降采取延时扩容等等。像 Knative 就有两种模式:Stable / 稳定模式和 Panic / 恐慌模式。
数量
决策结果出来之后就简单了。你可以根据结果来让资源数量处于一个预期值,这一步完全不需要你操心,不论是 Node 或者 Pod 维度,K8s 的 HPA 都会来做好。
为服务引入预测能力
最后,还记得我们最开始说的场景 1 的日志处理情况吗?日志的流量出现了明显的波浪式浮动,比如晚上 23 点之后流量减少,上午 6 点之后流量增加。那么,为了应对流量的波峰到来比较迅猛的情况,我们还可以提前预测预扩,让系统的扩容更平滑。
这里还是以 Node 级别的扩缩容为例。我们可以在扩缩容模块的基础上,以旁路的方式额外增加一个预测模块。定期统计 Node 状态和数量的信息,同时根据历史信息对外进行周期预测。当然,历史的信息时间窗口和预测周期都需要经过大量的数据训练以及人工干预因子,才可以在生产环境用得比较顺手。
但我们也要知道,它不是“万金油”,并不是什么情况都能预测准确,只能说在一定阈值范围内起到一定的辅助作用。
同时,这是一个性能和成本博弈的过程,尤其对于云厂商来说,极致的优化成本是我们每个研发人员追求的目标。
小结
最后,我来小结一下我们今天的内容。
这节课,我从常见的两个场景案例入手,介绍了 Serverless 核心特性扩缩容相对于普通 PaaS 服务的优势。这个优势不仅体现在资源的利用率上,进一步来说,更反应在成本上。
接着,我从实际市场运用和历史演进的角度出发,介绍了 Node 和 Pod 两个维度中市面上常见的扩缩容机制。介绍的重点放在了 Kubernetes 的 HPA 以及目前比较火热的明星 Serverless 引擎 Knative 的 KPA 机制上,也从流量指标的收集、实例数据的调整和区别最大的的 0 到 1 的过程来讲解了整个扩缩容的原理。
而上述的讲解,都是为了我们能从共性中抽象出一套设计扩缩容系统的思路,这里面,最主要的还是要抓住三个核心要点:指标、决策、数量。
最后,我们如果是要以平台的视角提供服务,还可以做得更智能,通过预测系统让扩容变得更加平滑,性能更优。