Kubernetes 弹性伸缩之 HPA 原理详解

Kubernetes 弹性伸缩之 HPA 原理详解

本文基于 kubernetes 1.23 版本源码进行分析。

1. 前言

Kubernetes 弹性伸缩机制的主要目的是解决资源与业务负载之间供需平衡的问题。当随着业务的实际使用逐渐增大时,其对资源的需求也将逐渐变得更多,这时就需要对集群和业务增加资源以满足需求,即扩容;而当随着业务结束后,其所需要的资源将变得很少,此时为了不浪费资源就需要将集群或业务的资源进行缩小,同时可将缩下来的资源提供给其他需要的业务进行使用以提高资源整体的利用率,即缩容。在 Kubernetes 的 autoscaler 体系中存在基于两种对象(pod 和node)的自动扩缩容机制,即基于集群角度的 cluster-autoscale 和基于业务 pod 角度的 hpa 和 vpa 扩缩容机制。Kubernetes 中主要的弹性伸缩机制:

类型 操作对象 说明
Cluster-Autoscale node 集群容量(node数量)自动伸缩,跟自动化部署相关的,依赖iaas的弹性伸缩,主要用于虚拟机容器集群
Horizontal-Pod-Autoscaler (HPA) pod Pod水平自动伸缩,如自动scale deployment的replicas,依赖业务实时负载指标
Vertical Pod Autoscaler (VPA) pod Pod垂直(资源配置)自动伸缩,如自动计算或调整deployment的Pod模板limit/request,依赖业务历史负载指标

其中 HPA和 VPA都是从业务负载角度对 pod 数量/资源进行优化的。HPA需要解决的是当业务负载波动变化很大时,HPA controller 会根据业务忙闲情况自动调整副本数量,确保业务的整体的负载达到期望状态以维持负载的平衡稳定,保证服务的可靠性。VPA 主要解决资源配额(Pod的CPU、内存的limit/request)评估不准的问题,业务在运行初期无法确定资源的使用情况,用户往往会申请过多的资源而导致资源的极大浪费,而 VPA 会根据业务的历史资源使用情况来推荐合理的资源请求值,从而使业务的资源申请使用更合理。本文主要详细剖析 hpa 的工作原理。

2. HPA 架构设计详解

hpa 的全称是 Horizontal Pod Autoscaler,其主要是通过监控业务的资源利用情况来实现对 pod副本数进行自动水平扩缩容(scale)的机制,也是k8s里使用需求最广泛的一种自动扩缩容机制。hpa 的实现原理可以简单的理解为两个部分:

  • 数据采集: 即监控指标的采集,HPA 通过 metrcis-server(支持 custom/external metrics server 用户自定义方式采集)从 kubelet 中获取 node/pod 的监控指标,并通过 k8s apiserver 向外部暴露 api 接口。
  • 自动扩缩容: hpa 控制器通过 k8s apiserver 暴露出来的 api 接口获取到监控指标数据并计算出期望副本数,同时 hpa 控制器还会通过一系列算法和约束条件调整期望副本数,保证 hpa 扩缩容的稳定性。hpa 在得到期望副本数后会修改关联对象(如 deployment)子资源的 scale 副本数,然后通过关联对象(如 deployment)来实现对 pod 的自动扩缩容。

其主要架构如下图所示:

hpa

如上图可知,其主要组件包括 hpa controller(核心组件), scaleTargetRef, apiserver, custom metrics server(核心组件), prometheus-server, prometheus-adapter 等。

2.1. hpa controller

hpa controller 主要处理 hpa 对象的业务逻辑,hpa 控制器会定期(可在 kube-controller-manager 通过 –horizontal-pod-autoscaler-sync-period 配置,默认 15s)reconcile 每个 HPA对像,通过定期检查业务的监控指标来触发扩缩容操作。reconcile 逻辑主要是:

  • 首先通过 metrics 的 API 获取该 HPA 的 metrics 实时最新值(在当前副本数服务情况下),并将其与目标期望值进行比较。
  • 然后根据比较的结果确定后期 hpa 的操作:扩容、缩容、不变。若比较结果为不变则直接返回,不需要进行扩缩容操作。
  • 否则会使用相应的算法来计算出目标副本数,最后调用操作对象(如 deployment)的scale接口来调整当前副本数,然后通过操作对象的控制器(如 deployment)来实现 pod 的扩缩容,使业务的每个pod的最终metrics指标(平均值)基本维持到用户期望的水平。

2.2. scaleTargetRef

可理解为 hpa 的扩缩容操作对象,hpa 会通过 scaleTargetRef 所指定的资源对象(必须支持 scale 接口)来实现对 pod 的扩缩容操作。scaleTargetRef 目前支持的对象: Deployment,StatefulSet, ReplicatSet。

2.3. apiserver

是 k8s 的 apiserver,这里主要是指 kube aggregator server,即通过 k8s apiserver 的扩展机制 hpa 的 http 请求将通过 aggregator server 转发到真正的 metrics server 后端进行处理。同时 hpa controller 也会通过 list-watch 机制与 apiserver 建立联系,不断从 apiserver 中获取 metrics 监控指标来 reconcile hpa 对象。如下图所示:

apiserver

2.4. custom metrics server

server 是真正采集/聚合 metrics 指标的服务器,通常会在 k8s 中启动一个 pod 来提供服务,也可以自定义 metrics 服务器(只需实现 标准的 metrics API 接口 即可)。目前 k8s 支持三种类型的 server(括号中为 k8s中对应的 api接口): metrcis-server(metrics.k8s.io), custom metrcis server(custom.metrics.k8s.io), external metrics server(external.metrics.k8s.io)。

  • metrcis server:
    metrics server 是实现 metrics API 接口的服务端,它通过 k8s 的 kubelet API 接口从 pod/node 中获取数据; 同时 metrics server 需要注册到 k8s apiserver 中,并通过 API Aggregation 聚合层来向外部提供各种数据的查询服务(kubectl top 以及 HPA/VPA 都是通过),其实现示意图如下所示。使用 metrics server 时需要注意:
    • metrcis-server 只支持查询node/pod CPU 和内存 的使用指标。注意在 autoscaling/v1 版本时 HPA 只支持CPU指标 ,后期版本 hpa 的spec 值中 metrics 字段改成了列表形式才支持 cpu 和 内存两中形式。
    • metrics-serve 通过 node 节点上的kubelet 提供的接口将采集到的数据汇总到本地,由于 metrics-server 没有持久化模块,数据的采集全保留在内存中,因此是没有保留历史数据,只提供当前最新采集的数据。因此如果要通过 metrics server 获取历史值一般会借助 prometheus 进行数据持久化。

metrics-server

  • custom/external metrics server:
    为了适应更灵活的需求,metrics API开始扩展支持用户自定义metrics指标(custom metrics),自定义数据采集/聚合则需要用户自行开发custom metrics server,社区有提供专门的 custom adpater 框架 custom-metrics-apiserver ,该框架定义了 Custom 和 External 的 MetricsProvider 接口(如下所示),用户只需要实现对应的接口,即可实现用户自定义的 metrics server。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    type MetricsProvider interface {
    CustomMetricsProvider
    ExternalMetricsProvider
    }

    type CustomMetricsProvider interface {
    // GetMetricByName fetches a particular metric for a particular object.
    // The namespace will be empty if the metric is root-scoped.
    GetMetricByName(name types.NamespacedName, info CustomMetricInfo, metricSelector labels.Selector) (*custom_metrics.MetricValue, error)

    // GetMetricBySelector fetches a particular metric for a set of objects matching
    // the given label selector. The namespace will be empty if the metric is root-scoped.
    GetMetricBySelector(namespace string, selector labels.Selector, info CustomMetricInfo, metricSelector labels.Selector) (*custom_metrics.MetricValueList, error)

    // ListAllMetrics provides a list of all available metrics at
    // the current time. Note that this is not allowed to return
    // an error, so it is reccomended that implementors cache and
    // periodically update this list, instead of querying every time.
    ListAllMetrics() []CustomMetricInfo
    }

    type ExternalMetricsProvider interface {
    GetExternalMetric(namespace string, metricSelector labels.Selector, info ExternalMetricInfo) (*external_metrics.ExternalMetricValueList, error)

    ListAllExternalMetrics() []ExternalMetricInfo
    }

    目前社区已经有人基于 prometheus server 的监控数据源,实现了一个 prometheus adapter 来提供 custom metrics server 服务,如果需要的自定义指标数据已经在 prometheus 里了,则可以直接对接使用,否则要先把自定义的指标数据注入到 prometheus server里才行。因为 HPA的负载一般来源于监控数据,而 prometheus 又是 CNCF 标准的监控服务,所以 prometheus adapter基本也可以满足我们所有自定义metrics的HPA的扩展需求。

2.5. prometheus-server

prometheus 是一个知名开源监控系统,具有数据维度多,存储高效,使用便捷等特点。用户可以通过丰富的表达式和内置函数,定制自己所需要的监控数据。

2.6. prometheus-adapter

许多监控系统通过 adapter 实现了接口转换操作,同时给HPA提供指标数据。prometheus-adapter 在 prometheus 和 api-server 中起到了适配器的作用。prometheus-adapter 接受从 HPA 中发来的请求(通过apiserver aggregator中转的指标查询请求),然后根据内容发送相应的请求给prometheus 拿到指标数据,经过处理后返回给 HPA使用。prometheus 可以同时实现 metrics.k8s.io、custom.metrics.k8s.io、 external.metrics.k8s.io 三种 api接口,代替 k8s 自己的 metrics-server 提供指标数据服务。

3. HPA 对象设计详解

3.1. HPA 版本介绍

目前 kubernetes 1.23 版本 autoscaling 支持 4个版本 hpa 对象资源,分别是 autoscaling/v1autoscaling/v2beta1autoscaling/v2beta2autoscaling/v2、。其主要区别如下表所示:

hpa 版本 spec 字段 说明
autoscaling/v1 TargetCPUUtilizationPercentage 只支持 cpu 利用率指标,且将 metrics字段(指标类型如 object, resource 等metrics 类型)放在了annotation中进行处理
autoscaling/v2beta1 Metrics []MetricSpec 支持 Resource 类型的指标(如pod的CPU 和 内存)和 Custom Metrics 自定义指标
autoscaling/v2bet2 Metrics []MetricSpec,Behavior 支持 Resource 类型的指标(如pod的CPU 和 内存)、Custom Metrics 自定义指标 和 ExternalMetrics(k8s 外部指标);同时支持自定义扩缩容行为 behavior
autoscaling/v2 Metrics []MetricSpec,Behavior 支持 Resource 类型的指标(如pod的CPU 和 内存)、Custom Metrics 自定义指标 和 ExternalMetrics(k8s 外部指标);同时支持自定义扩缩容行为 behavior

3.2. hpa 结构字段详解

下面将主要分析 autoscaling/v2 最新版本的 hpa 对象字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# https://github.com/kubernetes/kubernetes/blob/release-1.23/staging/src/k8s.io/api/autoscaling/v2/types.go
// HorizontalPodAutoscalerSpec describes the desired functionality of the HorizontalPodAutoscaler.
type HorizontalPodAutoscalerSpec struct {
// scaleTargetRef points to the target resource to scale, and is used to the pods for which metrics
// should be collected, as well as to actually change the replica count.
ScaleTargetRef CrossVersionObjectReference `json:"scaleTargetRef" protobuf:"bytes,1,opt,name=scaleTargetRef"`
// minReplicas is the lower limit for the number of replicas to which the autoscaler
// can scale down. It defaults to 1 pod. minReplicas is allowed to be 0 if the
// alpha feature gate HPAScaleToZero is enabled and at least one Object or External
// metric is configured. Scaling is active as long as at least one metric value is
// available.
// +optional
MinReplicas *int32 `json:"minReplicas,omitempty" protobuf:"varint,2,opt,name=minReplicas"`
// maxReplicas is the upper limit for the number of replicas to which the autoscaler can scale up.
// It cannot be less that minReplicas.
MaxReplicas int32 `json:"maxReplicas" protobuf:"varint,3,opt,name=maxReplicas"`
// metrics contains the specifications for which to use to calculate the
// desired replica count (the maximum replica count across all metrics will
// be used). The desired replica count is calculated multiplying the
// ratio between the target value and the current value by the current
// number of pods. Ergo, metrics used must decrease as the pod count is
// increased, and vice-versa. See the individual metric source types for
// more information about how each type of metric must respond.
// If not set, the default metric will be set to 80% average CPU utilization.
// +listType=atomic
// +optional
Metrics []MetricSpec `json:"metrics,omitempty" protobuf:"bytes,4,rep,name=metrics"`

// behavior configures the scaling behavior of the target
// in both Up and Down directions (scaleUp and scaleDown fields respectively).
// If not set, the default HPAScalingRules for scale up and scale down are used.
// +optional
Behavior *HorizontalPodAutoscalerBehavior `json:"behavior,omitempty" protobuf:"bytes,5,opt,name=behavior"`
}

对应的 yaml 部署文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: php-apache
namespace: default
spec:
# HPA的伸缩对象描述,HPA会动态修改该对象的pod数量
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: php-apache
# HPA的最小pod数量和最大pod数量,控制 hpa 扩缩容的上下界
minReplicas: 1
maxReplicas: 10
# 监控的指标数组,支持多种类型的指标共存
metrics:
# Object类型的指标
- type: Object
object:
metric:
# 指标名称
name: requests-per-second
# 监控指标的对象描述,指标数据来源于该对象
describedObject:
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
name: main-route
# Value类型的目标值,Object类型的指标只支持Value和AverageValue类型的目标值
target:
type: Value
value: 10k
# Resource类型的指标
- type: Resource
resource:
name: cpu
# Utilization类型的目标值,Resource类型的指标只支持Utilization和AverageValue类型的目标值
target:
type: Utilization
averageUtilization: 50
# ContainerResource类型的指标
- type: ContainerResource
containerResource:
container: C1 # 只收集容器 container name 为 C1 的 memory 指标
name: memory
target:
type: AverageValue
averageValue: 300Mi
# Pods类型的指标
- type: Pods
pods:
metric:
name: packets-per-second
# AverageValue类型的目标值,Pods指标类型下只支持AverageValue类型的目标值
target:
type: AverageValue
averageValue: 1k
# External类型的指标
- type: External
external:
metric:
name: queue_messages_ready
# 该字段与第三方的指标标签相关联,(此处官方文档有问题,正确的写法如下)
selector:
matchLabels:
env: "stage"
app: "myapp"
# External指标类型下只支持Value和AverageValue类型的目标值
target:
type: AverageValue
averageValue: 30
# 扩缩容策略,控制扩缩容的速率
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 100
periodSeconds: 15
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 100
periodSeconds: 15
- type: Pods
value: 4
periodSeconds: 15
selectPolicy: Max

如上所示,hpa spec 字段中的含义:

  • ScaleTargetRef: 定义 hpa 的操作对象,该对象(需要实现 scale 接口,支持对 scale 子资源(sub-resource)进行操作)也是自动扩缩容的管理对象,如 Deployment, ReplicatSet, StatefulSet。

  • MinReplicas: hpa 所能缩容副本数的最小值(下限),可在一定范围内对计算的值起到纠正作用,也起到兜底的作用;默认值是1。k8s v1.16版本之后 minReplicas 可设为 0,但必须满足两个条件:1. metrics 必须配置至少有一个 Object 或者 External 类型;2. 需要对 apiserver 开启 --feature-gates HPAScaleToZero=true 特性。

  • MaxReplicas: hpa 所能扩容副本数的最大值(上限),可确保集群中的资源不被某一个 hpa 对象耗尽。

  • Metrics: hpa 扩容时需要根据用户指定的 metrics 类型(type)和 目标期望值(target)进行扩缩容。目前 metrics 中的 type字段有五种类型的值:Object、Pods、Resource、ContainerResource、External,其中 Pods、External、Object 支持使用筛选器进行筛选,允许进行条件选择。target 共有3种类型:Utilization、Value、AverageValue;其中 Utilization 表示平均使用率,Value表示裸值,AverageValue表示平均值,当业务的整体指标值大于 target 中指定的值时 hpa 会执行扩容逻辑。其详细描述如下表所示:

    metrics type 类型 支持的 target 类型 说明
    Resource Utilization, AverageValue 支持k8s里Pod的所有系统资源(包括cpu、memory等),但是一般只会用cpu,memory因为不太敏感而且跟语言相关
    ContainerResource Utilization, AverageValue 支持 k8s 中某个 pod 下特定 container的cpu和memory指标, 如一个 pod 中包含业务 container 和 sidecar, ContainerResource 可只根据指定的业务container(不考虑 sidecar)指标进行扩缩容
    Pods AverageValue 表示除了cpu,memory 等系统资源之外且是由 Pod 自身提供的自定义 metrics 数据。如 web服务中对 pod 的 QPS 实时监控指标, 该指标数据可通过 prometheus 采集获取并提供给 hpa。需要第三方adapter提供数据。
    Object Value, AverageValue metrics 指标数据不是由 Pod本身的服务提供,但可以由 k8s 内部的其他资源提供,比如ingress等。object 类型一般需要聚合 Deployment下所有关联 pods的总指标。需要第三方adapter提供数据。
    External Value, AverageValue metrics 指标数据的来源跟 k8s本身无关,指标数据完全取自外部系统。需要第三方adapter提供数据。

    注意: 如果HPA spec里不配置任何metrics,k8s会默认设置 Resource类型的 CPU指标,目标类型是 AverageUtilization,value为80%。

  • Behavior: behavior 在 autoscaling/v2beta2 和 autoscaling/v2 中定义,用来精确控制 hpa 扩缩容的速率,遵循 “快速扩容,谨慎缩容“ 的思想,尽量保证线上服务的可靠性。其数据结构定义可参考github,其结构关系可用如下思维导航图表述:

    behavior

    从 behavior 结构定义中可知,主要包括:scaleDown:(缩容速度策略) 和 scaleUp(扩容速度策略) 两个策略,每个扩缩容策略的规则主要包含 StabilizationWindowSeconds(稳定窗口)、SelectPolicy(策略选择)、Policies(扩缩容策略定义)三个字段。

    字段 说明
    StabilizationWindowSeconds(稳定窗口) 当用于扩缩的指标不断波动时,稳定窗口用于限制副本计数的波动。自动扩缩算法使用此窗口来推断先前的期望状态并避免对工作负载规模进行不必要的更改,注意:稳定窗口的推荐值求解是扩容取最小值,缩容取最大值,参考github 源码 ,这样可使变化最小保证稳定。如当指标显示目标应该缩容时,自动扩缩算法查看之前计算的期望状态,并使用指定时间间隔内的最大值作为缩容的期望值。这样的好处是避免了扩缩算法频繁增删 Pod。
    SelectPolicy(扩缩容选择方向) 可以指定扩缩方向的 selectPolicy 字段来更改策略选择。每一个扩缩容策略规则(scaleDown)中可以定义多个 Policies 规则,这样同一个 hpa 对象会计算得到多个 metrics 数据或者副本数,这时可以通过该字段(SelectPolicy:Max/Min/Disabled)对 metrics 数据进行聚合。其中 Max:表示选择其中最大值作为变化量,Min:表示选择其中最小值作为变化量,Disabled: 表示完全禁用该方向的扩缩容操作。注意:如果该字段没有设置或为空,默认按照 Max 进行聚合,表示扩缩容时副本数的变化量选择最大的值。变化量:是指每次扩容(缩容)最多扩(缩)多少个副本数,变化量不是最终的期望值。
    Policies(扩缩容策略) 定义真正的扩缩容速率规则,其值是个切片(同一个hpa可以通过 polices 定义多个规则)。Policies 包含三个字段:Type(指定规则的计算方式), Vaule(指定规则的计算基数), PeriodSeconds(指定在该规则下扩缩容的执行周期)。Policies 规则(Type字段)有两种计算方式(注意:这两种方法计算出的值都是变化值,不是目标值,即每次扩缩容多少个),一种是直接基于 Pods 进行计算,即每次扩缩容时直接指定需要扩缩容多少个 pod;另一种是基于 Percent 百分比计算后得到每次需要扩缩容的 pod 数。注意:Policies 的 PeriodSeconds 必须大于 0 且小等于 1800s (30min)。

    autoscaling/v2bet2 版本的 HPA的 Behavior如果不设置,k8s会自动设置扩缩容的默认配置, 具体内容如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    behavior:
    scaleDown:
    stabilizationWindowSeconds: 300
    policies:
    - type: Percent
    value: 100
    periodSeconds: 15
    scaleUp:
    stabilizationWindowSeconds: 0
    policies:
    - type: Percent
    value: 100
    periodSeconds: 15
    - type: Pods
    value: 4
    periodSeconds: 15
    selectPolicy: Max

    默认配置里分别定义了扩容和缩容的速率策略,缩容按照百分比,每15秒最多减少currentReplicas*100%个副本(但最终不可小于minReplicas),且缩容后的最终副本不得低于过去300s内计算的历史副本数的最大值;扩容则采用快速扩容,不考虑历史计算值(窗口时间为0),每15秒副本数翻倍或者每15秒新增4个副本数(两者取最大值),即:max(2*currentReplicas,4)。

4. HPA 算法详解

HPA 扩缩容算法主要在 HPA 控制器里面实现,其中比较重要的算法主要有:1. 如何从 metrics 获取指标并计算得到期望副本数。2. 如何计算出的副本值进行约束(规范化),避免 metrics 中的数据异常导致扩缩容异常。3. 如何通过 behavior 方式控制扩缩容的速率,保证扩缩容的稳定性。下面先看看 hpa controller 的主要逻辑是如何实现的。

4.1. HPA 扩缩容主流程

HPA controller 会定期(可在 kube-controller-manager 通过 –horizontal-pod-autoscaler-sync-period 配置,默认 15s)通过list-watch 机制从 apiserver 中 watch 到 hpa 对象并不断调协每个 HPA 对像使其达到期望值,hpa controller 的主要逻辑是如何通过一定的算法计算出期望值,然后再调用 ScaleTargetRef 的 scale 子资源接口间接对 pod 进行扩缩容操作。从源码中可知 hpa controller 的主要逻辑如下图所:

autoscale01

从上图可知,hpa controller 的主要流程如下:

  • 首先通过 scaleTargetRef 从 scheme 中获取到关联对象的 GVK/GVR 等资源,并通过 resources 获取到子资源 scale。并将子资源的replicas 计为当前 hpa 的 currentReplicas。
  • 随后对 currentReplicas 的值进行一系列的判断,根据 currentReplicas 的值做出不同的操作,并计算出 desiredReplicas。
    • 标注 [1] 表示该资源不支持扩所容操作,会直接结束 reconcile。
    • 标注 [2], [3] 表示 hpa scale 的当期值不满足 hpa 对象的定义,直接将 scale 的值规范到 hpa定义的约束中,再执行扩缩容操作。
    • 标注 [4] 表示 hpa scale 的值满足 hpa 定义的约束,接下来就是根据 metrics 指标数据进行自动扩缩容,后面会主要讲这部分的逻辑。
  • 根据计算得出的 desiredReplicas 调用 scale 接口进行扩缩容操作。

上图中最重要的逻辑是标注[4],该部分逻辑主要包括:通过 metrics 指标计算 desiredReplicas;对计算出来 desiredReplicas 进行规范化处理;如果定义了 behavior 字段,会通过 behavior 字段中的值对扩缩容速率进行限制。

4.2. 通过 metrics 指标计算副本数

hpa 的主要思想是通过用户指定 metrics 指标从监控中获取数据并计算出扩缩容的副本数,这部分的主要逻辑主要在 computeReplicasForMetrics中。对副本的计算方法,不同类型的 metrics 其计算 desiredReplicas 实现细节不一样,且同一种 metric 不同的 target 类型其算法也是不一样的,因此每个 metrics 都有其单独的算法来计算副本数,如 computeReplicasForMetric。但是总的来说,计算公式可以抽象为:

1
desiredReplicas = ceil[currentReplicas * ( currentMetricValue / desiredMetricValue )]

从上述公式中,我们可以通过metrics指标的系数比 (currentMetricValue / desiredMetricValue) 来判断 hpa 是扩容还是缩容逻辑,如果: 系数比>1.0,则 hpa 会进行扩容操作; 系数比=1.0,则不变;系数比<1.0,则 hpa 会进行缩容操作。同时公式中的 ceil 操作会对计算出的浮点数向上取整操作,如计算值=5.2 时 desiredReplicas 会取值 6。

此外为避免获取的 metrics 指标异常或者 metrics 指标数据变化频繁而导致 hpa 频繁扩缩最终服务抖动的现象出现,可设置 hpa 的容忍度配置(默认为 0.1,即如果期望变化的副本倍数在[0.9, 1.1] 之间就直接停止计算并返回),容忍度配置可在 kube-controller-manager里的参数 horizontal-pod-autoscaler-tolerance 指定。

注意:如果 hpa 对象中指定了多个 metrics 指标,hpa controller 会从多个 metrics 中取最大的值作为期望的副本数。

1
说明:这里对各种 metrics 的具体实现暂不做分析,后期有时间会专门研究另作一篇文章。

4.3. 扩缩容副本数规范化(desiredReplicas 约束)

扩缩容副本数规范化是指对从 metrics 指标中计算出的 desiredReplicas 进行规范约束,即将 desiredReplicas 的值设置在 hpa 定义的合理范围内,避免 metrics 指标异常导致计算的副本数异常,可在一定范围内保证服务的稳定性。对 desiredReplicas 值的规范约束可用如下图表示:

desiredReplicas

其中 hapMaxReplicas、hapMinReplicas(没有指定时默认为1) 是指 hpa 对象能扩缩容的最上、下限,分别对应 hpa spec 中的 MaxReplicas,、MinReplicas。maximumAllowedReplicas、minimumAllowedReplicas 分别表示 hpa 对象扩缩容经过一系列约束(包含稳定窗口约束、behavior 扩缩容速率约束等)后被允许达到的最大值、最小值, maximumAllowedReplicas 的取值必须在 [minimumAllowedReplicas, hapMaxReplicas] 范围内; minimumAllowedReplicas 的取值必须在 [hapMinReplicas, maximumAllowedReplicas] 范围内。desiredReplicas 是扩缩容的期望副本数,其取值范围在[minimumAllowedReplicas, maximumAllowedReplicas] 内。

在 hpa controller 逻辑里主要有两个函数实现了 desiredReplicas 规范化:normalizeDesiredReplicasnormalizeDesiredReplicasWithBehaviors 函数。

  • normalizeDesiredReplicas: behavior 字段为空时对副本数进行规范化处理(可等同于behavior 不为空时的默认扩缩容机制),主要包括下面三个处理流程。如下图所示:

    • 设置默认的缩容稳定窗口 downscaleStabilisationWindow,即缩容冷却机制
    • 计算扩容副本约束值,max(2currentReplicas,4),即单次扩容的副本数不能超过当前的2倍(而如果原副本数小于2,则可以一次性*扩容到 4个副本)。其目的是避免 metrics 中计算的副本数变化倍数太大时 hpa 一次性扩容太多而将集群中的资源消耗殆尽。
    • 通过约束值对 desiredReplicas 进行合理求值。

    autoscale02

  • normalizeDesiredReplicasWithBehaviors: behavior 字段非空时 通过 behavior 字段中设置的值来约束副本数,即对副本数进行规范化处理,主要也包含下面三个处理流程。如下图所示:

    • 通过 behavior 中的 StabilizationWindowSeconds 字段(如果为空,缩容会设置默认值为 downscaleStabilisationWindow,即设置默认的缩容稳定窗口冷却机制;扩容不会设置默认值)来求出稳定窗口中的推荐值,扩容时:推荐值等于所有记录中的最小值,缩容时:推荐值等于所有记录中的最大值。推荐值会作为候选 desiredReplicas 继续后面的约束判断。
    • 根据上面的 desiredReplicas 和 currentReplicas 来判断扩/缩容逻辑。

    autoscale03

综上可知:hpa controller 的 desiredReplicas 规范化约束包含两种模式:一种适应于旧版本(如 autoscaling/v1),一种是添加 behavior 字段的 autoscaling/v2bet2 以上版本中使用。从源代码的角度可知,behavior 字段包含了 StabilizationWindowSeconds、SelectPolicy、Policies 字段,实现了更为精细的扩缩容控制。

4.4. 扩缩容的几个约束

在上一节讲述 desiredReplicas 值的取值约束中,maximumAllowedReplicas、minimumAllowedReplicas 的设计思想目的是尽量保证 hpa 扩缩容的稳定性,确保尽量做的”快速扩容,谨慎缩容”,而这两个值的计算也是通过一些系列约束得出的。在 hpa controller 逻辑里主要包含下面几个约束:缩容冷却机制稳定窗口behavior 扩缩容速率约束

4.4.1. 缩容冷却机制

缩容冷却机制总的来说是避免因流量异常而导致服务缩容异常,最终导致服务抖动不稳定。k8s HPA算法的默认扩缩容原则是:快速扩容,谨慎缩容

  • 对 hpa 对象的单次缩容机制:
    虽然 HPA 同时支持扩容和缩容,但在生产环境上扩容一般来说重要性更高,特别是在大促流量突增的时候,能否快速扩容决定了系统的稳定性,所以HPA的算法里对扩容的时机是没有额外限制的,只要达到扩容条件就执行扩容逻辑(当前一次至少扩容到原来的1.1倍)。但对于缩容,为了避免过早缩导致服务来回波动(thrashing 抖动)而影响服务的稳定性,HPA的算法对缩容的要求比较严格,通常会设置一个滑动窗口(默认为 5分钟,可通过 kube-controller-manager 的参数 horizontal-pod-autoscaler-downscale-stabilization 来修改)来记录过去最近一段时间内的期望副本数,只有连续窗口时间内(如 5分钟内)计算出的期望副本数都比当前副本数小,才执行scale缩容操作,缩容的目标副本数取窗口期内所有记录的最大值。从源码中可以看到在 hpa controller 对 desiredReplicas 进行规范化约束前会执行缩容冷却机制

  • 对同一个 hpa 对象的多次扩缩容之间的冷却机制:
    在弹性伸缩中,冷却周期是不能逃避的一个话题,很多时候我们期望快速弹出与快速回收,而另一方面,我们又不希望集群震荡导致服务不稳定,所以一个弹性伸缩活动冷却周期的具体数值是多少,一直被开发者所挑战。在 HPA 中,默认的扩容冷却周期是 3 分钟,即对于同一个 hpa 对象的两次扩容时间间隔是 3分钟;缩容冷却周期是 5分钟,即对于同一个 hpa 对象的两次缩容时间间隔是 5分钟。

4.4.2. 稳定窗口机制

稳定窗口机制在早期是只有缩容时才有的机制(缩容冷却机制 downscaleStabilisationWindow),即在缩容时会通过一个滑动窗口来记录最近计算的副本数,并选择最大值作为目标期望副本数,且该滑动窗口可以通过参数在 horizontal-pod-autoscaler-downscale-stabilization 来修改,扩容使用固定速率:max(2*currentReplicas, 4)。但随着 autoscaling/v2bet2 版本添加 behavior 字段后,使扩缩容做到更精确更细致的控制,通过 StabilizationWindowSeconds 字段对扩容和缩容实现了稳定窗口机制,对于扩容会选择窗口内的所有记录中的最小值作为候选 desiredReplicas,对于缩容会选择窗口内的所有记录中的最大值作为候选 desiredReplicas。扩容选择最小值缩容选择最大值都是为保证在一次扩缩容时使得变化量最少,可最大程度保证服务的稳定性。如下图所示:

behavior01

4.4.3. 扩缩容速率控制

在 HPA controller里默认扩缩容总原则是:快速扩容,谨慎缩容。早期版本,扩容是使用固定速率:max(2*currentReplicas, 4)来进行扩容;缩容仅仅只是通过设置一个集群全局的窗口时间(downscaleStabilisationWindow)来约束,窗口期过后也就失去控制能力。但 autoscaling/v2bet2 版本后对扩缩容速率做了更多细致化的优化,即会通过 behavior 字段进行约束(StabilizationWindowSeconds + SelectPolicy + Policies)。下面主要讲述 autoscaling/v2bet2 下基于 bahavior 方式来控制扩缩容的速率。具体看如下图所示:

  • behavior 中的 StabilizationWindowSeconds 用来实现滑动窗口机制,在扩容/缩容时都可以配置滑动窗口的时间来更细粒度的控制扩/缩容的速率,在计算滑动窗口的推荐值时,扩容会选则滑动窗口期内的所有记录的最小值作为推荐值,缩容会选择滑动窗口期内的所有记录的最大值作为推荐值。注意: 缩容时,如果 ScaleDown 策略中的 behavior 非空并且 StabilizationWindowSeconds 没有设置时,hpa 会使用默认的 downscaleStabilisationWindow 窗口来限制缩容的速率。

  • behavior 中的 Policies 是真正控制扩缩容速率的部分:

    • policies 中对副本的计算方式有两种类型 type:Pods 和 Percent
    • 指定周期 PeriodSeconds,每次扩缩容周期内只能扩/缩 podsValue(currentReplicas*PercentValue) 个副本数
    • 结合 behavior 中的选择策略方向 SelectPolicy,当有多个策略同时计算副本时,副本数的最后结果需要通过SelectPolicy:Max/Min/Disabled做聚合,注意:Max表示选择变化量最大的值,Min表示选择变化量最小的值,Disabled表示禁止这个方向的扩缩容。

behavior02

5. 总结

本文首先从架构设计的角度进行分析,对 hpa 的各个组件的功能和设计上进行了简单描述,从整体上了解到 hpa 工作原理,简述如下:hpa controller 通过 apiserver 从 metrics-server 中获取监控指标,并通过一定的算法来计算出期望的副本数,随后 hpa controller 会调用 hpa 对象所关联对象的 scale 接口实现对 pod 的扩缩容操作;而监控指标的获取是 metrics-server 调用 kubelet 提供的 api 接口从容器中获取的。由此可简单的根据 pod cpu 指标的变化情况实现 hpa 对pod 的扩缩容操作;但为了满足复杂的业务场景,hpa 也支持用户通过自定义指标来实现自动扩缩容操作,目前用户可通过 custom.metrics.k8s.io 和 external.metrics.k8s.io 两个接口实现自定义 api 接口的扩展,但是相应的需要自己实现类似 metrics-server 的 server 功能,实现对指标的采集和聚合操作并向外提供api查询服务,由此社区也提供了 prometheus + prometheus-adapter 的方案。

随后本文从源码角度剖析了 hpa 的实现原理,先是简单介绍了 hpa 多个版本的区别,之后通过分析 hpa 对象的字段结构、yaml文件配置等了解到如何使用 hpa 的。hpa 对象的 spec 字段主要包括 ScaleTargetRef、MaxReplicas、MinReplicas、Metrics、Behavior 五个字段,其中 ScaleTargetRef 和 MaxReplicas 是必选字段,其他三个是可选字段。

  • ScaleTargetRef 用来指定扩缩容的关联对象;hpa 会通过它的 scale 子资源接口实现扩缩容操作。
  • MinReplicas 和 MaxReplicas 用来指定 hpa 扩缩容的上下界,MinReplicas 未指定时默认值为1;
  • Metrics 用来指定扩缩容指标,是一个切片类型,可以指定多个metric,目前每个 metric 支持的类型主要要五种:Resource、ContainerResource、Pods、Object、External 类型,每种类型的使用场景和值类型也不一样,其计算 metrics指标的聚合数据方法也是不一样的,前面 4种类型使用于 k8s 内部资源指标,External 使用于 k8s 外部资源指标。
  • Behavior 字段用来指定 hpa 扩缩容速率,其包含三个字段:StabilizationWindowSeconds、SelectPolicy、Policies。StabilizationWindowSeconds 字段用来实现稳定窗口,目的是确保在 metrics 数据异常或者流量激增的情况下可保证服务的稳定性,特别是缩容场景中。SelectPolicy 和 Policies 结合使用,用来确定扩缩容的速率以及方向或者禁用扩缩容。

最后详细介绍了 hpa 逻辑中涉及到的一些算法和约束,如缩容冷却机制,desiredReplicas 规范化,稳定窗口机制,扩缩容速率约束等算法。通过这些算法,从总体上实现了 k8s hpa 扩缩容的基本原则:快速扩容,谨慎缩容

6. 参考


Kubernetes 弹性伸缩之 HPA 原理详解
https://qingwei8.github.io/2022/01/18/kubernetes-controller-hpa-2022-01-18-hpa/
作者
qingwei
发布于
2022年1月18日
许可协议