圖解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

相關文章
相關標籤/搜索