分享一下Kubernetes(k8s)動態垂直縮放的相關知識

今天分享一下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=1024 1024;1M=1000 1000此外,不難看到,Kubernetes 裏 Pod 的 CPU 和內存資源,實際上還要分爲 limits 和 requests 兩種情況,如下所示:

  • 備註:1Mi=1024 1024;1M=1000 1000
  • 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 設置爲 128 1024 1024。

我們可以通過docker 模版查看

#docker ps |grep nginx
45676528fbea        nginx

我們限制了128Mi則在docker的cgroup這麼計算,128 1024 1024得到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

Kubernetes VPA 包含以下組件:

Recommender:用於根據監控指標結合內置機制給出資源建議值

Updater:用於實時更新 pod resource requests

History Storage:用於採集和存儲監控數據

Admission Controller: 用於在 pod 創建時修改 resource requests

它的架構是這樣的

vpa的實現的主要流程它是這樣的:

Recommender在啓動時從History Storage獲取歷史數據,根據內置機制修改VPA API object資源建議值。Updater監聽VPA API object,依據建議值動態修改 pod resource requests。VPA Admission Controller則是用於 pod 創建時修改 pod resource requests。History Storage則是通過Kubernetes Metrics API採集和存儲監控數據

現在去檢查Vertical Pod Autoscaler是否在您的集羣中完全正常運行的一種簡單方法是創建示例部署和相應的VPA配置:

---
apiVersion: "autoscaling.k8s.io/v1beta2"
kind: VerticalPodAutoscaler
metadata:
  name: hamster-vpa
  namespace: kube-system
spec:
  targetRef:
    apiVersion: "apps/v1"
    kind: Deployment
    name: hamster
  resourcePolicy:
    containerPolicies:
      - containerName: '*'
        minAllowed:
          cpu: 100m
          memory: 50Mi
        maxAllowed:
          cpu: 1
          memory: 500Mi
        controlledResources: ["cpu", "memory"]
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hamster
  namespace: kube-system
spec:
  selector:
    matchLabels:
      app: hamster
  replicas: 2
  template:
    metadata:
      labels:
        app: hamster
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 65534 # nobody
      containers:
        - name: hamster
          image: nginx
          resources:
            requests:
              cpu: 100m
              memory: 50Mi
          command: ["/bin/sh"]
          args:
            - "-c"
            - "while true; do timeout 0.5s yes >/dev/null; sleep 0.5s; done"

kubectl create -f examples/hamster.yaml

上面的命令創建了一個包含2個Pod的部署,每個Pod運行一個請求100m的容器,並嘗試使用最高不超於500m的容器。該命令還會創建一個指向部署的VPA配置。VPA將觀察Pod的行爲,大約5分鐘後,它們應使用更高的CPU請求進行更新(請注意,VPA不會在部署中修改模板,但Pod的實際請求會被更新)。

要查看VPA配置和當前推薦的資源請求,可以運行以下命令:

#kubectl get vpa -A
NAMESPACE     NAME          AGE
kube-system   hamster-vpa   15m

現在我們的默認值是以下配置
            requests:
              cpu: 100m
              memory: 50Mi

大概等待60s之後pod會重建,來獲取推薦的request的值

#kubectl describe vpa nginx-deployment-basic -n kube-system
    resources:
      requests:
        cpu: 587m
        memory: 262144k

這個值獲取到這裏並分爲4個指標建議

1、 Lower Bound: 也就是最低的下限,爲cpu 25m,memory爲262144k

2、Target: 目標我理解爲也就是平均值,爲cpu25m,memory爲262144k vpa的作者也是建議使用target推薦的值

3、Uncapped Target無上限,爲cpu25m,memory爲262144k

4、Upper Bound 上限,爲cpu2651m,也就是2.651個cpu

Recommendation:
    Container Recommendations:
      Container Name:  nginx
      Lower Bound:
        Cpu:     25m
        Memory:  262144k
      Target:
        Cpu:     25m
        Memory:  262144k
      Uncapped Target:
        Cpu:     25m
        Memory:  262144k
      Upper Bound:
        Cpu:     2651m
        Memory:  2771500k

那麼這種情況下我們操作會導致我們的pod會重啓,也會將pod調度到其他的節點,對於不設置任何調度規則的話,這樣對於我們的業務肯定會受到影響

需要注意以下的在生產中使用時的注意事項

VPA不會驅逐沒有在副本控制器管理下的Pod。目前對於這類Pod,Auto模式等同於Initial模式。

目前VPA不能和監控CPU和內存度量的Horizontal Pod Autoscaler (HPA)同時運行,除非HPA只監控其他定製化的或者外部的資源度量。

VPA使用admission webhook作爲其准入控制器。如果集羣中有其他的admission webhook,需要確保它們不會與VPA發生衝突。准入控制器的執行順序定義在API Server的配置參數中。

VPA會處理絕大多數OOM(Out Of Memory)的事件,但不保證所有的場景下都有效。

VPA的性能還沒有在大型集羣中測試過。

VPA對Pod資源requests的修改值可能超過實際的資源上限,例如節點資源上限、空閒資源或資源配額,從而造成Pod處於Pending狀態無法被調度。同時使用集羣自動伸縮(ClusterAutoscaler)可以一定程度上解決這個問題。

多個VPA同時匹配同一個Pod會造成未定義的行爲。

VPA在三種模式下運行:

"Auto":VPA會在吊艙創建時分配資源請求,並使用首選更新機制在現有吊艙上更新資源請求。當前,這等效於"Recreate"(請參見下文)。一旦有可用的Pod請求免費重啓(“就地”)更新,該"Auto"模式就可以將其用作首選更新機制。注意: VPA的此功能是試驗性的,可能會導致應用程序停機。

"Recreate":當請求的資源與新建議明顯不同(尊重Pod中斷預算,如果已定義)時,VPA會在創建廣告連播時分配資源請求,並通過驅逐它們來更新現有的廣告連播。僅當您需要確保每當資源請求更改時都重新啓動Pod時,才應很少使用此模式。否則,請選擇一種"Auto"模式,該模式可在可用時重新啓動免費更新。注意: VPA的此功能是試驗性的,可能會導致您的應用程序停機。

"Initial":VPA僅在吊艙創建時分配資源請求,以後再也不會更改它們。

"Off":VPA不會自動更改容器的資源要求。將計算建議,並可以在VPA對象中對其進行檢查。

我們其實要做的就是不採用Auto的形式,只通過推薦參考的形式,將歷史給出的target作爲我們項目參考的request值

現在運行一個redis的示例,並獲取vpa推薦的值

---
apiVersion: autoscaling.k8s.io/v1beta2
kind: VerticalPodAutoscaler
metadata:
  name: redis-vpa
spec:
  targetRef:
    apiVersion: "apps/v1"
    kind: Deployment
    name: redis-master
  updatePolicy:
    updateMode: "Off"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis-master
  labels:
    app: redis
spec:
  selector:
    matchLabels:
      app: redis
  replicas: 3
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
        - name: master
          image: redis  # or just image: redis
          ports:
            - containerPort: 6379

運行完之後,我們可以看到已經幫我們將值獲取出來,可以通過describe進行查看

[root@master examples]# kubectl get vpa
NAME          AGE
hamster-vpa   53m
redis-vpa     90s
[root@master examples]# kubectl describe vpa redis-vpa
  Recommendation:
    Container Recommendations:
      Container Name:  master
      Lower Bound:
        Cpu:     25m
        Memory:  262144k
      Target:
        Cpu:     25m
        Memory:  262144k
      Uncapped Target:
        Cpu:     25m
        Memory:  262144k
      Upper Bound:
        Cpu:     15077m
        Memory:  7538772727

最後我們就可以根據推薦值來實際配置Deployment中資源的requests。VPA會持續的監控應用資源的使用情況,並提供優化建議。

通過將target的值可以根據換算進行使用到我們的生產環境當中

來源:https://www.tuicool.com/articles/INfeQvR
歡迎關注微信公衆號【慕容千語】

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