为什么HPA扩容比较慢

最近遇到业务活动期间遇到突发流量,由于pod资源使用飙升导致业务可用性降低的问题。这里面导致业务不可用的原因有很多,其中一个直接原因是流量来临时候资源使用飙升,而HPA没有及时的进行扩容。 这篇文章就是针对这个问题进行研究,主要从这三方面进行阐述:

  1. 扩容有多慢
  2. 为什么扩容慢
  3. 有什么解决方案

为了说明扩容有多慢,进行HPA扩容测试,记录每一时刻的副本数和pod的资源使用,对比流量增加时间和第一次副本数增加时间,粗略得出扩容的延迟。

hpa的数据源配置有"Object"、“Pods”、“Resource”、“ContainerResource”、“External”。为了测试简单这里只使用"Resource"进行测试,同时kubernetes版本为1.23。

测试方法:准备一个nginx deployment和service,然后对这个service进行压测,记录下每一时刻的副本数。

nginx deployment副本数为2,CPU的request为20m,hpa的目标设置为request的20%平均利用率。

 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
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx:1.18
        imagePullPolicy: IfNotPresent
        name: nginx
        ports:
        - containerPort: 80
          protocol: TCP
        resources:
          requests:
            cpu: 20m
---
 apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler  
metadata:
  name: nginx-deployment
  namespace: default
spec:
  maxReplicas: 10
  metrics:
  - resource:
      name: cpu
      target:
        averageUtilization: 20
        type: Utilization
    type: Resource
  minReplicas: 2
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: nginx-deployment
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: nginx
  name: ngx-service
  namespace: default
spec:
  clusterIP: 10.252.211.253
  clusterIPs:
  - 10.252.211.253
  externalTrafficPolicy: Cluster
  internalTrafficPolicy: Cluster
  ipFamilies:
  - IPv4
  ipFamilyPolicy: SingleStack
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: nginx
  sessionAffinity: None
  type: ClusterIP

使用ab命令对cluster ip进行压测

1
2
3
4
# date;ab -n 100000 -c 20 10.252.211.253/;date
Thu Nov  2 13:10:00 CST 2023
.....
Thu Nov  2 13:10:11 CST 2023

在另一个窗口记录pod metrics

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#while :; do date; kubectl get pods.metrics.k8s.io  -l app=nginx;echo;sleep 1;done

Thu Nov  2 13:10:23 CST 2023
NAME                                CPU   MEMORY   WINDOW
nginx-deployment-596d9ffddd-6lrhv   0     9604Ki   17.068s
nginx-deployment-596d9ffddd-w6cm2   0     2060Ki   17.634s

Thu Nov  2 13:10:25 CST 2023
NAME                                CPU          MEMORY   WINDOW
nginx-deployment-596d9ffddd-6lrhv   505634152n   9548Ki   13.763s
nginx-deployment-596d9ffddd-w6cm2   523202787n   2060Ki   13.914s

Thu Nov  2 13:10:27 CST 2023
NAME                                CPU          MEMORY   WINDOW
nginx-deployment-596d9ffddd-6lrhv   505634152n   9548Ki   13.763s
nginx-deployment-596d9ffddd-w6cm2   523202787n   2060Ki   13.914s

在另一个窗口记录副本数

1
#while :; do date;kubectl get deployments.apps  nginx-deployment ;sleep 1;echo;done

在另一个窗口记录hpa资源变化

1
 kubectl get hpa nginx-deployment -o yaml -w 

完整的测试记录在https://gist.github.com/wu0407/ebea8c0ee9ecbc15e94b3122f1a193dc

  1. 从13:10:00开始进行压测到13:10:11压测结束。
  2. 在13:10:26扩容2个副本,13:10:42扩容4个副本,13:10:57扩2个副本
  3. 在13:10:25 pod metrics资源观察到pods使用资源增加

replicas-result

由于pod的扩容阈值是CPU平均使用为4m,可以粗略的认为只要有请求,pod的CPU平均使用率就超过4.2m(这里要加--horizontal-pod-autoscaler-tolerance,默认为0.1),那么这个实验里的扩容延迟为26s。

扩容是分成3个阶段,而不是一下扩容到10个副本。

甚至在没有压测流量时候还进行扩容,hpa对象里的status.currentMetrics里资源使用为0,但是desiredReplicascurrentReplicas不相等。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
  currentMetrics:
  - resource:
      current:
        averageUtilization: 0
        averageValue: "0"
      name: cpu
    type: Resource
  currentReplicas: 8
  desiredReplicas: 10
  lastScaleTime: "2023-11-02T05:10:57Z"

为什么会出现上面扩容的行为呢?为什么会有扩容延迟?

为了回答上面问题,首先需要知道HPA的扩容机制和HPA的扩容算法。

horizontal pod autoscaler controller是kube-controller-manager的一部分,的它通过访问apiserver获得各个类型的资源监控数据。而这些监控数据是metrics-server提供的,metrics-server作为Aggregated API Servers扩展metrics.k8s.io 组下的API。custom.metrics.k8s.ioexternal.metrics.k8s.io 组下的API是由prometheus-adapter作为Aggregated API Servers提供的。

下面是HPA的架构图

hpa-architecture

图片来自:https://www.weave.works/blog/kubernetes-horizontal-pod-autoscaler-and-prometheus

horizontal pod autoscaler controller默认每隔15秒执行一个HPA对象的调谐,即每隔15秒根据监控数据计算期望的副本数。如果期望的副本数不等于当前的副本数,则进行扩缩容。

流程:

  1. 访问apiserver获得监控数据
  2. 计算期望的副本数
  3. 扩缩容行为控制

根据不同类型的数据源访问不同地址,这里就不展开了,详细可以看我的HPA代码注释

ratio为当前metric值与目标值的比值

tolerance--horizontal-pod-autoscaler-tolerance,它指定了扩缩时候容忍抖动的范围,默认值为0.1

副本数为workload里scale资源的spec.replicas

当前的副本数为scale资源里的status.replicas

desiredReplicas为期望的副本数

  1. target类型是"Value",ratio = MetricValue/spec.metrics[*].object.target.value
    • 如果spec.replicas为0,则desiredReplicas为ratio向上取整。
    • spec.replicas大于0,如果ratio在[1-tolerance, 1+tolerance]范围内就不进行扩缩容,desiredReplicas为spec.replicas。否则desiredReplicas为ratio*readyPodCount向上取整
  2. target类型为"AverageValue",ratio = MetricValue / (spec.metrics[*].object.target.averageValue * status.replicas)
    • 如果ratio在[1-tolerance, 1+tolerance]范围内,不进行扩缩容,desiredReplicas为spec.replicas。
    • 否则desiredReplicas为MetricValue / spec.metrics[*].object.target.averageValue向上取整

hpa-object

readyPodCount为处于ready状态的pod的数量

  1. target类型是"Value",ratio = totalValue / spec.metrics[*].external.target.value
    • 如果spec.replicas为0,否则desiredReplicas为ratio向上取整
    • spec.replicas大于0,如果ratio在[1-tolerance, 1+tolerance],则不进行扩缩容,desiredReplicas为spec.replicas。否则desiredReplicas为ratio*readyPodCount向上取整
  2. target类型是"AverageValue",ratio = totalValue / (spec.metrics[*].external.target.averageValue * status.replicas)
    • 如果ratio在[1-tolerance, 1+tolerance],则不进行扩缩容,desiredReplicas为status.replicas。
    • 否则desiredReplicas为totalValue / spec.metrics[*].external.target.averageValue向上取整

hpa-external

因为pod的状态和pod是否有监控数据会影响副本数计算,所以要进行pod分类,对不同类型的pod进行不同的监控数据修复。

cpuInitializationPeriod:为--horizontal-pod-autoscaler-cpu-initialization-period的值,默认为5分钟

delayOfInitialReadinessStatus:为--horizontal-pod-autoscaler-initial-readiness-delay的值,默认为30s

根据pod状态和监控数据,对pod进行分类。分为“ready且有监控数据”、“unreadyPods”、“ignoredPods”、“missingPods”

unreadyPods

  • pod的Phase为pending
  • resource、containerResource类型的数据源且为CPU资源监控的数据
    • pod status没有type为Ready的condition或pod.Status.StartTime为nil(pod未被kubelet接管)
    • 计算副本时间还未超过pod启动时间加cpuInitializationPeriod,且ready condition为false
    • 计算副本时间还未超过pod启动时间加cpuInitializationPeriod,且为ready状态且metric的timestamp是在readyCondition.LastTransitionTime加metric.Window之前
    • 计算副本时间已经超过pod启动时间加cpuInitializationPeriod,且ready condition为false,且readyCondition.LastTransitionTime在pod.Status.StartTime加delayOfInitialReadinessStatus时间内

missingPods:没有监控数据的pod

ignoredPods:pod被删除或pod的phase为“Failed”

上面两种类型的监控数据是聚合数据,即多个pod对应一个监控数据。而下面三种类型的监控数据不是聚合数据,即每个pod对应一个监控数据。如果pod异常和监控数据缺失都会导致计算出的副本数异常,为了避免过多的扩容和缩容,所以需要进行数据修复。

  • 根据ready pod数量和已有的监控数据,计算出副本数,根据这个副本数在不需要考虑tolerance下是否需要扩缩容。如果需要扩容,则没有监控数据pod的监控数据修正为0。需要缩容,则没有监控数据pod的监控数据修正为HPA对象里设置目标值。
  • 需要扩容且存在unready pod,则unready pod的监控数据修复为0。(为了防止新生成的pod启动时候使用CPU很高,导致一直触发扩容)
unreadyPods > 0 missingPods > 0 action
scale up true true fix unreadyPods and missingPods metrics value as 0
scale up true false no action
scale up false true fix missingPods metrics value as 0
scale up false false no action
scale down true true fix missingPods metrics as target value
scale down true false no action
scale down false true fix missingPods metrics as target value
scale down false false no action

readyMissingPodMetricsCount为所有pod metrics中移除ignoredPodsunreadyPods后的metrics数量。

afterFixMetricsCount为数据修复之后pod的metric数量

  1. 计算ratio=metricsTotal / (readyMissingPodMetricsCount * spec.metrics[*].pods.target.averageValue)
  2. 如果有missingPods且ratio小于1(缩容),则missingPods的监控数据修复为spec.metrics[*].pods.target.averageValueafterFixMetricsCount重新包括了missingPods数量。
  3. 如果有missingPods且ratio大于等于1(扩容或不扩不缩),则missingPods的监控数据修复为0,afterFixMetricsCount重新包括了missingPods数量。如果有unreadyPods,则unreadyPods的监控数据修复为0。afterFixMetricsCount重新包括了unreadyPods数量。
  4. 如果没有missingPods且ratio大于1+tolerance(扩容)且存在unreadyPods,则unreadyPods的监控数据修复为0,afterFixMetricsCount重新包括了missingPods数量
  5. 如果没有missingPods且ratio大于1+tolerance(扩容)且不存在unreadyPods,则desiredReplicasratio*readyPodCount向上取整
  6. 如果没有missingPods且ratio在[1-tolerance, 1+tolerance],则不进行扩缩容desiredReplicas为spec.replicas
  7. 如果没有missingPods且ratio小于1-tolerance,则进行缩容desiredReplicasratio*readyPodCount向上取整
  8. 重新计算新的ratio,newRatio=afterFixMetricsTotal / (afterFixMetricsCount * spec.metrics[*].pods.target.averageValue
  9. 如果新的ratio在[1-tolerance, 1+tolerance],则不进行扩缩容desiredReplicas为spec.replicas
  10. 如果新的ratio大于1+tolerance,且修复前的ratio小于1-tolerance,即修复之前是缩容且修复之后是扩容,则不进行扩缩容desiredReplicas为spec.replicas
  11. 如果新的ratio小于1-tolerance,且修复前的ratio大于1+tolerance,即修复之前是扩容且修复之后是缩容,则不进行扩缩容desiredReplicas为spec.replicas
  12. 计算新的副本数ceil( afterFixMetricsTotal / spec.metrics[*].pods.target.averageValue)
  13. 如果新的ratio大于1+tolerance,且修复前的ratio大于1+tolerance,即修复之前是扩容且修复之后是扩容,且新的副本数小于spec.replicas,则不进行扩缩容desiredReplicas为spec.replicas
  14. 如果新的ratio小于1-tolerance,且修复前的ratio小于1-tolerance,即修复之前是缩容且修复之后是缩容,且新的副本数大于spec.replicas,则不进行扩缩容desiredReplicas为spec.replicas
  15. 剩余的情况,desiredReplicasceil( afterFixMetricsTotal / spec.metrics[*].pods.target.averageValue)向上取整

hpa-pods

这个跟Pods类型的基本一样,只是获取metrics数据的方法不一样,target value是spec.metrics[*].resource.target.averageValue

readyMissingPodMetricsCount为所有pod metrics中移除ignoredPodsunreadyPods后的metrics数量。

afterFixMetricsCount为数据修复之后pod的metric数量

  1. 计算ratio=metricsTotal / (readyMissingPodMetricsCount * spec.metrics[*].resource.target.averageValue)
  2. 如果有missingPods且ratio小于1(缩容),则missingPods的监控数据修复为spec.metrics[*].resource.target.averageValueafterFixMetricsCount重新包括了missingPods数量。
  3. 如果有missingPods且ratio大于等于1(扩容或不扩不缩),则missingPods的监控数据修复为0,afterFixMetricsCount重新包括了missingPods数量。如果有unreadyPods,则unreadyPods的监控数据修复为0。afterFixMetricsCount重新包括了unreadyPods数量。
  4. 如果没有missingPods且ratio大于1+tolerance(扩容)且存在unreadyPods,则unreadyPods的监控数据修复为0,afterFixMetricsCount重新包括了missingPods数量
  5. 如果没有missingPods且ratio大于1+tolerance(扩容)且不存在unreadyPods,则desiredReplicasratio*readyPodCount向上取整
  6. 如果没有missingPods且ratio在[1-tolerance, 1+tolerance],则不进行扩缩容desiredReplicas为spec.replicas
  7. 如果没有missingPods且ratio小于1-tolerance,则进行缩容desiredReplicasratio*readyPodCount向上取整
  8. 重新计算新的ratio,newRatio=afterFixMetricsTotal / (afterFixMetricsCount * spec.metrics[*].resource.target.averageValue
  9. 如果新的ratio在[1-tolerance, 1+tolerance],则不进行扩缩容desiredReplicas为spec.replicas
  10. 如果新的ratio大于1+tolerance,且修复前的ratio小于1-tolerance,即修复之前是缩容且修复之后是扩容,则不进行扩缩容desiredReplicas为spec.replicas
  11. 如果新的ratio小于1-tolerance,且修复前的ratio大于1+tolerance,即修复之前是扩容且修复之后是缩容,则不进行扩缩容desiredReplicas为spec.replicas
  12. 计算新的副本数ceil( afterFixMetricsTotal / spec.metrics[*].resource.target.averageValue)
  13. 如果新的ratio大于1+tolerance,且修复前的ratio大于1+tolerance,即修复之前是扩容且修复之后是扩容,且新的副本数小于spec.replicas,则不进行扩缩容desiredReplicas为spec.replicas
  14. 如果新的ratio小于1-tolerance,且修复前的ratio小于1-tolerance,即修复之前是缩容且修复之后是缩容,且新的副本数大于spec.replicas,则不进行扩缩容desiredReplicas为spec.replicas
  15. 剩余的情况,desiredReplicasceil( afterFixMetricsTotal / spec.metrics[*].resource.target.averageValue)向上取整

hpa-resource-AverageValue

这里计算ratio发生了变化,ratio = metricsTotal * 100 / (requestTotal * spec.metrics[*].resource.target.averageUtilization)

其中requestTotal为所有pod里的container资源的request总和

readyMissingPodMetricsCount为所有pod metrics中移除ignoredPodsunreadyPods后的metrics数量。

afterFixMetricsCount为数据修复之后pod的metric数量

  1. 计算ratio= metricsTotal * 100 / (requestTotal * spec.metrics[*].resource.target.averageUtilization)
  2. 如果有missingPods且ratio小于1(缩容),则missingPods的监控数据修复为spec.metrics[*].resource.target.averageUtilizationafterFixMetricsCount重新包括了missingPods数量。
  3. 如果有missingPods且ratio大于等于1(扩容或不扩不缩),则missingPods的监控数据修复为0,afterFixMetricsCount重新包括了missingPods数量。如果有unreadyPods,则unreadyPods的监控数据修复为0。afterFixMetricsCount重新包括了unreadyPods数量。
  4. 如果没有missingPods且ratio大于1+tolerance(扩容)且存在unreadyPods,则unreadyPods的监控数据修复为0,afterFixMetricsCount重新包括了missingPods数量
  5. 如果没有missingPods且ratio大于1+tolerance(扩容)且不存在unreadyPods,则desiredReplicasratio*readyPodCount向上取整
  6. 如果没有missingPods且ratio在[1-tolerance, 1+tolerance],则不进行扩缩容desiredReplicas为spec.replicas
  7. 如果没有missingPods且ratio小于1-tolerance,则进行缩容desiredReplicasratio*readyPodCount向上取整
  8. 重新计算新的ratio,newRatio=afterFixMetricsTotal * 100 / (requestTotal * spec.metrics[*].resource.target.averageUtilization
  9. 如果新的ratio在[1-tolerance, 1+tolerance],则不进行扩缩容desiredReplicas为spec.replicas
  10. 如果新的ratio大于1+tolerance,且修复前的ratio小于1-tolerance,即修复之前是缩容且修复之后是扩容,则不进行扩缩容desiredReplicas为spec.replicas
  11. 如果新的ratio小于1-tolerance,且修复前的ratio大于1+tolerance,即修复之前是扩容且修复之后是缩容,则不进行扩缩容desiredReplicas为spec.replicas
  12. 计算新的副本数ceil( afterFixMetricsTotal * newRatio)
  13. 如果新的ratio大于1+tolerance,且修复前的ratio大于1+tolerance,即修复之前是扩容且修复之后是扩容,且新的副本数小于spec.replicas,则不进行扩缩容desiredReplicas为spec.replicas
  14. 如果新的ratio小于1-tolerance,且修复前的ratio小于1-tolerance,即修复之前是缩容且修复之后是缩容,且新的副本数大于spec.replicas,则不进行扩缩容desiredReplicas为spec.replicas
  15. 剩余的情况,desiredReplicasceil( afterFixMetricsCount * newRatio)向上取整

hpa-resource-AverageUtilization

这个跟“Resource类型的数据源且type为AverageValue”计算方式类似,只是metricsTotal是每个pod metrics里container(spec.metrics[*].containerResource.container)的metrics value。

readyMissingPodMetricsCount为所有pod metrics中移除ignoredPodsunreadyPods后的metrics数量。

afterFixMetricsCount为数据修复之后pod的metric数量

  1. 计算ratio=metricsTotal / (readyMissingPodMetricsCount * spec.metrics[*].containerResource.target.averageValue)
  2. 如果有missingPods且ratio小于1(缩容),则missingPods的监控数据修复为spec.metrics[*].containerResource.target.averageValueafterFixMetricsCount重新包括了missingPods数量。
  3. 如果有missingPods且ratio大于等于1(扩容或不扩不缩),则missingPods的监控数据修复为0,afterFixMetricsCount重新包括了missingPods数量。如果有unreadyPods,则unreadyPods的监控数据修复为0。afterFixMetricsCount重新包括了unreadyPods数量。
  4. 如果没有missingPods且ratio大于1+tolerance(扩容)且存在unreadyPods,则unreadyPods的监控数据修复为0,afterFixMetricsCount重新包括了missingPods数量
  5. 如果没有missingPods且ratio大于1+tolerance(扩容)且不存在unreadyPods,则desiredReplicasratio*readyPodCount向上取整
  6. 如果没有missingPods且ratio在[1-tolerance, 1+tolerance],则不进行扩缩容desiredReplicas为spec.replicas
  7. 如果没有missingPods且ratio小于1-tolerance,则进行缩容desiredReplicasratio*readyPodCount向上取整
  8. 重新计算新的ratio,newRatio=afterFixMetricsTotal / (afterFixMetricsCount * spec.metrics[*].containerResource.target.averageValue
  9. 如果新的ratio在[1-tolerance, 1+tolerance],则不进行扩缩容desiredReplicas为spec.replicas
  10. 如果新的ratio大于1+tolerance,且修复前的ratio小于1-tolerance,即修复之前是缩容且修复之后是扩容,则不进行扩缩容desiredReplicas为spec.replicas
  11. 如果新的ratio小于1-tolerance,且修复前的ratio大于1+tolerance,即修复之前是扩容且修复之后是缩容,则不进行扩缩容desiredReplicas为spec.replicas
  12. 计算新的副本数ceil( afterFixMetricsTotal / spec.metrics[*].containerResource.target.averageValue)
  13. 如果新的ratio大于1+tolerance,且修复前的ratio大于1+tolerance,即修复之前是扩容且修复之后是扩容,且新的副本数小于spec.replicas,则不进行扩缩容desiredReplicas为spec.replicas
  14. 如果新的ratio小于1-tolerance,且修复前的ratio小于1-tolerance,即修复之前是缩容且修复之后是缩容,且新的副本数大于spec.replicas,则不进行扩缩容desiredReplicas为spec.replicas
  15. 剩余的情况,desiredReplicasceil( afterFixMetricsTotal / spec.metrics[*].containerResource.target.averageValue)向上取整

hpa-ContainerResource-AverageValue

这个跟“Resource类型的数据源且type为AverageUtilization”计算流程类似。

这里的totalRequest为pod里的container(在spec.metrics[*].containerResource.container)的资源的request

readyMissingPodMetricsCount为所有pod metrics中移除ignoredPodsunreadyPods后的metrics数量。

afterFixMetricsCount为数据修复之后pod的metric数量

  1. 计算ratio= metricsTotal * 100 / (requestTotal * spec.metrics[*].containerResource.target.averageUtilization)
  2. 如果有missingPods且ratio小于1(缩容),则missingPods的监控数据修复为spec.metrics[*].containerResource.target.averageUtilizationafterFixMetricsCount重新包括了missingPods数量。
  3. 如果有missingPods且ratio大于等于1(扩容或不扩不缩),则missingPods的监控数据修复为0,afterFixMetricsCount重新包括了missingPods数量。如果有unreadyPods,则unreadyPods的监控数据修复为0。afterFixMetricsCount重新包括了unreadyPods数量。
  4. 如果没有missingPods且ratio大于1+tolerance(扩容)且存在unreadyPods,则unreadyPods的监控数据修复为0,afterFixMetricsCount重新包括了missingPods数量
  5. 如果没有missingPods且ratio大于1+tolerance(扩容)且不存在unreadyPods,则desiredReplicasratio*readyPodCount向上取整
  6. 如果没有missingPods且ratio在[1-tolerance, 1+tolerance],则不进行扩缩容desiredReplicas为spec.replicas
  7. 如果没有missingPods且ratio小于1-tolerance,则进行缩容desiredReplicasratio*readyPodCount向上取整
  8. 重新计算新的ratio,newRatio=afterFixMetricsTotal * 100 / (requestTotal * spec.metrics[*].containerResource.target.averageUtilization
  9. 如果新的ratio在[1-tolerance, 1+tolerance],则不进行扩缩容desiredReplicas为spec.replicas
  10. 如果新的ratio大于1+tolerance,且修复前的ratio小于1-tolerance,即修复之前是缩容且修复之后是扩容,则不进行扩缩容desiredReplicas为spec.replicas
  11. 如果新的ratio小于1-tolerance,且修复前的ratio大于1+tolerance,即修复之前是扩容且修复之后是缩容,则不进行扩缩容desiredReplicas为spec.replicas
  12. 计算新的副本数ceil( afterFixMetricsTotal * newRatio)
  13. 如果新的ratio大于1+tolerance,且修复前的ratio大于1+tolerance,即修复之前是扩容且修复之后是扩容,且新的副本数小于spec.replicas,则不进行扩缩容desiredReplicas为spec.replicas
  14. 如果新的ratio小于1-tolerance,且修复前的ratio小于1-tolerance,即修复之前是缩容且修复之后是缩容,且新的副本数大于spec.replicas,则不进行扩缩容desiredReplicas为spec.replicas
  15. 剩余的情况,desiredReplicasceil( afterFixMetricsCount * newRatio)向上取整

hpa-ContainerResource-AverageUtilization

在上面的流程执行完会得到一个期望的副本数,但是这个副本数并不是HPA controller最终计算的副本数,它还需要经过扩缩容行为控制策略处理才能得到最终的副本数。

扩缩容行为策略是对控制扩缩容的速度进行限制,防止过快扩容和过快的缩容导致的不稳定的行为。

扩缩容行为控制分为两种,HPA对象里未设置了spec.behavior(默认扩缩容行为)和设置spec.behavior。

downscaleStabilisationWindow:为–horizontal-pod-autoscaler-downscale-stabilization的值,默认为5分钟

  1. 将上面的流程执行的副本数和执行的时间记录到内存里
  2. 从内存中查找downscaleStabilisationWindow窗口内最大的副本数stabilizedRecommendation
  3. 这个窗口内的最大扩容上限scaleUpLimitmax(2*spec.replicas, 4)
  4. stabilizedRecommendation进行规整(大于上限等于上限,小于下限就等于下限),得出最终的副本数。上限为min(scaleUpLimit, hpa.Spec.MaxReplicas),下限为minReplicas(默认为hpa.Spec.minReplicas,当hpa.Spec.minReplicas没有设置时候为1),即确保期望副本数在[minReplicas, min(max(2*spec.replicas, 4), hpa.Spec.maxReplicas)]

在每次HPA controller进行扩缩容时侯,会将HPA对应的workload的副本数的变化量和扩缩容时间记录到内存中

  1. 将上面流程desiredReplicas和执行时间记录到内存里

  2. hpa.spec.behavior.scaleUp.stabilizationWindowSeconds窗口中(包括desiredReplicas)查找最小副本数upRecommendation

  3. hpa.spec.behavior.scaleDown.stabilizationWindowSeconds窗口中(包括desiredReplicas)查找最大副本数downRecommendation

  4. spec.replicas进行规整得到稳定窗口得副本数stabilizedRecommendation,即如果spec.replicas大于downRecommendation,则stabilizedRecommendationdownRecommendation。如果spec.replicas小于upRecommendation,则stabilizedRecommendationupRecommendation。即stabilizedRecommendation[upRecommendation, downRecommendation ],只有spec.replicas小于hpa.spec.behavior.scaleUp.stabilizationWindowSeconds窗口中的最小值才有可能扩容,只有spec.replicas大于hpa.spec.behavior.scaleDown.stabilizationWindowSeconds窗口中的最大值才有可能缩容。

  5. 在需要扩容情况下(stabilizedRecommendation大于spec.replicas

    • hpa.spec.behavior.scaleUp.selectPolicy为Disabled,则不进行扩缩容最终副本数为spec.replicas

    • hpa.spec.behavior.scaleUp.selectPolicy为Max,遍历hpa.spec.behavior.scaleUp.policies执行下面:

      • 从内存中查找在policy.periodSeconds这个策略窗口内的累计副本数变化量replicasAddedInCurrentPeriod,窗口开始时候的副本数periodStartReplicas=spec.replicas - replicasAddedInCurrentPeriod

      • 这个策略类型policy.Type为"Pods",则这个策略窗口内的副本数上限policyLimitperiodStartReplicas + policy.Value

      • 这个策略类型policy.Type为"Percent",则这个策略窗口里的副本数上限policyLimitCeil(periodStartReplicas * (1 + policy.Value/100))向上取整

        这个窗口内的最大扩容上限scaleUpLimit为所有策略里最大的副本数上限,即scaleUpLimit=max(policyLimit1, policyLimit2....)

    • hpa.spec.behavior.scaleUp.selectPolicy为Min,遍历hpa.spec.behavior.scaleUp.policies执行下面:

      • 从内存中查找在policy.periodSeconds这个策略窗口内的累计副本数变化量replicasAddedInCurrentPeriod,窗口开始时候的副本数periodStartReplicas=spec.replicas - replicasAddedInCurrentPeriod

      • 这个策略类型policy.Type为"Pods",则这个策略窗口内的副本数上限policyLimitperiodStartReplicas + policy.Value

      • 这个策略类型policy.Type为"Percent",则这个策略窗口里的副本数上限policyLimitCeil(periodStartReplicas * (1 - policy.Value/100))向上取整

        这个窗口内的最大扩容上限scaleUpLimit为所有策略里最小的副本数上限,即scaleUpLimit=min(policyLimit1, policyLimit2....)

    • 最终的副本数为min(stabilizedRecommendation, min(scaleUpLimit, hpa.Spec.maxReplicas))

  6. 在需要缩容情况下(stabilizedRecommendation大于spec.replicas

    • hpa.spec.behavior.scaleDown.selectPolicy为Disabled,则不进行扩缩容最终副本数为spec.replicas

    • hpa.spec.behavior.scaleDown.selectPolicy为Max,遍历hpa.spec.behavior.scaleUp.policies执行下面:

      • 从内存中查找在policy.periodSeconds这个策略窗口内的累计副本数变化量replicasAddedInCurrentPeriod,窗口开始时候的副本数periodStartReplicas=spec.replicas + replicasAddedInCurrentPeriod

      • 这个策略类型policy.Type为"Pods",则这个策略窗口内的副本数下限policyLimitperiodStartReplicas - policy.Value

      • 这个策略类型policy.Type为"Percent",则这个策略窗口里的副本数下限policyLimitCeil(periodStartReplicas * (1 - policy.Value/100))向上取整

        这个窗口内的最大扩容上限scaleUpLimit为所有策略里最大副本数上限,即scaleUpLimit=max(policyLimit1, policyLimit2....)

    • hpa.spec.behavior.scaleDown.selectPolicy为Min,遍历hpa.spec.behavior.scaleDown.policies执行下面:

      • 从内存中查找在policy.periodSeconds这个策略窗口内的累计副本数变化量replicasAddedInCurrentPeriod,窗口开始时候的副本数periodStartReplicas=spec.replicas + replicasAddedInCurrentPeriod

      • 这个策略类型policy.Type为"Pods",则这个策略窗口内的副本数下限policyLimitperiodStartReplicas - policy.Value

      • 这个策略类型policy.Type为"Percent",则这个策略窗口里的副本数下限policyLimitCeil(periodStartReplicas * (1 - policy.Value/100))向上取整

        这个窗口内的最大扩容上限scaleUpLimit为所有策略里最大副本数上限,即scaleUpLimit=min(policyLimit1, policyLimit2....)

    • 最终的副本数为max(stabilizedRecommendation, max(scaleUpLimit, hpa.Spec.maxReplicas))

扩容慢包含三个方面,一个是扩容的响应时间,另一个是每次扩容的副本数即扩容速度,还有扩容的敏感度。

由于metrics-server是周期性的收集kubelet上的监控信息,这个周期默认是15s。而kubelet里的cadvisor周期性的收集pod相关的监控信息,这个周期是30s。HPA controller每15秒执行一次HPA对象的workload副本数计算。

所以resourcecontainerResource数据源类型的扩容延迟的时间在[0, 60s],即延迟最大为60s。

而其他类型的扩容延迟时间,还受监控组件(比如Prometheus、VictoriaMetrics)收集监控信息周期影响,所以延迟为[0, 15+监控收集周期]

了解了HPA controller计算副本数的流程,最终的副本数由监控数据和扩缩容行为控制决定。分析一下开头的测试结果,先看压测开始后第一次HPA资源的变化时候的status字段,这里给出了当前的监控的averageUtilization为2575,averageValue为515m。

根据这个数据我们看一下期望的副本数为258 = ceil(spec.replicas * averageValue * 100 / request * target.averageUtilization) = ceil(2 * 515 * 100/20 * 20)

由于hpa.spec.maxReplicas为10,会将10保存到内存中。然后没有配置hpa.spec.behavior,所以这个窗口的扩容上限为4=max(2 * spec.replicas, 4)= max(4, 4),所以第一次最终的副本数为4。

 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
status:
  conditions:
  - lastTransitionTime: "2023-11-02T03:27:06Z"
    message: the HPA controller was able to update the target scale to 4
    reason: SucceededRescale
    status: "True"
    type: AbleToScale
  - lastTransitionTime: "2023-11-02T03:37:07Z"
    message: the HPA was able to successfully calculate a replica count from cpu resource
      utilization (percentage of request)
    reason: ValidMetricFound
    status: "True"
    type: ScalingActive
  - lastTransitionTime: "2023-11-02T05:01:38Z"
    message: the desired replica count is increasing faster than the maximum scale
      rate
    reason: ScaleUpLimit
    status: "True"
    type: ScalingLimited
  currentMetrics:
  - resource:
      current:
        averageUtilization: 2575
        averageValue: 515m
      name: cpu
    type: Resource
  currentReplicas: 2
  desiredReplicas: 4
  lastScaleTime: "2023-11-02T05:10:26Z"

第二次HPA资源变化的status字段里的监控数据averageUtilizationaverageValue都为0,由于最小副本数为1(没有配置hpa.spec.minReplicas,默认为0),所以期望副本数为1,然后保存到内存中。由于这个窗口内内存里最大的副本数为10,所以还是会进行扩容。所以最终副本数为8=max(2 * spec.replicas, 4)= max(8, 4)。后面第三次扩容类似,这里就不分析了。

 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
status:
  conditions:
  - lastTransitionTime: "2023-11-02T03:27:06Z"
    message: the HPA controller was able to update the target scale to 8
    reason: SucceededRescale
    status: "True"
    type: AbleToScale
  - lastTransitionTime: "2023-11-02T03:37:07Z"
    message: the HPA was able to successfully calculate a replica count from cpu resource
      utilization (percentage of request)
    reason: ValidMetricFound
    status: "True"
    type: ScalingActive
  - lastTransitionTime: "2023-11-02T05:01:38Z"
    message: the desired replica count is increasing faster than the maximum scale
      rate
    reason: ScaleUpLimit
    status: "True"
    type: ScalingLimited
  currentMetrics:
  - resource:
      current:
        averageUtilization: 0
        averageValue: "0"
      name: cpu
    type: Resource
  currentReplicas: 4
  desiredReplicas: 8
  lastScaleTime: "2023-11-02T05:10:41Z"

--horizontal-pod-autoscaler-tolerance参数决定了扩缩容时可以容忍的抖动范围。该参数旨在防止因监控数据波动而引发的意外扩缩容行为,但同时也可能导致扩缩容的敏感度降低。默认值为0.1,意味着可容忍10%的监控数据变化。

比如上面的例子里,只有pod的CPU平均使用率达到request的22%时候才会进行扩容。

在1.23版本中HPA controller只启动一个goroutine来处理集群中所有HPA资源,在大量的HPA对象的集群中会有性能瓶颈,所以在1.26版本中增加--concurrent-horizontal-pod-autoscaler-syncs命令行选项支持配置goroutine数量PR#108501

由于存在unready pod监控数据修复问题(扩容时候数据修复为0),所以扩容的速度会变慢。

pod ready需要的时间越短扩缩容速度越快,而pod从启动到ready时间,取决于pod调度、kubelet响应pod调度完成、镜像下载、容器创建、应用启动、应用readiness。

Constituents-of-lag-in-autoscaling

图片来自:https://medium.com/expedia-group-tech/autoscaling-in-kubernetes-why-doesnt-the-horizontal-pod-autoscaler-work-for-me-5f0094694054

解决思路:

  1. 缩短获取监控的链路长度
  2. 缩短pod ready时间(pod生成到pod ready每个环节都进行优化)
  3. HPA controller性能提升
  4. 设置合理的扩容行为策略
  5. 设置合理的容忍度
  6. 预测式扩缩容或定时扩容

除了kubernetes自身的HPA控制器进行水平扩缩容,还有KnativeKEDA项目也是进行水平扩缩容。

既然监控数据获取的链路过长导致扩缩容响应时间长,那么Knative就从简化链路长度来解决扩缩响应问题,支持基于qps和tps扩容,做到秒级弹性。

根据KEDA文档它的角色是取代Prometheus-adaptor,提供external和custom metrics,即依然使用HPA机制,并没有解决链路长问题。而是支持事件驱动然扩缩容更灵敏,这个knative也有这个功能。

这个话题就比较大了,包括镜像加速(dragonfly p2p,预拉取、containerd nydus),容器运行时(crunpodmancri-o),调度器性能优化,拓扑感知。

使用1.26以上版本增加goroutine,当然不差钱的,有多好配置上多好配置。

如果不设置behavior字段,每次扩容的数量受限于downscaleStabilisationWindow窗口内最大的副本数和当前的副本数spec.replicas(最大扩容上限scaleUpLimitmax(2*spec.replicas, 4))。

所以要在不设置behavior字段的情况下提高扩容速度,基本不可能。因为downscaleStabilisationWindow决定窗口内最大副本数的持续时间,它的作用的是防止突高随后突降导致副本数不稳定,而spec.replicas在HPA对象的调谐周期内是固定的。

既然behavior字段的配置影响扩缩容,那么合理的配置behavior字段能够提高扩缩容速度。

  1. 增加扩容速度: 减小hpa.spec.behavior.scaleUp.stabilizationWindowSeconds(如果没有设置默认值为0)和增加hpa.spec.behavior.scaleUp.policies[*].Value和减小hpa.spec.behavior.scaleUp.policies[*].PeriodSeconds
  2. 增加缩容速度:减小hpa.spec.behavior.scaleDown.stabilizationWindowSeconds(如果没有设置默认值为–horizontal-pod-autoscaler-downscale-stabilization值)和增加hpa.spec.behavior.scaleDown.policies[*].Value和减小hpa.spec.behavior.scaleDown.policies[*].PeriodSeconds

设置合理的--horizontal-pod-autoscaler-tolerance,它是一把双刃剑,如果调整不合理容易频繁的扩缩容行为。

换个思路,既然扩容慢不如提前扩容,像阿里云的AHPA,蚂蚁金服的kapacity

基于历史流量进行预测式扩容,比如crane的EHPA,蚂蚁金服的kapacity,阿里云的AHPA

由于监控数据获取链路过长和HPA controller性能问题导致的扩容延迟,而应用的ready时间长短和hpa.spec.behavior会影响扩容速度。

缩短pod ready时间,需要在pod生成到pod ready的每个环节进行优化。

knative从简化监控获取链路角度来解决扩容延迟问题,提前扩容角度解决问题有crane的EHPA和蚂蚁金服的kapacity。

我们如何构建生产级HPA:从有效算法到无风险的自动扩展 | How We Build Production-Grade HPA: From Effective Algorithm to Risk-Free Autoscaling - Ziqiu Zhu & Yiru Guo, Ant Group

Autoscaling in Kubernetes: Why doesn’t the Horizontal Pod Autoscaler work for me?

Kubernetes 1.27: updates on speeding up Pod startup

相关内容