k8s 自動擴縮容HPA原理及adapter配置詳解?

藍胖子的程式設計夢發表於2023-09-22

大家好,我是藍胖子,都知道,k8s擁有自動擴縮容機制HPA,我們能夠透過配置針對不同的擴縮容場景進行自動擴縮容,往往初學者在面對其中繁多配置的時候會學了又忘記,今天我將會以一種不同的視角,結合api server 請求 來探索這部分的配置,看完本篇,應該會對擴縮容這部分配置會有更深的理解。

自動擴縮容架構圖

image.png

我們先來看一下自動擴縮容的原理,在k8s中HPA這個模組的邏輯會定時請求api server 獲取相應的pod或者CRD或者其他資源的指標資訊,這些指標資訊是使用者建立HPA的yaml配置檔案時指定的。

api server收到請求後,根據請求的api group,api version 轉發給內部的api service服務進行處理,當我們想讓k8s借用prometheus的相關指標進行擴縮容時,就需要在叢集裡用api service的方式安裝prometheus adapter,它會將發往api server的請求經過包裝,轉發到prometheus伺服器獲取對應指標資訊,然後將結果經過封裝返回給客戶端即HPA模組。HPA模組收到指標後,在根據自身配置檔案中的target值判斷是否需要進行自動擴縮容。

api server 處理請求的方式

既然提到了prometheus adapter是以api service 方式安裝到k8s叢集中的,我再對api server的架構已經處理請求的方式再闡述下。

api server 處理請求的方式是鏈式的,你可以簡單的理解為api server裡有多個http server ,當某個請求的路徑不屬於某個http server處理範疇內的話,會將這個請求委託給下一個http server進行處理。同時,k8s允許使用者自定義api service作為http server,prometheus adapter 就是一個自定義的api service。

api server 請求路徑格式

向api server傳送http請求,請求格式是按一定規則進行組裝的,我主要檢視了HPA模組原始碼,所以拿這塊去舉例,hpa發往api server的請求是將api version和api group 以及要請求資源的名稱空間,資源名拼接到一起組成的路徑。如下:

image.png

不同的HPA指標型別這個路徑的拼接會有所不同(下面會詳細講到),但是整體的api風格是和這個一致的。

當HPA在向api server傳送請求的時候則是根據不同的擴縮容指標型別選擇了不同的api group 去傳送請求。

❗️❗️? 注意,這裡HPA選擇的api group是k8s這部分程式碼已經固定好了的,所以prometheus adapter在以api service安裝時指定的api group 需要和這裡吻合。目前針對指標型別,HPA會從metrics.k8s.io, customer.metrics.k8s.io, external.metrics.k8s.io這3個api group種選取對應的group。

HPA 擴縮容的4種指標型別

接下來,我們來詳細看下,HPA擴縮容的4種指標型別。

Pods

先看第一種pods型別,它表示的是由pod產生的指標, 其在HPA宣告的配置yaml檔案裡寫法如下,

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
	name: sample-app
	namespace: default
spec:
	maxReplicas: 10
	minReplicas: 2
	metrics:
	  - pods:
		   metric:
			  name: http_requests
			  selector:
		        matchLabels:
		          <label-key>: <label-value>
		   target:
			  averageValue: 500m
			  type: AverageValue
		type: Pods
scaleTargetRef:
	apiVersion: apps/v1
	kind: Deployment
	name: sample-app

可以看到spec.metrics.type 值為pods型別,HPA的pods 指標型別 是指pod這個資源物件產生的指標,其中定義了指標名為http_requests,最終發往api server的url path如下所示,

/apis/custom.metrics.k8s.io/v1beta1/namespaces/default/pods/*/http_requests?labelSelector=<label-key>=<label-value>

api server 收到這個請求後會將請求轉發給prometheus adapter,那麼prometheus adapter 又是如何將http_requests 與具體的prometheus中的指標對應起來的呢?

prometheus adapter在啟動的時候我們會配置一個規則配置檔案,在這個檔案定義了這個對映關係,下面是這針這種型別的指標配置規則部分,

rules:
- seriesQuery: 'http_requests_total{}'
  resources:
    overrides:
      kubernetes_namespace: {resource: "namespace"}
      kubernetes_pod_name: {resource: "pods"}
  name:
    matches: "http_requests_total"
    as: "http_requests"
  metricsQuery: 'sum(rate(<<.Series>>{<<.LabelMatchers>>}[2m])) by (<<.GroupBy>>)'

我們將hpa配置檔案和發往api server的請求以及prometheus adapter的規則檔案結合起來,看看prometheus adapter 規則檔案裡那些模板變數的含義。

首先是hpa的配置檔案中指定了metric.name是http_requests,http_requests在prometheus adapter的配置檔案裡是將prometheus的http_requests_total與之對應了起來,並且從規則配置檔案的resources.overrides 配置中可以發現namespace和pods資源在指標http_requests_total中會有kubernetes_namespace和kubernetes_pod_name標籤與之對應,這層關係其實主要是為了metricsQuery 中模板變數的替換。

metricsQuery 中 <<.Series>> 其實就是seriesQuery這部分。

<<.LabelMatchers>> 是篩選指標時的標籤,在hpa裡面我們指定了metric.selector,發往api server的請求裡的引數labelSelector就會替代<<.LabelMatchers>>模板變數,同時發往api server請求中的namespace的值也會寫到<<.LabelMatchers>>中,並且namespace的對應標籤名就是resources.overrides中定義的kubernetes_namespace。

<<.GroupBy>> 變數在這裡會將k8s的resource資源型別作為分組的維度,並且在這個場景下,在發往api server的請求中,k8s的資源是pods型別,而pods型別在指標中的標籤名是kubernetes_pod_name。

所以最終,在prometheus 中進行查詢時執行的promql語句為,

sum(rate(http_requests_total{"kubernetes_namespace":"default","<label-key>":"<label-value>"}[2m])) by (kubernetes_pod_name)

Object

在看了pods型別的hpa指標後,我們再來看看Object型別的指標是如何配置的,因為在k8s裡資源型別除了pod型別,還有其他型別,所以如果由其他資源型別產生的指標,則由Object來表示。

先看下hpa的yaml配置檔案是如何寫的。

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
	name: sample-app
	namespace: default
spec:
	maxReplicas: 10
	minReplicas: 2
	metrics:
	 - object:  
		metric:  
		  name: requests-per-second  
		describedObject:  
		  apiVersion: extensions/v1beta1  
		  kind: Ingress  
		  name: main-route  
		target:  
		  type: Value  
	      value: 2k
	   type: Object 
scaleTargetRef:
	apiVersion: apps/v1
	kind: Deployment
	name: sample-app

發往api server的請求格式如下

/apis/custom.metrics.k8s.io/v1beta1/namespaces/default/ingress/main-route/requests-per-second

prometheus adapter配置此類規則和Pods型別類似,模板變數解析方式也是類似,唯一有點不一樣的是此時的<<.GroupBy>> 模板變數會被ingress/main-route 也就是資源名加上資源示例名替代。

Resource, ContainerResource

接著看一下hpa中的Resource型別的指標配置,它表示對pod的cpu或者記憶體值來進行擴縮容,本質上可以用Pods 型別的配置來代替這部分配置,那為什麼還有Resource型別呢?因為Resource出來的時候還沒有Pods 型別。

Kubernetes 1.20 在 HorizontalPodAutoscaler (HPA) 中引入了 ContainerResource 型別指標,不論是Resource還是ContainerResource都只能對cpu和記憶體這兩個維度進行監控,它們的區別如下,

Resource 計算pod的資源使用率是

sum{每個容器的資源使用量} / sum{每個容器的資源請求}

但是一個pod有多個容器,可能會出現單個容器資源使用率高,但是平均下來每個容器資源使用率低的情況,而ContainerResource 則能夠指定以pod中的哪個容器拿來計算擴容指標,能夠提供更準確的擴容機制。

ContainerResource在hpa的yaml配置檔案中配置如下,其中container標籤指明瞭容器名稱。

type: ContainerResource
containerResource:
  name: cpu
  container: application
  target:
    type: Utilization
    averageUtilization: 60

而Resource型別的擴容指標則是針對pod中所有容器計算指標,

type: Resource
resource:
  name: cpu
  target:
    type: Utilization
    averageUtilization: 60

它們發往api server 的請求格式如下

/apis/metrics.k8s.io/v1beta1/namespaces/default/pods

注意,這裡的配置檔案沒有加上selector,實際上我們平時寫配置檔案的時候肯定是有selector的,所以發往api server的請求也會有selector的引數

prometheus adapter 在收到這個請求後,會將對應的pod的cpu和記憶體資訊全部返回,然後k8s的hpa模組篩選其需要用到的部分,像ContainerResource就會篩選返回結果中和container標籤值代表的容器名稱一樣的指標進行計算。

prometheus adapter針對此型別的指標規則配置如下, 其中的containerLabel 表明了指標中容器名稱是用哪個標籤表示的,此時的<<.GroupBy>>模板變數 會由pod資源名稱和容器名標籤兩個維度替代。

以下是prometheus adapter官方給出的配置模版,自己配置的時候需要改掉實際查詢的指標名已經標籤名等。

"resourceRules":  
  "cpu":  
    "containerLabel": "container"  
    "containerQuery": |  
      sum by (<<.GroupBy>>) (  
        irate (  
            container_cpu_usage_seconds_total{<<.LabelMatchers>>,container!="",pod!=""}[4m]  
        )  
      )  
    "nodeQuery": |  
      sum by (<<.GroupBy>>) (  
        irate(  
            node_cpu_usage_seconds_total{<<.LabelMatchers>>}[4m]  
        )  
      )  
    "resources":  
      "overrides":  
        "namespace":  
          "resource": "namespace"  
        "node":  
          "resource": "node"  
        "pod":  
          "resource": "pod"  
  "memory":  
    "containerLabel": "container"  
    "containerQuery": |  
      sum by (<<.GroupBy>>) (  
        container_memory_working_set_bytes{<<.LabelMatchers>>,container!="",pod!=""}  
      )  
    "nodeQuery": |  
      sum by (<<.GroupBy>>) (  
        node_memory_working_set_bytes{<<.LabelMatchers>>}  
      )  
    "resources":  
      "overrides":  
        "node":  
          "resource": "node"  
        "namespace":  
          "resource": "namespace"  
        "pod":  
          "resource": "pod"  
  "window": "5m"

External

最後,我們來看下external 型別的擴容指標如何配置,上面講到的hpa 擴縮容指標型別都是在k8s叢集裡產生的指標,它們都限定在了一個namespace裡面,除此以外,hpa模組還允許配置第三方的指標型別,比如叢集外部的訊息佇列產生的指標,這型別的指標被稱作External型別。 在hpa裡配置案例如下,

type: External  
external:  
	metric:  
		name: queue_messages_cnt  
		selector:  
			matchLabels:  
				app: "lanpangzi"  
		# External指標型別下只支援Value和AverageValue型別的目標值  
	target:  
		type: AverageValue  
		averageValue: 30

發往api server的請求格式如下

/apis/external.metrics.k8s.io/v1beta1/namespaces/default/queue_messages_cnt

由於外部指標和namespace無關,所以在配置prometheus adapter的規則配置檔案的時候,指定下指標是namespace無關的。

externalRules:  
- seriesQuery: 'queue_messages_cnt'  
resources:  
	namespaced: false  
name:  
	matches: 'queue_messages_cnt' 
	as: 'queue_messages_cnt'  
metricsQuery: avg(<<.Series>>{<<.LabelMatchers>>})  

總結

探索HPA配置的含義過程中,其實可以發現k8s在針對HPA擴容依據的擴充方式上,就是規定了3組api group(metrics.k8s.io,external.metrics.k8s.io,custom.metrics.k8s.io),並且用基本一致的http請求,讓第三方(prometheus adapter)在宣告為api service 的時候指定為對應的api group,然後解析請求路徑和引數來進而對prometheus查詢 即完成了對HPA擴容指標的查詢。

關於prometheus adapter更多的配置案例建議直接看prometheus adapter的doc目錄下的示例。

相關文章