05-Kube-Controller-Manager源码分析(HPA控制器)

本文基于1.29.0版本

HPA 控制器源码分析

简介

Horizontal Pod Autoscaler (HPA) 是 Kubernetes 中实现应用水平自动扩缩容的核心控制器。它通过监控指定的指标(如 CPU 利用率、内存使用量或自定义指标),自动调整 Deployment、ReplicaSet 或 StatefulSet 等工作负载的副本数,以应对流量波动和资源需求变化。

本文将深入分析 HPA 控制器的源码,探究其工作原理和实现细节。

入口点

与许多其他控制器一样,HPA 控制器在 kube-controller-manager 进程中运行。其初始化入口位于 cmd/kube-controller-manager/app/autoscaling.go 文件中。

1
2
3
4
5
6
7
8
// cmd/kube-controller-manager/app/autoscaling.go

func newHorizontalPodAutoscalerControllerDescriptor() *controllerDescriptor {
	return &controllerDescriptor{
		name: "horizontal-pod-autoscaler",
		init: startHorizontalPodAutoscalerControllerWithRESTClient,
	}
}

newHorizontalPodAutoscalerControllerDescriptor 函数定义了 HPA 控制器的描述符,其中 name 字段指定了控制器名称,init 字段指向了初始化函数 startHorizontalPodAutoscalerControllerWithRESTClient

控制器初始化

startHorizontalPodAutoscalerControllerWithRESTClient 函数是 HPA 控制器初始化的起点。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// cmd/kube-controller-manager/app/autoscaling.go

func startHorizontalPodAutoscalerControllerWithRESTClient(ctx context.Context, controllerContext ControllerContext) (controller.Interface, bool, error) {
	// ...
	return startHPAControllerWithMetricsClient(
		ctx,
		controllerContext,
		controllerContext.RESTMapper,
		controllerContext.ClientBuilder.ClientOrDie("horizontal-pod-autoscaler").Discovery(),
		scale.New(
			controllerContext.ClientBuilder.ClientOrDie("horizontal-pod-autoscaler").Discovery(),
			controllerContext.RESTMapper,
			dynamic.LegacyAPIPathResolverFunc,
			scale.NewDiscoveryScaleKindResolver(
				controllerContext.ClientBuilder.ClientOrDie("horizontal-pod-autoscaler").Discovery(),
			),
		),
	)
}

此函数主要负责准备 HPA 控制器所需的依赖,例如 RESTMapperDiscoveryClientScaleClient,然后调用 startHPAControllerWithMetricsClient

startHPAControllerWithMetricsClient 函数进一步构建了用于获取指标的客户端,并最终创建和启动了 HorizontalController

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// cmd/kube-controller-manager/app/autoscaling.go

func startHPAControllerWithMetricsClient(
	ctx context.Context,
	controllerContext ControllerContext,
	restMapper meta.RESTMapper,
	discoveryClient discovery.DiscoveryInterface,
	scaleClient scale.ScalesGetter,
) (controller.Interface, bool, error) {
	// ...
	metricsClient := metrics.NewRESTMetricsClient(
		resourceclient.NewForConfigOrDie(controllerContext.ClientBuilder.ConfigOrDie("resource-metrics-client")),
		customclient.NewForConfig(controllerContext.ClientBuilder.ConfigOrDie("custom-metrics-client")),
		externalclient.NewForConfigOrDie(controllerContext.ClientBuilder.ConfigOrDie("external-metrics-client")),
	)

	go podautoscaler.NewHorizontalController(
		// ...
	).Run(ctx, controllerContext.ComponentConfig.HPA.ConcurrentHorizontalPodAutoscalerSyncs)

	return nil, true, nil
}

在这里,NewHorizontalController 函数(位于 pkg/controller/podautoscaler/horizontal.go)被调用以创建 HorizontalController 的实例。然后,Run 方法在一个新的 Goroutine 中被调用,启动了控制器的核心循环。

NewHorizontalController 函数初始化了 HorizontalController 结构体,其中包含了 HPA 控制器运行所需的所有组件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// pkg/controller/podautoscaler/horizontal.go

type HorizontalController struct {
	scaleNamespacer scale.ScalesGetter
	hpaNamespacer   autoscalingv1.HorizontalPodAutoscalersGetter
	mapper          meta.RESTMapper
	replicaCalc     *ReplicaCalculator
	eventRecorder   record.EventRecorder
	// ...
	downscaleStabilisationWindow time.Duration

	// ...
	hpaLister       autoscalinglisters.HorizontalPodAutoscalerLister
	podLister       corelisters.PodLister
	// ...
	queue workqueue.RateLimitingInterface

	// ...
	recommendations map[string][]timestampedRecommendation
	scaleEvents     map[string][]timestampedScaleEvent
}
  • scaleNamespacer: 用于与 scale 子资源交互的客户端。
  • hpaNamespacer: 用于与 HPA 对象交互的客户端。
  • mapper: 用于在不同 API 版本和资源类型之间进行映射。
  • replicaCalc: 用于计算期望副本数。
  • eventRecorder: 用于记录事件。
  • downscaleStabilisationWindow: 缩容稳定窗口,用于防止频繁缩容。
  • hpaLister, podLister: 用于从本地缓存中获取 HPA 和 Pod 对象。
  • queue: 工作队列,用于存放需要处理的 HPA 对象。
  • recommendations, scaleEvents: 用于存储扩缩容建议和事件,以实现稳定化。

NewHorizontalController 还设置了事件处理器,用于监听 HPA 对象的创建、更新和删除事件,并将相关的 HPA 对象加入到工作队列中。

核心协调循环

HPA 控制器的核心协调循环由 Run 方法和 worker 函数驱动。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// pkg/controller/podautoscaler/horizontal.go

func (a *HorizontalController) Run(ctx context.Context, workers int) {
	// ...
	for i := 0; i < workers; i++ {
		go wait.UntilWithContext(ctx, a.worker, time.Second)
	}
	// ...
}

func (a *HorizontalController) worker(ctx context.Context) {
	for a.processNextWorkItem(ctx) {
	}
}

Run 方法启动了多个 worker Goroutine。每个 worker 都在一个无限循环中调用 processNextWorkItem 函数。

processNextWorkItem 函数从工作队列中取出一个 HPA 对象的 key(格式为 namespace/name),然后调用 reconcileKey 函数来处理它。

1
2
3
4
5
6
7
8
9
// pkg/controller/podautoscaler/horizontal.go

func (a *HorizontalController) processNextWorkItem(ctx context.Context) bool {
	key, quit := a.queue.Get()
	// ...
	err := a.reconcileKey(ctx, key.(string))
	// ...
	return true
}

reconcileKey 函数是协调过程的入口。它首先从 hpaLister 中获取 HPA 对象。如果对象不存在(可能已被删除),它会清理与该 HPA 相关的数据。如果对象存在,它会调用 reconcileAutoscaler 函数来执行主要的扩缩容逻辑。

1
2
3
4
5
6
7
8
// pkg/controller/podautoscaler/horizontal.go

func (a *HorizontalController) reconcileKey(ctx context.Context, key string) error {
	// ...
	hpa, err := a.hpaLister.HorizontalPodAutoscalers(namespace).Get(name)
	// ...
	return a.reconcileAutoscaler(ctx, hpa, key)
}

核心扩缩容逻辑

reconcileAutoscaler 函数是 HPA 控制器的心脏,它实现了完整的扩缩容逻辑。

1
2
3
4
5
// pkg/controller/podautoscaler/horizontal.go

func (a *HorizontalController) reconcileAutoscaler(ctx context.Context, hpa *autoscalingv2.HorizontalPodAutoscaler, key string) error {
	// ...
}

其工作流程可以通过以下流程图来概括:

graph TD
    A[reconcileAutoscaler] --> B{获取 Scale 子资源};
    B --> C[computeReplicasForMetrics];
    C --> D{遍历所有 hpa.Spec.Metrics};
    D --> E[replicaCalc.GetMetricReplicas];
    E --> F{指标类型?};
    F -- Resource --> G1[GetResourceReplicas];
    F -- Pods --> G2[GetPodsMetricReplicas];
    F -- Object --> G3[GetObjectMetricReplicas];
    F -- External --> G4[GetExternalMetricReplicas];
    F -- ContainerResource --> G5[GetContainerResourceReplicas];
    G1 --> H["计算期望副本数 = ceil(当前副本数 * (当前指标值 / 目标指标值))"];
    G2 --> H;
    G3 --> H;
    G4 --> H;
    G5 --> H;
    H --> I[返回所有指标计算结果的最大值];
    I --> J{hpa.Spec.Behavior 是否定义?};

    J -- 是 --> K_subgraph;
    subgraph K_subgraph [normalizeDesiredReplicasWithBehaviors]
        K1[stabilizeRecommendationWithBehaviors
稳定化推荐值] K2["convertDesiredReplicasWithBehaviorRate
限制变更速率"]; K1 --> K2 end J -- 否 --> L_subgraph; subgraph L_subgraph [normalizeDesiredReplicas] L1[stabilizeRecommendation] --> L2[convertDesiredReplicasWithRules]; end K_subgraph --> O{最终期望副本数}; L_subgraph --> O; O --> P{是否需要扩缩容?}; P -- 是 --> Q[更新 Scale 子资源]; P -- 否 --> R[跳过]; Q --> S[更新 HPA Status]; R --> S;
graph TD
    A[reconcileAutoscaler] --> B{获取 Scale 子资源};
    B --> C[computeReplicasForMetrics];
    C --> D{遍历所有 hpa.Spec.Metrics};
    D --> E[replicaCalc.GetMetricReplicas];
    E --> F{指标类型?};
    F -- Resource --> G1[GetResourceReplicas];
    F -- Pods --> G2[GetPodsMetricReplicas];
    F -- Object --> G3[GetObjectMetricReplicas];
    F -- External --> G4[GetExternalMetricReplicas];
    F -- ContainerResource --> G5[GetContainerResourceReplicas];
    G1 --> H["计算期望副本数 = ceil(当前副本数 * (当前指标值 / 目标指标值))"];
    G2 --> H;
    G3 --> H;
    G4 --> H;
    G5 --> H;
    H --> I[返回所有指标计算结果的最大值];
    I --> J{hpa.Spec.Behavior 是否定义?};

    J -- 是 --> K_subgraph;
    subgraph K_subgraph [normalizeDesiredReplicasWithBehaviors]
        K1[stabilizeRecommendationWithBehaviors
稳定化推荐值] K2["convertDesiredReplicasWithBehaviorRate
限制变更速率"]; K1 --> K2 end J -- 否 --> L_subgraph; subgraph L_subgraph [normalizeDesiredReplicas] L1[stabilizeRecommendation] --> L2[convertDesiredReplicasWithRules]; end K_subgraph --> O{最终期望副本数}; L_subgraph --> O; O --> P{是否需要扩缩容?}; P -- 是 --> Q[更新 Scale 子资源]; P -- 否 --> R[跳过]; Q --> S[更新 HPA Status]; R --> S;
graph TD
    A[reconcileAutoscaler] --> B{获取 Scale 子资源};
    B --> C[computeReplicasForMetrics];
    C --> D{遍历所有 hpa.Spec.Metrics};
    D --> E[replicaCalc.GetMetricReplicas];
    E --> F{指标类型?};
    F -- Resource --> G1[GetResourceReplicas];
    F -- Pods --> G2[GetPodsMetricReplicas];
    F -- Object --> G3[GetObjectMetricReplicas];
    F -- External --> G4[GetExternalMetricReplicas];
    F -- ContainerResource --> G5[GetContainerResourceReplicas];
    G1 --> H["计算期望副本数 = ceil(当前副本数 * (当前指标值 / 目标指标值))"];
    G2 --> H;
    G3 --> H;
    G4 --> H;
    G5 --> H;
    H --> I[返回所有指标计算结果的最大值];
    I --> J{hpa.Spec.Behavior 是否定义?};

    J -- 是 --> K_subgraph;
    subgraph K_subgraph [normalizeDesiredReplicasWithBehaviors]
        K1[stabilizeRecommendationWithBehaviors
稳定化推荐值] K2["convertDesiredReplicasWithBehaviorRate
限制变更速率"]; K1 --> K2 end J -- 否 --> L_subgraph; subgraph L_subgraph [normalizeDesiredReplicas] L1[stabilizeRecommendation] --> L2[convertDesiredReplicasWithRules]; end K_subgraph --> O{最终期望副本数}; L_subgraph --> O; O --> P{是否需要扩缩容?}; P -- 是 --> Q[更新 Scale 子资源]; P -- 否 --> R[跳过]; Q --> S[更新 HPA Status]; R --> S;

下面我们来详细解析其中的关键步骤。

1. 获取 Scale 子资源

  • 控制器首先解析 HPA 的 scaleTargetRef,以确定要扩缩容的目标资源(例如 Deployment、ReplicaSet)。
  • 然后,它使用 scaleNamespacer 客户端获取该目标资源的 scale 子资源。scale 子资源包含了当前的副本数(spec.replicasstatus.replicas)。

2. 计算期望副本数 (computeReplicasForMetrics)

computeReplicasForMetrics 函数负责根据 HPA 规范中定义的所有指标计算最终的期望副本数。它的核心逻辑是:

  1. 遍历所有指标:函数会迭代处理 hpa.Spec.Metrics 中定义的每一个指标。

  2. 调用 replicaCalc.GetMetricReplicas:对于每个指标,它都会调用 replicaCalc.GetMetricReplicas 来计算一个独立的期望副本数。

  3. 分派到具体实现GetMetricReplicas 内部会根据指标的 TypeResourcePodsObjectExternalContainerResource)将计算任务分派给更具体的函数:

    • GetResourceReplicas:处理 Resource 指标,如 CPU 和内存。
    • GetPodsMetricReplicas:处理 Pods 指标。
    • GetObjectMetricReplicas:处理 Object 指标。
    • GetExternalMetricReplicas:处理 External 指标。
    • GetContainerResourceReplicas:处理 ContainerResource 指标。
  4. 获取指标并计算:这些具体的函数会通过 metricsClientmetrics-server 或其他指标源获取指标的当前值,然后根据目标值计算期望副本数。其通用计算公式为:

    期望副本数 = ceil(当前副本数 * (当前指标值 / 目标指标值))

  5. 选择最大值computeReplicasForMetrics 会在所有指标计算出的期望副本数中,选择最大值作为最终的计算结果。这是为了确保能够满足所有指标的资源需求。

3. 稳定期望副本数

计算出的期望副本数并不会立即被采用,而是会经过一个稳定化过程,以避免因指标的短暂波动而导致过于频繁的扩缩容(抖动)。

behavior 的稳定化 (normalizeDesiredReplicasWithBehaviors)

如果 HPA 规范中定义了 behavior 字段,控制器会调用 normalizeDesiredReplicasWithBehaviors 函数。此函数提供了更精细的扩缩容速率控制。其底层实现分为两个核心步骤:

  1. 稳定化推荐值 (stabilizeRecommendationWithBehaviors)

    • 此函数负责处理 scaleUpscaleDown 行为中定义的 stabilizationWindowSeconds,以平滑期望副本数的变化。
    • 扩容基准计算:当考虑扩容时,它会回顾 scaleUp.stabilizationWindowSeconds 时间窗口内的所有历史推荐值。然后,它会选择这些历史值与当前计算出的期望值中的最小值作为扩容基准。这种保守策略可以有效防止因指标短暂飙升而导致的过度扩容。
    • 缩容基准计算:当考虑缩容时,它会回顾 scaleDown.stabilizationWindowSeconds 时间窗口内的所有历史推荐值。然后,它会选择这些历史值与当前计算出的期望值中的最大值作为缩容基准。这可以防止因指标短暂下跌而导致的过度缩容,确保服务的稳定性。
  2. 限制变更速率 (convertDesiredReplicasWithBehaviorRate)

    • 此函数负责处理 behavior 中定义的 policies,以限制单位时间内的副本数变更速率。

    • 核心思想:计算出基于速率策略的扩容上限和缩容下限,然后将稳定化后的期望副本数限制在此范围内。

      • 最终副本数 = min(期望副本数, 扩容上限) (扩容时)
      • 最终副本数 = max(期望副本数, 缩容下限) (缩容时)
    • “期望副本数” vs “提议副本数”

      • 期望副本数 (Desired Replicas):这是 HPA 根据当前指标利用率计算出的"理想"副本数,旨在使资源利用率趋近目标值。它是扩缩容的最终目标
      • 提议副本数 (Proposed Replicas):这是在速率限制环节中,根据 behavior每一条策略单独计算出的"建议副本数"。它是用于计算最终速率限制的中间值
      • 关系:HPA 首先计算出 “期望副本数”。然后,通过计算多个 “提议副本数” 并根据 selectPolicy 从中选择,得出最终的 “扩容上限” 或 “缩容下限”。最后,用这个限制去约束"期望副本数",从而得到最终要执行的副本数量。简单来说,“期望副本数"是目标,而"提议副本数"是计算速率限制的工具。
    • 扩容上限计算:它会遍历所有 scaleUp 策略(policies),并为每个策略计算出允许增加的最大副本数。在所有策略计算出的允许值中,它会选择最激进(允许增加最多副本数)的一个作为最终的扩容上限。

    • 缩容下限计算:它会遍历所有 scaleDown 策略,并为每个策略计算出允许减少的最大副本数。在所有策略计算出的允许值中,它会选择最保守(允许减少最少副本数)的一个作为最终的缩容下限。

    计算公式:

    为了更清晰地理解速率限制,我们可以将其计算逻辑简化为以下公式:

    • 扩容 (scaleUp)

      • 提议副本数 (固定值) = 周期初副本数 + 策略值
      • 提议副本数 (百分比) = ceil(周期初副本数 * (1 + 策略值 / 100))
      • 扩容上限 = max(所有 scaleUp 策略计算出的提议副本数) (因为 selectPolicy 默认为 Max,选择最激进的策略)
      • 最终扩容上限 = min(HPA.maxReplicas, 扩容上限)
    • 缩容 (scaleDown)

      • 提议副本数 (固定值) = 周期初副本数 - 策略值
      • 提议副本数 (百分比) = floor(周期初副本数 * (1 - 策略值 / 100))
      • 缩容下限 = max(所有 scaleDown 策略计算出的提议副本数) (因为 selectPolicy 默认为 Max,选择最保守的策略,即副本数最多的那个)
      • 最终缩容下限 = max(HPA.minReplicas, 缩容下限)

    代码实现:

    convertDesiredReplicasWithBehaviorRate 函数是速率限制的入口,它根据扩容或缩容场景,分别调用 calculateScaleUpLimitWithScalingRulescalculateScaleDownLimitWithBehaviors

     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
    
    // pkg/controller/podautoscaler/horizontal.go
    
    func (a *HorizontalController) convertDesiredReplicasWithBehaviorRate(ctx context.Context, args *conversionArgs) (int32, string, string) {
        // ...
    
        if args.DesiredReplicas > args.CurrentReplicas {
            scaleUpLimit := calculateScaleUpLimitWithScalingRules(args.CurrentReplicas, a.scaleUpEvents[args.Key], a.scaleDownEvents[args.Key], args.Hpa.Spec.Behavior.ScaleUp)
            maximumAllowedReplicas := min(args.Hpa.Spec.MaxReplicas, scaleUpLimit)
    
            if args.DesiredReplicas > maximumAllowedReplicas {
                // ...
                return maximumAllowedReplicas, "ScaleUpLimit", "the desired replica count is increasing faster than the maximum scale rate"
            }
        }
    
        if args.DesiredReplicas < args.CurrentReplicas {
            scaleDownLimit := calculateScaleDownLimitWithBehaviors(args.CurrentReplicas, a.scaleUpEvents[args.Key], a.scaleDownEvents[args.Key], args.Hpa.Spec.Behavior.ScaleDown)
            minimumAllowedReplicas := max(args.Hpa.Spec.MinReplicas, scaleDownLimit)
    
            if args.DesiredReplicas < minimumAllowedReplicas {
                // ...
                return minimumAllowedReplicas, "ScaleDownLimit", "the desired replica count is decreasing faster than the maximum scale rate"
            }
        }
    
        return args.DesiredReplicas, "DesiredReplicas", "the desired replica count is within the acceptable range"
    }

    calculateScaleUpLimitWithScalingRules 函数负责计算扩容上限。它会遍历所有 scaleUp 策略,并根据 SelectPolicy(默认为 Max)选择最激进的结果。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    // pkg/controller/podautoscaler/horizontal.go
    
    func calculateScaleUpLimitWithScalingRules(currentReplicas int32, scaleUpEvents, scaleDownEvents []timestampedScaleEvent, scalingRules *autoscalingv2.HPAScalingRules) int32 {
        var result int32
        var proposed int32
        var selectPolicyFn func(int32, int32) int32
        // ...
        // 默认使用 max 函数,选择最激进的策略
        result = math.MinInt32
        selectPolicyFn = max 
    
        for _, policy := range scalingRules.Policies {
            // ... 计算周期初的副本数 periodStartReplicas ...
    
            if policy.Type == autoscalingv2.PodsScalingPolicy {
                proposed = periodStartReplicas + policy.Value
            } else if policy.Type == autoscalingv2.PercentScalingPolicy {
                proposed = int32(math.Ceil(float64(periodStartReplicas) * (1 + float64(policy.Value)/100)))
            }
            result = selectPolicyFn(result, proposed)
        }
        return result
    }

    calculateScaleDownLimitWithBehaviors 函数负责计算缩容下限。它会遍历所有 scaleDown 策略,并根据 SelectPolicy(默认为 Max,但对于缩容场景,Max 意味着选择变化最小的策略)选择最保守的结果。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
    // pkg/controller/podautoscaler/horizontal.go
    
    func calculateScaleDownLimitWithBehaviors(currentReplicas int32, scaleUpEvents, scaleDownEvents []timestampedScaleEvent, scalingRules *autoscalingv2.HPAScalingRules) int32 {
        var result int32
        var proposed int32
        var selectPolicyFn func(int32, int32) int32
        // ...
        // 默认使用 min 函数,选择最激进的策略(减少最多)
        // 但 SelectPolicy 默认为 Max,所以会使用 min 函数,选择最保守的策略
        result = math.MaxInt32
        selectPolicyFn = min
    
        for _, policy := range scalingRules.Policies {
            // ... 计算周期初的副本数 periodStartReplicas ...
    
            if policy.Type == autoscalingv2.PodsScalingPolicy {
                proposed = periodStartReplicas - policy.Value
            } else if policy.Type == autoscalingv2.PercentScalingPolicy {
                proposed = int32(float64(periodStartReplicas) * (1 - float64(policy.Value)/100))
            }
            result = selectPolicyFn(result, proposed)
        }
        return result
    }

配置策略示例

为了更具体地理解 behavior 配置如何工作,我们可以参考一个实际的 hpa.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
# hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: example-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: example-deployment
  minReplicas: 2
  maxReplicas: 10
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 0
      selectPolicy: Max
      policies:
      - type: Percent
        value: 100
        periodSeconds: 180
    scaleDown:
      stabilizationWindowSeconds: 300
      selectPolicy: Max
      policies:
      - type: Percent
        value: 100
        periodSeconds: 30
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 50

在这个示例中,我们定义了以下扩缩容行为:

  • 扩容 (scaleUp):

    • stabilizationWindowSeconds: 0: 扩容时没有稳定窗口。这意味着一旦指标超过阈值,HPA 会立即计算新的期望副本数,而不会回顾历史推荐值。
    • selectPolicy: Max: 如果有多个扩容策略,选择最激进的(允许增加最多副本数)一个。
    • policies:
      • type: Percent, value: 100, periodSeconds: 180: 在 180 秒的时间窗口内,最多允许将副本数翻倍(增加 100%)。
  • 缩容 (scaleDown):

    • stabilizationWindowSeconds: 300: 缩容前有 300 秒(5 分钟)的稳定窗口。HPA 会回顾这 5 分钟内的历史推荐值,并选择其中的最大值作为缩容基准,以防止因指标短暂下降而过快缩容。
    • selectPolicy: Max: 如果有多个缩容策略,选择最保守的(允许减少最少副本数)一个。
    • policies:
      • type: Percent, value: 100, periodSeconds: 30: 在 30 秒的时间窗口内,最多允许将副本数减少 100%(即缩容到 minReplicas)。

这个配置旨在实现“快速扩容,缓慢缩容”的策略,这在应对突发流量和保证服务稳定性方面是一种常见的最佳实践。

传统的稳定化 (normalizeDesiredReplicas)

如果 HPA 规范中未定义 behavior 字段,则会调用传统的 normalizeDesiredReplicas 函数。此函数只对缩容进行稳定化处理。其底层实现依赖于 stabilizeRecommendation 函数:

  1. 稳定化推荐值 (stabilizeRecommendation)

    • 此函数是传统缩容稳定化的核心。它使用 downscaleStabilisationWindow(默认为 5 分钟)作为时间窗口。
    • 当需要缩容时(即计算出的期望副本数小于当前副本数),它会回顾这个稳定窗口内的所有历史推荐值。
    • 然后,它会选择这些历史推荐值中的最大值作为最终的期望副本数。
    • 这种机制可以有效地防止因指标短暂下跌(例如,一次性的请求低谷)而触发不必要的缩容,从而保证了服务的稳定性。
    • 对于扩容,此函数不做任何限制,计算出的期望副本数会被直接采纳。
  2. 边界检查 (convertDesiredReplicasWithRules)

    • 在稳定化之后,normalizeDesiredReplicas 还会调用 convertDesiredReplicasWithRules 函数。
    • 这个函数非常简单,它只负责确保最终的副本数不会超过 HPA 定义的 minReplicasmaxReplicas 边界。

4. 执行扩缩容策略

在计算并稳定期望副本数之后,控制器就进入了最终的执行阶段。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// pkg/controller/podautoscaler/horizontal.go

if rescale {
    scale.Spec.Replicas = desiredReplicas
    _, err = a.scaleNamespacer.Scales(hpa.Namespace).Update(ctx, targetGR, scale, metav1.UpdateOptions{})
    if err != nil {
        // ... 错误处理和事件记录 ...
        return fmt.Errorf("failed to rescale %s: %v", reference, err)
    }
    // ... 成功处理和事件记录 ...
    a.storeScaleEvent(hpa.Spec.Behavior, key, currentReplicas, desiredReplicas)
} else {
    // ... 记录无需扩缩容的日志 ...
}
  1. 判断是否需要扩缩容rescale 标志位决定了是否执行扩缩容。只有当稳定化后的 desiredReplicascurrentReplicas 不相等时,该标志位才为 true

  2. 更新 Scale 子资源:如果需要扩缩容,控制器会:

    • 将目标资源的 scale 对象的 spec.replicas 字段更新为最终的 desiredReplicas
    • 调用 a.scaleNamespacer.Scales(hpa.Namespace).Update(...) 方法。这个调用会向 Kubernetes API Server 发送一个 UPDATE 请求,目标是 HPA scaleTargetRef 所指向资源的 scale 子资源。
  3. 触发实际的 Pod 数量调整:一旦 scale 子资源被成功更新,相应的工作负载控制器(如 Deployment 或 ReplicaSet 控制器)会监听到这一变化,并负责创建或删除 Pod,以使实际的副本数与新的期望值保持一致。

  4. 处理结果

    • 成功:如果 Update 调用成功,HPA 控制器会记录一个 SuccessfulRescaleNormal 事件,更新 AbleToScale 条件为 True,并调用 storeScaleEvent 来记录此次扩缩容事件,以供未来的 behavior 稳定化计算使用。
    • 失败:如果 Update 调用失败(例如,由于网络问题或权限不足),控制器会记录一个 FailedRescaleWarning 事件,将 AbleToScale 条件设置为 False,并返回错误。返回的错误会使该 HPA 的 key 被重新放回工作队列,以便稍后重试。
  5. 无需扩缩容:如果 rescalefalse,控制器仅记录一条日志,表明本次协调周期内无需进行扩缩容操作。

这一步是整个 HPA 流程的闭环,它将计算得出的决策转化为对集群状态的实际变更。

5. 更新 HPA 状态

  • 最后,控制器会调用 setStatus 函数来更新 HPA 对象的 .status 字段,包括 currentReplicasdesiredReplicaslastScaleTime 以及每个指标的当前状态。

6. 周期性同步与扩缩容延迟

HPA 控制器的扩缩容决策并非实时触发,而是依赖于周期性的协调循环。这个循环的频率由 kube-controller-manager 的启动参数 --horizontal-pod-autoscaler-sync-period 控制,默认值为 15 秒

这意味着,HPA 控制器每 15 秒才会执行一次完整的 reconcile 流程,包括从 metrics-server 获取指标、计算期望副本数、应用稳定化策略等。

延迟的来源:

扩缩容的端到端延迟主要来自两个方面:

  1. HPA 同步周期:由 --horizontal-pod-autoscaler-sync-period 参数决定,默认为 15 秒。
  2. 指标采集延迟metrics-server 本身也是周期性地从每个节点的 Kubelet 上抓取指标的,由 --metric-resolution 参数决定,这个周期默认为 15 秒。
  3. Kubelet 通过其内嵌的 cAdvisor 来获取容器指标的,这个获取周期由 Kubelet 的 --housekeeping-interval 参数控制,默认为 10 秒。因此,指标从容器产生到被 HPA 控制器获取,中间经过了多层缓存和周期性采集。

极端情况下的延迟分析:

在最坏的情况下,当流量高峰到来时,一个完整的扩容决策可能面临的延迟是 HPA 同步周期 + metrics-server 采集周期 + kubelet 采集周期 的总和。例如,如果一次流量高峰恰好发生在 metrics-server 采集之后、HPA 同步之前,那么 HPA 需要等待下一个同步周期才能看到上一个采集周期的"旧"数据,然后做出决策。

这种固有的延迟意味着 HPA 更适合应对可预测的、缓慢变化的负载。对于需要秒级响应的突发流量,单纯依赖 HPA 可能无法满足需求,此时可以考虑结合使用其他技术,例如基于请求的预测性扩缩容工具(如 KEDA)或预先保留足够的缓冲资源。

总结

HPA 控制器通过一个持续的协调循环,周期性地监控指标、计算期望副本数,并通过一个精密的稳定化机制来避免过于频繁的扩缩容。它提供了丰富的配置选项(特别是 behavior 字段),允许用户根据应用特性和稳定性要求,精细地调整扩缩容的行为。

理解 HPA 的工作原理、计算公式和稳定化策略,对于在生产环境中有效利用 Kubernetes 的自动扩缩容能力至关重要。

0%