Kubernetes 的 Horizontal Pod Autoscaler(HPA)是一种根据观察到的 CPU 利用率或其他自定义指标自动扩展 Pod 副本数的控制器。它在业务繁忙的时候可以有效的对 Pod 进行横线扩展,但是最近发现明明使用率已经超过了定义的目标值,但是为何没有扩容呢?
9906771bea31d64adb2a89a2f2b88207 MD5
为了搞清楚原由,我们从源码层面来找找原因。
一、HPA 的整体架构与核心组件
HPA 的实现位于 Kubernetes 的 k8s.io/kubernetes/pkg/controller/podautoscaler 目录下,主要由以下几个组件构成:
- HorizontalController:主控制器,负责监听 HPA 和 Pod 资源,协调扩缩容。
- ReplicaCalculator:计算目标副本数的核心逻辑。
- MetricsClient:获取指标数据(如 CPU、内存、自定义指标)。
- ScaleClient:用于修改工作负载(如 Deployment、ReplicaSet)的副本数。
二、源码入口:HPA 控制器的启动
HPA 控制器在cmd/kube-controller-manager 启动时被初始化。
在 cmd/kube-controller-manager/controllermanager.go 中的 Run() 调用 NewControllerDescriptors() 中将控制器注册。
复制然后在 cmd/kube-controller-manager/autoscaling.go 里面最终通过 startHPAControllerWithMetricsClient() 来启动。
复制三、控制器核心逻辑
控制器的核心实现逻辑的代码位于 k8s.io/kubernetes/pkg/controller/podautoscaler 中,其调用链路为:
复制其中主要的逻辑在 reconcileAutoscaler 中实现。
(1)使用 a.monitor.ObserveReconciliationResult(actionLabel, errorLabel, time.Since(start)) 记录协调过程中的监控指标。 (2)使用 hpaShared.DeepCopy() 和 hpa.Status.DeepCopy() 对 hpa 和 hpaStaus 对象进行深度拷贝,避免修改共享缓存。 (3)然后对资源进行解析并实现资源映射。
复制其中:
- schema.ParseGroupVersion : 解析目标资源的API版本
- a.mapper.RESTMappings : 获取资源的REST映射信息
- a.scaleForResourceMappings : 获取目标资源的Scale子资源
(4)对指标进行核心计算获取期望副本
复制(5)根据是否配置了 Behavior 选择不通的标准化策略
复制(6)对于满足扩缩容要求的进行扩缩容操作
复制这里使用 retry.RetryOnConflict 处理并发冲突的重试机制。实际上对目标资源的更新操作是调用 a.scaleNamespacer.Scales().Update 实现。
(7)最后更新状态和事件记录
复制以上就是 reconcileAutoscaler 这个方法中的主要流程。其中最核心的地方在于副本数计算,它是在 computeReplicasForMetrics 中实现。
四、核心算法
现在我们对 computeReplicasForMetrics 方法进行解析,看看具体是怎么实现的。
(1)进行前置验证和初始化
复制(2)对指标进行循环计算
复制这里调用 replicaCountProposal, metricNameProposal, timestampProposal, condition, err := a.computeReplicasForMetric(ctx, hpa, metricSpec, specReplicas, statusReplicas, selector, &statuses[i]) 对每个指标进行计算。
在 computeReplicasForMetric 会根据不通的指标类型进行计算。
复制这里我们只拿对象指标 autoscalingv2.ObjectMetricSourceType 进行说明。如果类型是对象指标,则会调用 a.computeStatusForObjectMetric 来进行计算。
在 computeStatusForObjectMetric 中会先初始化指标状态,用于记录指标的当前状态。
复制然后调用 a.tolerancesForHpa(hpa) 获取当前对象的容忍度,在 tolerancesForHpa 中的代码实现如下:
复制默认容忍度在 pkg\controller\podautoscaler\config\v1alpha1\defaults.go 中定义,默认是 0.1,也就是 10% 的容忍度。
复制获取到容忍度之后,会分别就 绝对值目标 和 平均值目标 进行计算。
复制在计算 绝对值 目标的副本数中,使用 usageRatio := float64(usage) / float64(targetUsage) 来计算使用率,然后通过replicaCountFloat := usageRatio * float64(readyPodCount) 获取期望的副本数,如果副本数不是整数,则会向上取整。
复制在处理 平均值 目标的副本数中,是采用 usageRatio := float64(usage) / (float64(targetAverageUsage) * float64(replicaCount)) 来计算使用率,也就是 使用率 = 实际指标值 / (目标平均值 × 当前副本数)。当使用率超出容忍范围,则采用 math.Ceil(实际指标值 / 目标平均值) 重新计算副本数,否则副本数不变。
复制(3)如果指标无效则返回错误,否则返回期望副本数
复制这里的 容忍度 可以解释为何指标达到了87%,但是未触发扩容。
在上面我们介绍了默认的容忍度是 0.1 ,也就是 10%,也就是当前使用率在目标值的 ±10% 范围内,不会触发扩缩容。 我们可以使用容忍度的比较方法 (1.0-t.scaleDown) <= usageRatio && usageRatio <= (1.0+t.scaleUp) 来进行计算。
复制五、约束机制
HPA 的扩缩容也不是无限制的,为了避免频繁的扩缩容,除了容忍度之外,还增加了许多约束条件。
其主要在 a.normalizeDesiredReplicas 或 a.normalizeDesiredReplicasWithBehaviors 中进行实现。这两个实现的区别在于:
- normalizeDesiredReplicas是基础的标准化处理,而 normalizeDesiredReplicasWithBehaviors是高级的行为策略处理
- 要使用 normalizeDesiredReplicasWithBehaviors,则需要配置 hpa.Spec.Behavior,比如:
下面,我们在 normalizeDesiredReplicas 中进行说明,源代码如下:
复制在 convertDesiredReplicasWithRules 中通过 calculateScaleUpLimit 来计算扩容限制。
复制其中:
- scaleUpLimitFactor = 2.0 (扩容因子)
- scaleUpLimitMinimum = 4.0 (最小扩容限制)
其计算逻辑是:
- 扩容限制 = max(当前副本数 × 2, 4)
- 例如:当前2个副本,扩容限制为max(2×2, 4) = 4
- 例如:当前10个副本,扩容限制为max(10×2, 4) = 20
假设当前副本数为5,HPA配置最小2个、最大20个:
- 期望副本数为1 :返回2(最小限制),条件"TooFewReplicas"
- 期望副本数为8 :返回8(在范围内),条件"DesiredWithinRange"
- 期望副本数为15 :
- 扩容限制 = max(5×2, 4) = 10
- 返回10(扩容限制),条件"ScaleUpLimit"
- 期望副本数为25 :
扩容限制 = max(5×2, 4) = 10
返回10(扩容限制),条件"ScaleUpLimit"
这个函数是HPA安全扩缩容机制的重要组成部分,确保扩缩容操作既满足业务需求又不会造成系统不稳定。
六、最后
上面我们通过源码了解 HPA 的工作机制,了解到为何 HPA 的目标值设置为 80%,但是实际使用达到 87% 而没触发扩容。
其直接原因是 容忍度 导致的,但是在其他常见也可能因为 冷却窗口 影响扩容,甚至还可能是指标采集延迟导致指标不准确等各种因素。如果要用好 HPA,我们应该:
- 监控 HPA 状态:使用 kubectl describe hpa 查看 Conditions 和 Current Metrics
- 合理设置目标值:避免设置过高的目标利用率(如 >75%)
- 启用 Behavior 配置:精细化控制扩缩容节奏
- 结合日志与事件:关注 ScalingActive、ScalingLimited 等状态变化