1. 程式人生 > 實用技巧 >kubernetes通過vertica pod autoscaler實現動態垂直縮放

kubernetes通過vertica pod autoscaler實現動態垂直縮放

今天分享一下k8s的垂直縮放這塊,垂直擴容會涉及到request的概念,所以這裡我會多囉嗦一下request到底是怎麼回事和docker的cpu shares又有什麼關係?

垂直容器自動縮放器(VPA)簡單說就是使使用者無需設定最新的資源限制和對容器中容器的要求。
配置後,它將根據使用情況自動設定請求,從而允許在節點上進行適當的排程,以便為每個Pod提供適當的資源量。 它還將保持限制和初始容器配置中指定的請求之間的比率。

它既可以根據資源的使用情況來縮減對資源過度使用的Pod的規模,也可以對資源需求不足的向上擴充套件的Pod的規模進行擴充套件。
自動縮放是使用稱為VerticalPodAutoscaler的自定義資源定義物件配置的。 它允許指定哪些吊艙應垂直自動縮放,以及是否/如何應用資源建議。

簡單來說是 Kubernetes VPA 可以根據實際負載動態設定 pod resource requests。

說到資源限制前面說一下request這塊到底是怎麼回事?

在我們使用kubernetes的過程中,我們知道Pod 是最小的原子排程單位。這也就意味著,所有跟排程和資源管理相關的屬性都應該是屬於 Pod 物件的欄位。而這其中最重要的部分,就是 Pod 的 CPU 和記憶體配置,如下所示:

apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: nginx
  name: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx
        name: nginx
        resources:
          requests:
            memory: "64Mi" 
            cpu: "250m"
          limits:
            memory: "128Mi" 
            cpu: "500m"

在 Kubernetes 中,像 CPU 這樣的資源被稱作“可壓縮資源”(compressible resources)。它的典型特點是,當可壓縮資源不足時,Pod 只會“飢餓”,但不會退出。而像記憶體這樣的資源,則被稱作“不可壓縮資源(incompressible resources)。當不可壓縮資源不足時,Pod 就會因為 OOM(Out-Of-Memory)被核心殺掉。

而由於 Pod 可以由多個 Container 組成,所以 CPU 和記憶體資源的限額,是要配置在每個 Container 的定義上的。這樣,Pod 整體的資源配置,就由這些 Container 的配置值累加得到。

其中,Kubernetes 裡為 CPU 設定的單位是“CPU 的個數”。比如,cpu=1 指的就是,這個 Pod 的 CPU 限額是 1 個 CPU。當然,具體“1 個 CPU”在宿主機上如何解釋,是 1 個 CPU 核心,還是 1 個 vCPU,還是 1 個 CPU 的超執行緒(Hyperthread),完全取決於宿主機的 CPU 實現方式。Kubernetes 只負責保證 Pod 能夠使用到“1 個 CPU”的計算能力。

此外,Kubernetes 允許你將 CPU 限額設定為分數,比如在我們的例子裡,CPU limits 的值就是 500m。所謂 500m,指的就是 500 millicpu,也就是 0.5 個 CPU 的意思。這樣,這個 Pod 就會被分配到 1 個 CPU 一半的計算能力。

當然,你也可以直接把這個配置寫成 cpu=0.5。但在實際使用時,我還是推薦你使用 500m 的寫法,畢竟這才是 Kubernetes 內部通用的 CPU 表示方式。

而對於記憶體資源來說,它的單位自然就是 bytes。Kubernetes 支援你使用 Ei、Pi、Ti、Gi、Mi、Ki(或者 E、P、T、G、M、K)的方式來作為 bytes 的值。比如,在我們的例子裡,Memory requests 的值就是 64MiB (2 的 26 次方 bytes) 。這裡要注意區分 MiB(mebibyte)和 MB(megabyte)的區別。備註:1Mi=10241024;1M=10001000此外,不難看到,Kubernetes 裡 Pod 的 CPU 和記憶體資源,實際上還要分為 limits 和 requests 兩種情況,如下所示:

  • 備註:1Mi=10241024;1M=10001000
  • spec.containers[].resources.limits.cpu
  • spec.containers[].resources.limits.memory
  • spec.containers[].resources.requests.cpu
  • spec.containers[].resources.requests.memory

這兩者的區別其實非常簡單:在排程的時候,kube-scheduler 只會按照 requests 的值進行計算。而在真正設定 Cgroups 限制的時候,kubelet 則會按照 limits 的值來進行設定。更確切地說,當你指定了 requests.cpu=250m 之後,相當於將 Cgroups 的 cpu.shares 的值設定為 (250/1000)*1024。
那麼我們來驗證一下這個cpu-shares具體的值
測試執行一個pod,這裡我給的資源cpu限制是250m

#kubectl describe po nginx-846bc8d9d4-lcrzk |grep -A 2  Requests:
    Requests:
      cpu:        250m
      memory:     64Mi

現在已經交給了cgroups的cpu.shares的值進行配置,如何計算(250/1000)*1024=256
這個256份額值我們可以通過下面的命令docker inspect 格式化直接獲取到,我們現在在docker所看到的256這個值則是pod我們進行request設定的值

#docker ps |grep nginx |awk '{print $1}'|head -1 |xargs docker inspect --format '{{.Id}}:CpuShare={{.HostConfig.CpuShares}}'
b164dc1c62f7eb16a28dc0a14a26e0ef764a7517487d97e9d87883034380302a:CpuShare=256

而當你沒有設定 requests.cpu 的時候,cpu.shares go模版的顯示預設則是 2,但是我們要知道實際上使用這裡預設為1024,每一個啟動的容器份額為1024
只是顯示的是這樣表示
可以通過定位到pod的啟動容器的具體目錄檢視

#cd  /sys/fs/cgroup/cpu/docker
#cat 709e4aeaea9331d09980d6f041e4fc0c8ff78c5d7477825852c076ffcc4fb3d5/cpu.shares 
1024

這樣,Kubernetes 就通過 cpu.shares 完成了對 CPU 時間的按比例分配。
這裡所說的時間分配又說到了cpu分配的優先順序,也就是cpu-shares其實是對cpu使用的一個優先分配的份額,我們知道cpu是可壓縮資源,當分配的時候也決定cpu誰有更快分配CPU的能力,我們可以通過下面的測試來驗證這個cpu-shares

計劃我這裡執行3個容器,為它們提供100、500和1000個cpu共享。

在後面,我們將使用實際的Linux基準測試工具使用自己的工作臺容器進行這些測試。我們將特別關注在非常短的執行時執行這些佔用CPU的系統,並且仍然可以得到準確的結果。

注意,dd、urandom和md5sum也不是工具,只是用來壓測我們的cpu-shares的分配的時間,誰更有優先去分配到cpu的能力

我們的CPU壓力應用程式:時間dd if=/dev/urandom bs=1M count=2 | md5sum

指標解釋:

時間度量執行時間:顯示這3個計時器行
dd if=/dev/urandom bs=1M count=2…複製bs=塊大小1 MB 進行100次
md5sum……計算md5安全雜湊值(給cpu一個負載)
讓我們執行它並調查結果:

docker container run -d --cpu-shares=1024 --name mycpu1024 alpine:3.8 /bin/sh -c 'time dd if=/dev/urandom bs=1M count=100 | md5sum'
docker container run -d --cpu-shares=500 --name mycpu500 alpine:3.8 /bin/sh -c 'time dd if=/dev/urandom bs=1M count=100 | md5sum'
docker container run -d --cpu-shares=100 --name mycpu100 alpine:3.8 /bin/sh -c 'time dd if=/dev/urandom bs=1M count=100 | md5sum'

檢視並獲取我們的容器返回的資料日誌

#docker logs mycpu1024
100+0 records in
100+0 records out
real    0m 0.96s
user    0m 0.00s
sys     0m 0.60s

#docker logs mycpu500
100+0 records in
100+0 records out
real    0m 0.99s
user    0m 0.00s
sys     0m 0.60s
b06118f07ce61d0e5a1201ad31659137  -

#docker logs mycpu100
100+0 records in
100+0 records out
real    0m 1.00s
user    0m 0.00s
sys     0m 0.60s
0046b35a22a48237cac7a75648e4e056  -

注意,所有容器都使用了相同的sys cpu時間這是可以理解的,因為它們都做了完全相同的工作。
--cpu-share =100顯然需要更長的時間,但是--cpu-share =500只比--cpu-share =1024稍微慢一點,這裡我測試顯示的和1024幾乎一樣,這跟測試也存在略微差別
問題是cpu-shares=1024執行非常快,然後退出。
那麼我們可以得到以下結論:
那麼--CPU -shares=500和--CPU -shares=100具有對CPU的完全訪問權。

然後--CPU -shares=500個完成,因為它擁有最多的CPU共享。

然後--CPU -shares=1024快速完成,因為它擁有大多數CPU共享

經過這個測試我希望你明白一個道理,pod進行配置的request資源的限制,其實和docker的cgroup做了同樣的操作,只不過docker將對應的pod的值進行計算得到另外一個值的形式顯示出來,而計算的值則是(250/1000)*1024來計算的,分配的request的值如果沒有限制則具有大多數cpu的共享,可以優先共享cpu資源。

下面limit這裡我也多囉嗦幾句,後面回到主題
而如果你指定了 limits.cpu=500m 之後,則相當於將 Cgroups 的 cpu.cfs_quota_us 的值設定為 (500/1000)*100ms,而 cpu.cfs_period_us 的值始終是 100ms。這樣,Kubernetes 就為你設定了這個容器只能用到 CPU 的 50%。

而對於記憶體來說,當你指定了 limits.memory=128Mi 之後,相當於將 Cgroups 的 memory.limit_in_bytes 設定為 12810241024。
我們可以通過docker 模版檢視

#docker ps |grep nginx
45676528fbea        nginx

我們限制了128Mi則在docker的cgroup這麼計算,12810241024得到cgroup的limit限制則是134217728位元組

#docker ps --quiet --all |xargs docker inspect --format '{{.Id }}:Memory={{.HostConfig.Memory}}'
45676528fbea55a94b80553a8f1c57396c31aecc674b91eddb61024931ac11c9:Memory=134217728

而需要注意的是,在排程的時候,排程器只會使用 requests.memory=64Mi 來進行判斷。
Kubernetes 這種對 CPU 和記憶體資源限額的設計,實際上參考了 Borg 論文中對“動態資源邊界”的定義,既:容器化作業在提交時所設定的資源邊界,並不一定是排程系統所必須嚴格遵守的,這是因為在實際場景中,大多數作業使用到的資源其實遠小於它所請求的資源限額。

基於這種假設,Borg 在作業被提交後,會主動減小它的資源限額配置,以便容納更多的作業、提升資源利用率。而當作業資源使用量增加到一定閾值時,Borg 會通過“快速恢復”過程,還原作業原始的資源限額,防止出現異常情況。而 Kubernetes 的 requests+limits 的做法,其實就是上述思路的一個簡化版:使用者在提交 Pod 時,可以宣告一個相對較小的 requests 值供排程器使用,而 Kubernetes 真正設定給容器 Cgroups 的,則是相對較大的 limits 值。不難看到,這跟 Borg 的思路相通的。

下面回到正題,說說vpa
部署vpa可參考我的github
https://github.com/zhaocheng173/vpa

Kubernetes VPA 包含以下元件:
Recommender:用於根據監控指標結合內建機制給出資源建議值
Updater:用於實時更新 pod resource requests
History Storage:用於採集和儲存監控資料
Admission Controller: 用於在 pod 建立時修改 resource requests