圖解kubernetes控制器HPA橫向伸縮的關鍵實現

HPA是k8s中橫向伸縮的實現,裏面有很多可以借鑑的思想,比如延遲隊列、時間序列窗口、變更事件機制、穩定性考量等關鍵機制, 讓我們一起來學習下大佬們的關鍵實現

1. 基礎概念

HorizontalPodAutoscaler(後面簡稱HPA)作爲通用橫向擴容的實現,有很多關鍵的機制,這裏我們先來看下這些關鍵的的機制的目標

1.1 橫向擴容實現機制

image.png

HPA控制器實現機制主要是通過informer獲取當前的HPA對象,然後通過metrics服務獲取對應Pod集合的監控數據, 接着根據當前目標對象的scale當前狀態,並根據擴容算法決策對應資源的當前副本並更新Scale對象,從而實現自動擴容的

1.2 HPA的四個區間

根據HPA的參數和當前Scale(目標資源)的當前副本計數,可以將HPA分爲如下四種個區間:關閉、高水位、低水位、正常,只有處於正常區間內,HPA控制器纔會進行動態的調整

1.3 度量指標類型

HPA目前支持的度量類型主要包含兩種Pod和Resource,剩下的雖然在官方的描述中有說明,但是代碼上目前並沒有實現,監控的數據主要是通過apiserver代理metrics server實現,訪問接口如下

/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裏面主要是計算邏輯比較多,核心實現部分今天代碼量會多一點

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 關注公告號閱讀更多源碼分析文章 圖解源碼 本文由博客一文多發平臺 OpenWrite 發佈

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章