[kubernetes系列]HPA模組深度講解

奇犽發表於2018-10-02

一,前言

對於kubernetes基礎性的知識,目前有很多資料,於是不會重複展開,想做一個對每個模組都深入講解的系列,包括基礎使用,原始碼解讀,和實踐中遇到的問題等,所以篇幅很比較長。

二,HPA模組

1,相關說明

(1) kubernetes版本:v1.9.2

(2) 適合對kubernetes基礎有一定了解的人群

2,基本概念和使用

(1) 概念

HPA是kubernetes中自動水平擴充套件模組,基於使用者設定和獲取到的指標(CPU,Memory,自定義metrics),對Pod進行伸縮(不是直接操作pod)。HPA controller屬於Controller Manager的一個controller。

(2) 基本使用

我們可以在pkg/apis/autoscaling 下可以看到,目前是有兩個版本:v1(僅支援CPU指標),v2beta1(支援CPU和Memory和自定義指標)。下面看一下,一個比較全面的hpa的寫法。

kind: HorizontalPodAutoscaler
apiVersion: autoscaling/v2beta1
metadata:
  name: example-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: example-app
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      targetAverageUtilization: 50

  - type: Resource
    resource:
      name: memory
      targetAverageUtilization: 50

  - type: Pods
    pods:
      metricName: receive_bytes_total
      targetAverageValue: 100

  - type: Object
    object:
      target:
        kind: endpoints
        name: example-app
      metricName: request_total
      targetValue: 500m

複製程式碼

3,原始碼分析

kubernetes中的程式碼都是有一定的“套路”(後面會專門寫一篇來深入分析這種“套路”),我們首先從api入手,再到controller

(1) api

這是一個標準的kubernetes的api寫法(可使用官方工具生成),register.go中新增了三個type:HorizontalPodAutoscaler/HorizontalPodAutoscalerList/Scale。接下來看types.go中關於這幾個的定義。對應上面的yaml定義來看。

// 1,HorizontalPodAutoscaler
type HorizontalPodAutoscaler struct {
    ......
	Spec HorizontalPodAutoscalerSpec
	Status HorizontalPodAutoscalerStatus
	......
}
// 使用者設定的值
type HorizontalPodAutoscalerSpec struct {
	MinReplicas *int32  //設定的最小的replicas
	MaxReplicas int32   //設定的最大的replicas
	Metrics []MetricSpec
}
// hpa的目前的狀態
type HorizontalPodAutoscalerStatus struct {
	ObservedGeneration *int64   //觀察的最近的generaction
	LastScaleTime *metav1.Time  //上次伸縮的時間
	CurrentReplicas int32       //目前的replicas數量
	DesiredReplicas int32       //期望的replicas數量
	CurrentMetrics []MetricStatus   //最近一次觀察到的metrics資料
	Conditions []HorizontalPodAutoscalerCondition   //在某個特定點的hpa狀態
}
// Metrics定義
type MetricSpec struct {
	Type MetricSourceType   //metrics type
	Object *ObjectMetricSource  // Object型別的metrics定義
	Pods *PodsMetricSource  // pods型別的metrics定義
	Resource *ResourceMetricSource // Resource型別的metrics定義
}

// 2,Scale 是resources的一次scaling請求,最後hpa都是要使用這個來實現
type Scale struct {
	Spec ScaleSpec         // 期望到達的狀態
	Status ScaleStatus     // 目前的狀態
}

複製程式碼

(2) controller

api定義完後,需要有一個controller來保證系統的狀態能符合我們定義的要求,這時候就需要hpa controller了,hpa controller通過從apiserver中獲取各個指標的值,根據特定的伸縮演算法,來維持在預期的狀態。

上面說過,hpa controller屬於controller manager,於是我們去cmd/kube-controller-manager下,經過一路跟蹤,可以看到hpa controller的啟動邏輯在options/autoscaling.go中

func startHPAController(ctx ControllerContext) (bool, error) {
    // 需要包含v1版本
	if !ctx.AvailableResources[schema.GroupVersionResource{Group: "autoscaling", Version: "v1", Resource: "horizontalpodautoscalers"}] {
		return false, nil
	}
    // 如果要使用自定義metrics,需要開啟該選項
	if ctx.Options.HorizontalPodAutoscalerUseRESTClients {
		return startHPAControllerWithRESTClient(ctx)
	}
    // 從Heapster拉取資料
	return startHPAControllerWithLegacyClient(ctx)
}

func startHPAControllerWithMetricsClient(ctx ControllerContext, metricsClient metrics.MetricsClient) (bool, error) {
    .......
    // 核心引數,根據metrics計算相應的replicas
    replicaCalc := podautoscaler.NewReplicaCalculator(
		metricsClient,
		hpaClient.CoreV1(),
		ctx.Options.HorizontalPodAutoscalerTolerance,
	)
	// 新建HorizontalController
	go podautoscaler.NewHorizontalController(
		hpaClientGoClient.CoreV1(),
		scaleClient, // scale相關客戶端,實現最終的pod伸縮
		hpaClient.AutoscalingV1(),
		restMapper,
		replicaCalc, // 副本計算器
		ctx.InformerFactory.Autoscaling().V1().HorizontalPodAutoscalers(), //infomer
		ctx.Options.HorizontalPodAutoscalerSyncPeriod.Duration, // hpa獲取資料的間隔
		ctx.Options.HorizontalPodAutoscalerUpscaleForbiddenWindow.Duration, // hpa擴容最低間隔
		ctx.Options.HorizontalPodAutoscalerDownscaleForbiddenWindow.Duration, // hpa縮容最低間隔
	).Run(ctx.Stop)
	return true, nil
}

// 接下來看HorizontalController的定義
type HorizontalController struct {
	scaleNamespacer scaleclient.ScalesGetter   // 負責scale的get和update
	hpaNamespacer   autoscalingclient.HorizontalPodAutoscalersGetter // 負責HorizontalPodAutoscaler的Create, Update, UpdateStatus, Delete, Get, List, Watch等
	mapper          apimeta.RESTMapper 
	replicaCalc   *ReplicaCalculator // 負責根據指標計算replicas
	eventRecorder record.EventRecorder  //event記錄
	upscaleForbiddenWindow   time.Duration
	downscaleForbiddenWindow time.Duration
    // 從informer中list/get資料
	hpaLister       autoscalinglisters.HorizontalPodAutoscalerLister
	hpaListerSynced cache.InformerSynced

	queue workqueue.RateLimitingInterface
}
複製程式碼

開始Run後,就是controller開發的那一套流程了,設計到相關的informer,workerqueue就不展開了,最關鍵的是下面的reconcileAutoscaler,其實就是通過一系列演算法調節當前副本數,期望副本數,邊界(最大最小)副本數三者的關係。(接下來分析可能比較長,只擷取部分關鍵程式碼,注意看註釋)

func (a *HorizontalController) reconcileAutoscaler(hpav1Shared *autoscalingv1.HorizontalPodAutoscaler) error {

    // 通過namespace和name獲取對應的scale
	scale, targetGR, err := a.scaleForResourceMappings(hpa.Namespace, hpa.Spec.ScaleTargetRef.Name, mappings)
	// 獲取當前副本
	currentReplicas := scale.Status.Replicas

	rescale := true
    // 副本為0,則不進行scale操作
	if scale.Spec.Replicas == 0 {
		desiredReplicas = 0
		rescale = false
	// 當前副本大於期望的最大副本數量,不進行操作
	} else if currentReplicas > hpa.Spec.MaxReplicas {
		rescaleReason = "Current number of replicas above Spec.MaxReplicas"
		desiredReplicas = hpa.Spec.MaxReplicas
    // 當前副本數小於期望的最小值
	} else if hpa.Spec.MinReplicas != nil && currentReplicas < *hpa.Spec.MinReplicas {
		rescaleReason = "Current number of replicas below Spec.MinReplicas"
		desiredReplicas = *hpa.Spec.MinReplicas
	} 
	// 當前副本為0也不進行操作
	else if currentReplicas == 0 {
		rescaleReason = "Current number of replicas must be greater than 0"
		desiredReplicas = 1
	} 
	// 當前副本數量處於設定的Min和Max之間才進行操作
	else { 
	    // 根據metrics指標計算對應的副本數
		metricDesiredReplicas, metricName, metricStatuses, metricTimestamp, err = a.computeReplicasForMetrics(hpa, scale, hpa.Spec.Metrics)
		rescaleMetric := ""
		// 這裡是防止擴容過快,限制了最大隻能擴當前例項的兩倍
		desiredReplicas = a.normalizeDesiredReplicas(hpa, currentReplicas, desiredReplicas)
        // 限制擴容和縮容間隔,預設是兩次縮容的間隔不得小於5min,兩次擴容的間隔不得小於3min
		rescale = a.shouldScale(hpa, currentReplicas, desiredReplicas, timestamp)
	}

    // 如果上面的限制條件都通過,則進行擴容,擴容注意通過scale實現
	if rescale {
		scale.Spec.Replicas = desiredReplicas
		_, err = a.scaleNamespacer.Scales(hpa.Namespace).Update(targetGR, scale)
	} else {
		desiredReplicas = currentReplicas
	}
}


複製程式碼

(3) 整套流程如下

[kubernetes系列]HPA模組深度講解

4,HPA實戰經驗

雖然說hpa能根據指標對pod進行彈性伸縮,達到根據使用量擴充套件機器的功能,但是,在實際運用中,我發現了以下的問題,希望能給要使用該模組的人帶來一些啟發。

(1) 問題詳情

我們遇到了這樣的一個業務場景:在某個時間段會突然流量劇增十倍,此時由於之前是處於低流量狀態,replicas一直處於較低值,那麼此時擴容由於擴容演算法的限制(最多為2倍),此時擴容的數量是不足夠的。然後,同樣由於擴容演算法的限制,兩次擴容週期預設為不低於三分鐘,那麼將無法在短期內到達一個理想的副本數。此時從監控上看pod的數量圖如下:

[kubernetes系列]HPA模組深度講解

那麼這樣將會造成很大的問題,無法及時處理這種實時性高的業務場景。同時,我們還遇到了這樣的業務情況,在一次大量擴容後,流量劇減,pod數量降到了一個極低值,但是由於出現業務流量的抖動,在接下來很短時間內,再一次出現大流量,此時pod數量無法處理如此高的流量,影響業務的SLA等。

(2) 問題解決思路

1,利用多指標 如果只使用單一指標,例如CPU,整個hpa將嚴重依賴於這項指標,該指標的準確性等直接影響整個hpa。在這裡,我們使用CRD進行了多指標的開發,結合某個業務的具體場景,開發合適的指標,然後結合著CPU等指標一起使用。

2,調整預設引數 預設的擴容和縮容週期不一定是最合適你們的業務的,所以可以根據業務自身的情況進行調整。

3,自行開發hpa controller 這裡還有一個思路是修改hpa controller,但是這樣將會不利於以後的升級。所以可以自行開發hpa controller,自行定義最使用你們業務的擴容縮容演算法即可。但是這樣的開發成本就稍微有點大。

三,注意事項

(1) 支援deployment的滾動升級,不支援RC的滾動升級

四,總結

(1) 如果是我,會如何來設計

提出自己的愚蠢的思路:如果我是hpa這塊的負責人,那麼我將會將擴容和縮容演算法這塊寫成是可擴充套件的,使用者可自定義的擴容縮容步長,可針對每個hpa自定義擴容縮容的時間間隔,這樣將會大大方便使用。

五,新版本特性研究(持續更新)

(1) v1.12.1

該版本對hpa這塊改動挺大,總之增加了很多靈活性和可用性

1,可通過設定HorizontalPodAutoscalerDownscaleStabilizationWindow來防止流量抖動造成pod突然的減少。該版本中會記錄HorizontalPodAutoscalerDownscaleStabilizationWindow時間內的每次擴充套件pod數量,每次取該時間內最大值,然後嘗試儘量到達該值

2,目前對擴容這塊還是有所限制,還是存在兩倍大小的限制,但是取消了擴容和縮容的時間間隔限制

3,可自行設定誤差容忍度

4,增加了pod readiness和missing metrics的考量。如何pod處於not ready狀態或者有丟失的metrics,那麼會將該pod閒置不處理。處理邏輯如下:如果是Resource和Pods的型別,那麼判斷pod是否ready的時候,首先pod的startTime加上cpuInitializationPeriod(可設定)大於當前時間,再判斷pod的狀態和metric獲取的時間,從而來判斷pod是否ready。pod的startTime加上cpuInitializationPeriod小於當前時間的情況下,則根據pod的狀態和pod的啟動時間加上readiness-delay(可設定)來判斷pod是否ready。對於Object和External型別,則直接判斷pod的狀態

六,參考文獻

(1) imkira.com/a24.html

(2) kubernetes.io/docs/tasks/…

相關文章