我司在线上的一个集群中对外提供了 Kubernetes 的 HPA Feature,这货一直工作好好但有几次出现了弹性失效的情况。 具体的表象为当服务 Scale Down 的时候就再也上不去,与此同时存在大量的 Terminating Pod 不过这是官方已知的一个 issue Pod Stuck on Terminating #51835 最后的结果就是业务出现严重的堆积,已有实例过载。
问题排查
之前只读过一到两次的 HPA 部分的代码,所以一开始我猜测这部分是因为 Terminating 的 Pod 计入了阈值的分母导致数值稀释引发的。 但再读代码后确认这块是排除的(采集 Pod 指标时就剔除了这部分的数据),于是只好再读代码。
HPA 代码分析
版本:v1.9.2 5fa2db2bd4,以下以 CPU HPA 为例
HPA 在初始化后调用 Run 函数持续运行
// Run begins watching and syncing.
func (a *HorizontalController) Run(stopCh <-chan struct{}) {
defer utilruntime.HandleCrash()
defer a.queue.ShutDown()
glog.Infof("Starting HPA controller")
defer glog.Infof("Shutting down HPA controller")
if !controller.WaitForCacheSync("HPA", stopCh, a.hpaListerSynced) {
return
}
// start a single worker (we may wish to start more in the future)
// 这边调用的 worker 是主体执行函数
go wait.Until(a.worker, time.Second, stopCh)
<-stopCh
}
而在 worker 中循环调用 processNextWorkItem
来处理 HPA 检查即主逻辑。
// 主逻辑入口
func (a *HorizontalController) processNextWorkItem() bool {
// 每次从 ratelimit queue 里取一个 key。实际格式为 `namespace/hpa-name`
key, quit := a.queue.Get()
if quit {
return false
}
defer a.queue.Done(key)
err := a.reconcileKey(key.(string))
if err == nil {
// don't "forget" here because we want to only process a given HPA once per resync interval
return true
}
// 重入 queue
a.queue.AddRateLimited(key)
utilruntime.HandleError(err)
return true
}
func (a *HorizontalController) reconcileKey(key string) error {
// 拆分上面取到的 Queue Key
namespace, name, err := cache.SplitMetaNamespaceKey(key)
if err != nil {
return err
}
// 取 HPA Object
hpa, err := a.hpaLister.HorizontalPodAutoscalers(namespace).Get(name)
if errors.IsNotFound(err) {
glog.Infof("Horizontal Pod Autoscaler has been deleted %v", key)
return nil
}
// 根据 HPA Spec 处理伸缩
return a.reconcileAutoscaler(hpa)
}
func (a *HorizontalController) reconcileAutoscaler(hpav1Shared *autoscalingv1.HorizontalPodAutoscaler) error {
// 省略一些 DeepCopy 跟 Parse
// 根据给定资源对象获取 Scale Object
scale, targetGR, err := a.scaleForResourceMappings(hpa.Namespace, hpa.Spec.ScaleTargetRef.Name, mappings)
if err != nil {
a.eventRecorder.Event(hpa, v1.EventTypeWarning, "FailedGetScale", err.Error())
setCondition(hpa, autoscalingv2.AbleToScale, v1.ConditionFalse, "FailedGetScale", "the HPA controller was unable to get the target's current scale: %v", err)
a.updateStatusIfNeeded(hpaStatusOriginal, hpa)
return fmt.Errorf("failed to query scale subresource for %s: %v", reference, err)
}
// ...
else {
// 计算期望要的实例数
metricDesiredReplicas, metricName, metricStatuses, metricTimestamp, err = a.computeReplicasForMetrics(hpa, scale, hpa.Spec.Metrics)
// 设置 desiredReplicas
// 根据 Mix Max 调整
desiredReplicas = a.normalizeDesiredReplicas(hpa, currentReplicas, desiredReplicas)
}
// Scale or nothing
}
func (a *HorizontalController) computeReplicasForMetrics(hpa *autoscalingv2.HorizontalPodAutoscaler, scale *autoscalingv1.Scale,
// 多个依次计算期望值
for i, metricSpec := range metricSpecs {
switch metricSpec.Type {
case autoscalingv2.ObjectMetricSourceType:
...
case autoscalingv2.PodsMetricSourceType:
...
case autoscalingv2.ResourceMetricSourceType:
if metricSpec.Resource.TargetAverageValue != nil {
...
} else {
/// 检查 TargetAverageUtilization 设置
if metricSpec.Resource.TargetAverageUtilization == nil { ... }
...
// 计算实例数
replicaCountProposal, percentageProposal, rawProposal, timestampProposal, err = a.replicaCalc.GetResourceReplicas(currentReplicas, targetUtilization, metricSpec.Resource.Name, hpa.Namespace, selector)
}
default:
...
}
}
setCondition...
}
GetResourceReplicas
func (c *ReplicaCalculator) GetResourceReplicas(currentReplicas int32, targetUtilization int32, resource v1.ResourceName, namespace string, selector labels.Selector) (replicaCount int32, utilization int32, rawUtilization int64, timestamp time.Time, err error) {
// 获取 Metrics 指标
// 根据 HPA Selector 获取 POD 列表
for _, pod := range podList.Items {
podSum := int64(0)
for _, container := range pod.Spec.Containers {
// 累加 POD Container Reuqests
}
requests[pod.Name] = podSum
// 如果不是 Running 或则 Ready 的 POD 直接跳过。**但 Request 照加**
if pod.Status.Phase != v1.PodRunning || !podutil.IsPodReady(&pod) {
// save this pod name for later, but pretend it doesn't exist for now
unreadyPods.Insert(pod.Name)
delete(metrics, pod.Name)
continue
}
if _, found := metrics[pod.Name]; !found {
// save this pod name for later, but pretend it doesn't exist for now
missingPods.Insert(pod.Name)
continue
}
}
// 撮合计算各指标的使用率,返回 使用率、使用量、平均用量
// 这里的计算实际会忽略 NonReady 跟 NonRunning 的 POD
usageRatio, utilization, rawUtilization, err := metricsclient.GetResourceUtilizationRatio(metrics, requests, targetUtilization)
if err != nil {
return 0, 0, 0, time.Time{}, err
}
// 当 Request 使用率 > 1 且有 unready 的 Pod
rebalanceUnready := len(unreadyPods) > 0 && usageRatio > 1.0
if !rebalanceUnready && len(missingPods) == 0 {
// 没 unready 且都有 metrics
// 使用率 < 1 且都有 metrics
// 使用率在容忍范围内
if math.Abs(1.0-usageRatio) <= c.tolerance {
return currentReplicas, utilization, rawUtilization, timestamp, nil
}
// 期望实例数 使用率 * Ready Pod 向上取整
return int32(math.Ceil(usageRatio * float64(readyPodCount))), utilization, rawUtilization, timestamp, nil
}
if len(missingPods) > 0 {
if usageRatio < 1.0 {
// 可能缩容,缺的 metrics = request
} else if usageRatio > 1.0 {
// 可能扩容,缺的 metrics = 0
}
}
if rebalanceUnready {
// 可能扩容,缺的 metrics = 0
}
// 重算使用率
// 这里把 NonReady 跟 NonRunning 补齐也就让 GetResourceUtilizationRatio 一起算了
newUsageRatio, _, _, err := metricsclient.GetResourceUtilizationRatio(metrics, requests, targetUtilization)
// 容忍范围内或新旧使用率一升一降
if math.Abs(1.0-newUsageRatio) <= c.tolerance || (usageRatio < 1.0 && newUsageRatio > 1.0) || (usageRatio > 1.0 && newUsageRatio < 1.0) {
// 不伸缩
return currentReplicas, utilization, rawUtilization, timestamp, nil
}
// 期望实例数 新使用率 * POD 数 向上取整
return int32(math.Ceil(newUsageRatio * float64(len(metrics)))), utilization, nil
}
func GetResourceUtilizationRatio(metrics PodMetricsInfo, requests map[string]int64, targetUtilization int32) (utilizationRatio float64, currentUtilization int32, rawAverageValue int64, err error) {
// 根据 metrics 进行循环
}
结论
当 Deployment 出现 POD Terminating 时可能导致 GetResourceUtilizationRatio
在 GetResourceReplicas
metrics 补齐前后出现 usageRatio 一个大于 1 一个小于 1 导致不进行伸缩。
所以目前给到业务的建议是重要业务不要建 HPA minReplicas 调整过低,并同期对 HPA 实例异常状况进行监控告警。