HPA是k8s中橫向伸縮的實現,裏面有不少能夠借鑑的思想,好比延遲隊列、時間序列窗口、變動事件機制、穩定性考量等關鍵機制, 讓咱們一塊兒來學習下大佬們的關鍵實現算法
HorizontalPodAutoscaler(後面簡稱HPA)做爲通用橫向擴容的實現,有不少關鍵的機制,這裏咱們先來看下這些關鍵的的機制的目標api
HPA控制器實現機制主要是經過informer獲取當前的HPA對象,而後經過metrics服務獲取對應Pod集合的監控數據, 接着根據當前目標對象的scale當前狀態,並根據擴容算法決策對應資源的當前副本並更新Scale對象,從而實現自動擴容的微信
根據HPA的參數和當前Scale(目標資源)的當前副本計數,能夠將HPA分爲以下四種個區間:關閉、高水位、低水位、正常,只有處於正常區間內,HPA控制器纔會進行動態的調整app
HPA目前支持的度量類型主要包含兩種Pod和Resource,剩下的雖然在官方的描述中有說明,可是代碼上目前並無實現,監控的數據主要是經過apiserver代理metrics server實現,訪問接口以下ide
/api/v1/model/namespaces/{namespace}/pod-list/{podName1,podName2}/metrics/{metricName}
HPA控制器並不監控底層的各類informer好比Pod、Deployment、ReplicaSet等資源的變動,而是每次處理完成後都將當前HPA對象從新放入延遲隊列中,從而觸發下一次的檢測,若是你沒有修改默認這個時間是15s, 也就是說再進行一次一致性檢測以後,即時度量指標超量也至少須要15s的時間纔會被HPA感知到函數
在從metrics server獲取pod監控數據的時候,HPA控制器會獲取最近5分鐘的數據(硬編碼)並從中獲取最近1分鐘(硬編碼)的數據來進行計算,至關於取最近一分鐘的數據做爲樣原本進行計算,注意這裏的1分鐘是指的監控數據中最新的那邊指標的前一分鐘內的數據,而不是當時間源碼分析
前面提過延遲隊列會每15s都會觸發一次HPA的檢測,那若是1分鐘內的監控數據有所變更,則就會產生不少scale更新操做,從而致使對應的控制器的副本時數量的頻繁的變動, 爲了保證對應資源的穩定性, HPA控制器在實現上加入了一個延遲時間,即在該時間窗口內會保留以前的決策建議,而後根據當前全部有效的決策建議來進行決策,從而保證指望的副本數量儘可能小的變動,保證穩定性學習
基礎的概念就先介紹這些,由於HPA裏面主要是計算邏輯比較多,核心實現部分今天代碼量會多一點ui
HPA控制器的實現,主要分爲以下部分:獲取scale對象、根據區間進行快速決策, 而後就是核心實現根據伸縮算法根據當前的metric、當前副本、伸縮策略來進行最終指望副本的計算,讓咱們依次來看下關鍵實現編碼
主要是根據神器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)
區間決策會首先根據當前的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最關鍵的實現部分之一 }
核心決策邏輯主要分爲兩個大的步驟: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
在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 } } }
由於篇幅限制這裏只講述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 } } } }
指望副本的計算實現主要是在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的縮放了,而且以前那些未知的節點依舊宕機,則就要從新進行擴容,這是否是在作無用功呢?
不帶behaviors的決策相對簡單一些,這裏咱們主要聊下帶behavior的決策實現,內容比較多也會分爲幾個小節, 全部實現主要是在stabilizeRecommendationWithBehaviors中
HPA控制器中針對擴容和縮容分別有一個時間窗口,即在該窗口內會盡可能保證HPA擴縮容的最終目標處於一個穩定的狀態,其中擴容是3分鐘,而縮容是5分鐘
if args.DesiredReplicas >= args.CurrentReplicas { // 若是指望的副本數大於等於當前的副本數,則延遲時間=scaleUpBehaviro的穩定窗口時間 scaleDelaySeconds = *args.ScaleUpBehavior.StabilizationWindowSeconds betterRecommendation = min } else { // 指望副本數<當前的副本數 scaleDelaySeconds = *args.ScaleDownBehavior.StabilizationWindowSeconds betterRecommendation = max }
在伸縮策略中, 針對擴容會按照窗口內的最小值來進行擴容,而針對縮容則按照窗口內的最大值來進行
首先根據延遲時間在當前窗口內,按照建議的比較函數去得到建議的目標副本數,
// 過期截止時間 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) } }
在以前進行決策我那次後,會決策出指望的最大值,此處就只須要根據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" }
週期事件是指的在穩定時間窗口內,對應資源的全部變動事件,好比咱們最終決策出指望的副本是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 }
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