图解kubernetes控制器HPA横向伸缩的关键实现

HPA是k8s中横向伸缩的实现,里面有不少能够借鉴的思想,好比延迟队列、时间序列窗口、变动事件机制、稳定性考量等关键机制, 让咱们一块儿来学习下大佬们的关键实现算法

1. 基础概念

HorizontalPodAutoscaler(后面简称HPA)做为通用横向扩容的实现,有不少关键的机制,这里咱们先来看下这些关键的的机制的目标api

1.1 横向扩容实现机制

image.png

HPA控制器实现机制主要是经过informer获取当前的HPA对象,而后经过metrics服务获取对应Pod集合的监控数据, 接着根据当前目标对象的scale当前状态,并根据扩容算法决策对应资源的当前副本并更新Scale对象,从而实现自动扩容的微信

1.2 HPA的四个区间

根据HPA的参数和当前Scale(目标资源)的当前副本计数,能够将HPA分为以下四种个区间:关闭、高水位、低水位、正常,只有处于正常区间内,HPA控制器才会进行动态的调整app

1.3 度量指标类型

HPA目前支持的度量类型主要包含两种Pod和Resource,剩下的虽然在官方的描述中有说明,可是代码上目前并无实现,监控的数据主要是经过apiserver代理metrics server实现,访问接口以下ide

/api/v1/model/namespaces/{namespace}/pod-list/{podName1,podName2}/metrics/{metricName}

1.4 延迟队列

image.png

HPA控制器并不监控底层的各类informer好比Pod、Deployment、ReplicaSet等资源的变动,而是每次处理完成后都将当前HPA对象从新放入延迟队列中,从而触发下一次的检测,若是你没有修改默认这个时间是15s, 也就是说再进行一次一致性检测以后,即时度量指标超量也至少须要15s的时间才会被HPA感知到函数

1.5 监控时间序列窗口

image.png

在从metrics server获取pod监控数据的时候,HPA控制器会获取最近5分钟的数据(硬编码)并从中获取最近1分钟(硬编码)的数据来进行计算,至关于取最近一分钟的数据做为样原本进行计算,注意这里的1分钟是指的监控数据中最新的那边指标的前一分钟内的数据,而不是当时间源码分析

1.6 稳定性与延迟

image.png

前面提过延迟队列会每15s都会触发一次HPA的检测,那若是1分钟内的监控数据有所变更,则就会产生不少scale更新操做,从而致使对应的控制器的副本时数量的频繁的变动, 为了保证对应资源的稳定性, HPA控制器在实现上加入了一个延迟时间,即在该时间窗口内会保留以前的决策建议,而后根据当前全部有效的决策建议来进行决策,从而保证指望的副本数量尽可能小的变动,保证稳定性学习

基础的概念就先介绍这些,由于HPA里面主要是计算逻辑比较多,核心实现部分今天代码量会多一点ui

2.核心实现

HPA控制器的实现,主要分为以下部分:获取scale对象、根据区间进行快速决策, 而后就是核心实现根据伸缩算法根据当前的metric、当前副本、伸缩策略来进行最终指望副本的计算,让咱们依次来看下关键实现编码

2.1 根据ScaleTargetRef来获取scale对象

主要是根据神器scheme来获取对应的版本,而后在经过版本获取对应的Resource的scale对象

targetGV, err := schema.ParseGroupVersion(hpa.Spec.ScaleTargetRef.APIVersion)    
	targetGK := schema.GroupKind{
        Group: targetGV.Group,
        Kind:  hpa.Spec.ScaleTargetRef.Kind,
    }
	scale, targetGR, err := a.scaleForResourceMappings(hpa.Namespace, hpa.Spec.ScaleTargetRef.Name, mappings)

2.2 区间决策

image.png

区间决策会首先根据当前的scale对象和当前hpa里面配置的对应的参数的值,决策当前的副本数量,其中针对于超过设定的maxReplicas和小于minReplicas两种状况,只须要简单的修正为对应的值,直接更新对应的scale对象便可,而scale副本为0的对象,则hpa不会在进行任何操做

if scale.Spec.Replicas == 0 && minReplicas != 0 {
        // 已经关闭autoscaling
        desiredReplicas = 0
        rescale = false
        setCondition(hpa, autoscalingv2.ScalingActive, v1.ConditionFalse, "ScalingDisabled", "scaling is disabled since the replica count of the target is zero")
    } else if currentReplicas > hpa.Spec.MaxReplicas {
        // 若是当前副本数大于指望副本
        desiredReplicas = hpa.Spec.MaxReplicas
    } else if currentReplicas < minReplicas {
        // 若是当前副本数小于最小副本
        desiredReplicas = minReplicas
    } else {
		// 该部分逻辑比较复杂,后面单独说,其实也就是HPA最关键的实现部分之一
    }

2.3 HPA动态伸缩决策核心逻辑

image.png

核心决策逻辑主要分为两个大的步骤:1)经过监控数据决策当前的目标指望副本数量 2)根据behavior来进行最终指望副本数量的修正, 而后咱们继续深刻底层

// 经过监控数据获取获取指望的副本数量、时间、状态
        metricDesiredReplicas, metricName, metricStatuses, metricTimestamp, err = a.computeReplicasForMetrics(hpa, scale, hpa.Spec.Metrics)
		
		// 若是经过监控决策的副本数量不为0,则就设置指望副本为监控决策的副本数
        if metricDesiredReplicas > desiredReplicas {
            desiredReplicas = metricDesiredReplicas
            rescaleMetric = metricName
        }
		// 根据behavior是否设置来进行最终的指望副本决策,其中也会考虑以前稳定性的相关数据
        if hpa.Spec.Behavior == nil {
            desiredReplicas = a.normalizeDesiredReplicas(hpa, key, currentReplicas, desiredReplicas, minReplicas)
        } else {
            desiredReplicas = a.normalizeDesiredReplicasWithBehaviors(hpa, key, currentReplicas, desiredReplicas, minReplicas)
        }
        // 若是发现当前副本数量不等于指望副本数
        rescale = desiredReplicas != currentReplicas

2.4 多维度量指标的副本计数决策

在HPA中可用设定多个监控度量指标,HPA在实现上会根据监控数据,从多个度量指标中获取提议最大的副本计数做为最终目标,为何要采用最大的呢?由于要尽可能知足全部的监控度量指标的扩容要求,因此就须要选择最大的指望副本计数

func (a *HorizontalController) computeReplicasForMetrics(hpa *autoscalingv2.HorizontalPodAutoscaler, scale *autoscalingv1.Scale,
    // 根据设置的metricsl来进行提议副本数量的计算
    for i, metricSpec := range metricSpecs {
        // 获取提议的副本、数目、时间
        replicaCountProposal, metricNameProposal, timestampProposal, condition, err := a.computeReplicasForMetric(hpa, metricSpec, specReplicas, statusReplicas, selector, &statuses[i])

        if err != nil {
            if invalidMetricsCount <= 0 {
                invalidMetricCondition = condition
                invalidMetricError = err
            }
            // 无效的副本计数 
            invalidMetricsCount++
        }
        if err == nil && (replicas == 0 || replicaCountProposal > replicas) {
            // 每次都取较大的副本提议
            timestamp = timestampProposal
            replicas = replicaCountProposal
            metric = metricNameProposal
        }
    }
}

2.5 Pod度量指标的计算与指望副本决策实现

image.png

由于篇幅限制这里只讲述Pod度量指标的计算实现机制,由于内容比较多,这里会分为几个小节,让咱们一块儿来探索

2.5.1 计算Pod度量指标数据

这里就是前面说的最近监控指标的获取部分, 在获取到监控指标数据以后,会取对应Pod最后一分钟的监控数据的平均值做为样本参与后面的指望副本计算

func (h *HeapsterMetricsClient) GetRawMetric(metricName string, namespace string, selector labels.Selector, metricSelector labels.Selector) (PodMetricsInfo, time.Time, error) {
    // 获取全部的pod
    podList, err := h.podsGetter.Pods(namespace).List(metav1.ListOptions{LabelSelector: selector.String()})

    // 最近5分钟的状态
    startTime := now.Add(heapsterQueryStart)
    metricPath := fmt.Sprintf("/api/v1/model/namespaces/%s/pod-list/%s/metrics/%s",
        namespace,
        strings.Join(podNames, ","),
        metricName)
    resultRaw, err := h.services.
        ProxyGet(h.heapsterScheme, h.heapsterService, h.heapsterPort, metricPath, map[string]string{"start": startTime.Format(time.RFC3339)}).
        DoRaw()
    var timestamp *time.Time
    res := make(PodMetricsInfo, len(metrics.Items))
    // 遍历全部Pod的监控数据,而后进行最后一分钟的取样
    for i, podMetrics := range metrics.Items {
        // 其pod在最近1分钟内的平均值 
        val, podTimestamp, hadMetrics := collapseTimeSamples(podMetrics, time.Minute)
        if hadMetrics {
            res[podNames[i]] = PodMetric{
                Timestamp: podTimestamp,
                Window:    heapsterDefaultMetricWindow, // 1分钟 
                Value:     int64(val),
            }

            if timestamp == nil || podTimestamp.Before(*timestamp) {
                timestamp = &podTimestamp
            }
        }
    }

}

2.5.2 指望副本计算实现

指望副本的计算实现主要是在calcPlainMetricReplicas中,这里须要考虑的东西比较多,根据个人理解,我将这部分拆成一段段,方便读者理解,这些代码都属于calcPlainMetricReplicas

1.在获取监控数据的时候,对应的Pod可能会有三种状况:

readyPodCount, ignoredPods, missingPods := groupPods(podList, metrics, resource, c.cpuInitializationPeriod, c.delayOfInitialReadinessStatus)

1)当前Pod还在Pending状态,该类Pod在监控中被记录为ignore即跳过的(由于你也不知道他到底会不会成功,但至少目前是不成功的) 记为ignoredPods 2)正常状态,即有监控数据,就证实是正常的,至少还能获取到你的监控数据, 被极为记为readyPod 3)除去上面两种状态而且还没被删除的Pod都被记为missingPods

2.计算使用率

usageRatio, utilization := metricsclient.GetMetricUtilizationRatio(metrics, targetUtilization)

计算使用率其实就相对简单,咱们就只计算readyPods的全部Pod的使用率便可

3.重平衡ignored

rebalanceIgnored := len(ignoredPods) > 0 && usageRatio > 1.0
// 中间省略部分逻辑 
    if rebalanceIgnored {
        // on a scale-up, treat unready pods as using 0% of the resource request
        // 若是须要重平衡跳过的pod. 放大后,将未就绪的pod视为使用0%的资源请求
        for podName := range ignoredPods {
            metrics[podName] = metricsclient.PodMetric{Value: 0}
        }
    }

若是使用率大于1.0则代表当前已经ready的Pod实际上已经达到了HPA触发阈值,可是当前正在pending的这部分Pod该如何计算呢?在k8s里面常说的一个句话就是最终指望状态,那对于这些当前正在pending状态的Pod其实最终大几率会变成ready。由于使用率如今已经超量,那我加上去这部分将来可能会成功的Pod,是否是就能知足阈值要求呢?因此这里就将对应的Value射为0,后面会从新计算,加入这部分Pod后是否能知足HPA的阈值设定

4.missingPods

if len(missingPods) > 0 {
        // 若是错误的pod大于0,即有部分pod没有获到metric数据
        if usageRatio < 1.0 {
           
            // 若是是小于1.0, 即表示未达到使用率,则将对应的值设置为target目标使用量
            for podName := range missingPods {
                metrics[podName] = metricsclient.PodMetric{Value: targetUtilization}
            }
        } else {
            
            // 若是>1则代表, 要进行扩容, 则此时就那些未得到状态的pod值设置为0
            for podName := range missingPods {
                metrics[podName] = metricsclient.PodMetric{Value: 0}
            }
        }
    }

missingPods是当前既不在Ready也不在Pending状态的Pods, 这些Pod多是失联也多是失败,可是咱们没法预知其状态,这就有两种选择,要么给个最大值、要么给个最小值,那么如何决策呢?答案是看当的使用率,若是使用率低于1.0即未到阈值,则咱们尝试给这部分未知的 Pod的最大值,尝试若是这部分Pod不能恢复,咱们当前会不会达到阈值,反之则会授予最小值,伪装他们不存在

5.决策结果

if math.Abs(1.0-newUsageRatio) <= c.tolerance || (usageRatio < 1.0 && newUsageRatio > 1.0) || (usageRatio > 1.0 && newUsageRatio < 1.0) {
        // 若是更改过小,或者新的使用率会致使缩放方向的更改,则返回当前副本
        return currentReplicas, utilization, nil
    }

在通过上述的修正数据后,会从新进行使用率计算即newUsageRatio,若是发现计算后的值在容忍范围以内,当前是0.1,则就会进行任何的伸缩操做

反之在从新计算使用率以后,若是咱们本来使用率<1.0即未达到阈值,进行数据填充后,如今却超过1.0,则不该该进行任何操做,为啥呢?由于本来ready的全部节点使用率<1.0,但你如今计算超出了1.0,则就应该缩放,你要是吧ready的缩放了,而且以前那些未知的节点依旧宕机,则就要从新进行扩容,这是否是在作无用功呢?

2.6 带Behavior的稳定性决策

image.png

不带behaviors的决策相对简单一些,这里咱们主要聊下带behavior的决策实现,内容比较多也会分为几个小节, 全部实现主要是在stabilizeRecommendationWithBehaviors中

2.6.1 稳定时间窗口

HPA控制器中针对扩容和缩容分别有一个时间窗口,即在该窗口内会尽可能保证HPA扩缩容的最终目标处于一个稳定的状态,其中扩容是3分钟,而缩容是5分钟

2.6.2 根据指望副本是否知足更新延迟时间

if args.DesiredReplicas >= args.CurrentReplicas {
		// 若是指望的副本数大于等于当前的副本数,则延迟时间=scaleUpBehaviro的稳定窗口时间
		scaleDelaySeconds = *args.ScaleUpBehavior.StabilizationWindowSeconds
		betterRecommendation = min
	} else {
		// 指望副本数<当前的副本数
		scaleDelaySeconds = *args.ScaleDownBehavior.StabilizationWindowSeconds
		betterRecommendation = max
	}

在伸缩策略中, 针对扩容会按照窗口内的最小值来进行扩容,而针对缩容则按照窗口内的最大值来进行

2.6.3 计算最终建议副本数

首先根据延迟时间在当前窗口内,按照建议的比较函数去得到建议的目标副本数,

// 过期截止时间
	obsoleteCutoff := time.Now().Add(-time.Second * time.Duration(maxDelaySeconds))

	// 截止时间
	cutoff := time.Now().Add(-time.Second * time.Duration(scaleDelaySeconds))
	for i, rec := range a.recommendations[args.Key] {
		if rec.timestamp.After(cutoff) {
			// 在截止时间以后,则当前建议有效, 则根据以前的比较函数来决策最终的建议副本数
			recommendation = betterRecommendation(rec.recommendation, recommendation)
		}
	}

2.6.4 根据behavior进行指望副本决策

在以前进行决策我那次后,会决策出指望的最大值,此处就只须要根据behavior(其实就是咱们伸缩容的策略)来进行最终指望副本的决策, 其中calculateScaleUpLimitWithScalingRules和calculateScaleDownLimitWithBehaviors其实只是根据咱们扩容的策略,来进行对应pod数量的递增或者缩减操做,其中关键的设计是下面周期事件的关联计算

func (a *HorizontalController) convertDesiredReplicasWithBehaviorRate(args NormalizationArg) (int32, string, string) {
	var possibleLimitingReason, possibleLimitingMessage string

	if args.DesiredReplicas > args.CurrentReplicas {
		// 若是指望副本大于当前副本,则就进行扩容
		scaleUpLimit := calculateScaleUpLimitWithScalingRules(args.CurrentReplicas, a.scaleUpEvents[args.Key], args.ScaleUpBehavior)
		if scaleUpLimit < args.CurrentReplicas {
			// 若是当前副本的数量大于限制的数量,则就不该该继续扩容,当前已经知足率了扩容需求
			scaleUpLimit = args.CurrentReplicas
		}
		// 最大容许的数量
		maximumAllowedReplicas := args.MaxReplicas
		if maximumAllowedReplicas > scaleUpLimit {
			// 若是最大数量大于扩容上线
			maximumAllowedReplicas = scaleUpLimit
		} else {
		}
		if args.DesiredReplicas > maximumAllowedReplicas {
			// 若是指望副本数量>最大容许副本数量
			return maximumAllowedReplicas, possibleLimitingReason, possibleLimitingMessage
		}
	} else if args.DesiredReplicas < args.CurrentReplicas {
		// 若是指望副本小于当前副本,则就进行缩容
		scaleDownLimit := calculateScaleDownLimitWithBehaviors(args.CurrentReplicas, a.scaleDownEvents[args.Key], args.ScaleDownBehavior)
		if scaleDownLimit > args.CurrentReplicas {
			scaleDownLimit = args.CurrentReplicas
		}
		minimumAllowedReplicas := args.MinReplicas
		if minimumAllowedReplicas < scaleDownLimit {
			minimumAllowedReplicas = scaleDownLimit
		} else {
		}
		if args.DesiredReplicas < minimumAllowedReplicas {
			return minimumAllowedReplicas, possibleLimitingReason, possibleLimitingMessage
		}
	}
	return args.DesiredReplicas, "DesiredWithinRange", "the desired count is within the acceptable range"
}

2.6.5周期事件

周期事件是指的在稳定时间窗口内,对应资源的全部变动事件,好比咱们最终决策出指望的副本是newReplicas,而当前已经有curRepicas, 则本次决策的完成在更新完scale接口以后,还会记录一个变动的数量即newReplicas-curReplicas,最终咱们能够统计咱们的稳定窗口内的事件,就知道在这个周期内咱们是扩容了N个Pod仍是缩容了N个Pod,那么下一次计算指望副本的时候,咱们就能够减去这部分已经变动的数量,只新加通过本轮决策后,仍然欠缺的那部分便可

func getReplicasChangePerPeriod(periodSeconds int32, scaleEvents []timestampedScaleEvent) int32 {
	// 计算周期
	period := time.Second * time.Duration(periodSeconds)
	// 截止时间
	cutoff := time.Now().Add(-period)
	var replicas int32
	// 获取最近的变动
	for _, rec := range scaleEvents {
		if rec.timestamp.After(cutoff) {
			// 更新副本修改的数量, 会有正负,最终replicas就是最近变动的数量
			replicas += rec.replicaChange
		}
	}
	return replicas
}

3.实现总结

image.png

HPA控制器实现里面,比较精彩的部分应该主要是在使用率计算那部分,如何根据不一样的状态来进行对应未知数据的填充并进行从新决策(比较值得借鉴的设计), 其次就是基于稳定性、变动事件、扩容策略的最终决策都是比较牛逼的设计,最终面向用户的只须要一个yaml,向大佬们学习

参考文献

https://kubernetes.io/zh/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/

kubernetes学习笔记地址: https://www.yuque.com/baxiaoshi/tyado3

微信号:baxiaoshi2020 关注公告号阅读更多源码分析文章 图解源码 更多文章关注 www.sreguide.com

相关文章
相关标签/搜索