先發(fā)制人遇事不慌:Kubernetes集群主動擴展?
當集群資源不足時,Cluster Autoscaler會提供新節(jié)點并將其加入集群。使用Kubernetes時你可能會注意到,創(chuàng)建節(jié)點并將其加入集群的過程可能需要花費數(shù)分鐘。在這段時間里,應用程序很容易被連接淹沒,因為已經(jīng)無法進一步擴展了。
虛擬機的配置可能需要花費數(shù)分鐘,在這期間可能無法擴展應用
如何消除如此長的等待時間?
主動擴展(Proactive scaling),或者:
- 理解集群Autoscaler的工作原理并最大限度提升其效用;
- 使用Kubernetes scheduler為節(jié)點分配另一個Pod;以及
- 主動配置工作節(jié)點,以改善擴展效果。
注意:本文涉及的所有代碼都已發(fā)布至LearnK8s GitHub。
Linode可以支持這些解決方案。近期Lincode加入了 Akamai解決方案大家庭,現(xiàn)在注冊Linode,就可免費獲得價值100美元的使用額度,可以隨意使用Linode云平臺提供的各種服務。立即點擊這里了解詳情并注冊吧↓↓↓
Cluster Autoscaler如何在Kubernetes中生效
Cluster Autoscaler在觸發(fā)自動擴展時并不檢查內(nèi)存或CPU的可用數(shù),而是會對事件作出反應,檢查所有不可調(diào)度的Pod。當調(diào)度器找不到能容納某個Pod的節(jié)點時,我們就說這個Pod是不可調(diào)度的。
我們可以這樣創(chuàng)建一個集群來測試看看。
bash
$ linode-cli lke cluster-create \
--label learnk8s \
--region eu-west \
--k8s_version 1.23 \
--node_pools.count 1 \
--node_pools.type g6-standard-2 \
--node_pools.autoscaler.enabled enabled \
--node_pools.autoscaler.max 10 \
--node_pools.autoscaler.min 1 \
$ linode-cli lke kubeconfig-view "insert cluster id here" --text | tail +2 | base64 -d > kubeconfig
請留意下列細節(jié):
- 每個節(jié)點有4GB內(nèi)存和2個vCPU(例如“g6-standard-2”實例)
- 集群中只有一個節(jié)點,并且
- Cluster autoscaler被配置為從1個節(jié)點擴展至10個節(jié)點
我們可以用下列命令驗證安裝已成功完成:
bash
$ kubectl get pods -A --kubecnotallow=kubeconfig
用環(huán)境變量導出kubeconfig文件通常是一種很方便的做法,為此我們可以運行:
bash
$ export KUBECONFIG=${PWD}/kubeconfig
$ kubectl get pods
部署應用程序
讓我們部署一個需要1GB內(nèi)存和250m* CPU的應用程序。
注意:m = 內(nèi)核的千分之一容量,因此250m = CPU的25%容量。
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: podinfo
spec:
replicas: 1
selector:
matchLabels:
app: podinfo
template:
metadata:
labels:
app: podinfo
spec:
containers:
- name: podinfo
image: stefanprodan/podinfo
ports:
- containerPort: 9898
resources:
requests:
memory: 1G
cpu: 250m
用下列命令將資源提交至集群:
bash
$ kubectl apply -f podinfo.yaml
隨后很快會發(fā)現(xiàn)一些情況。首先,三個Pod幾乎會立即開始運行,另有一個Pod處于“未決”狀態(tài)。
隨后很快:
- 幾分鐘后,Autoscaler創(chuàng)建了一個額外的Pod,并且
- 第四個Pod會被部署到一個新節(jié)點中。
最終,第四個Pod被部署到一個新節(jié)點中
第四個Pod為何沒有部署到第一個節(jié)點中?讓我們一起看看已分配的資源。
Kubernetes節(jié)點中資源的分配
Kubernetes集群中部署的Pod會消耗內(nèi)存、CPU以及存儲資源。而且在同一個節(jié)點上,操作系統(tǒng)和Kubelet也需要消耗內(nèi)存和CPU。
在Kubernetes工作節(jié)點上,內(nèi)存和CPU會被拆分為:
- 運行操作系統(tǒng)和系統(tǒng)守護進程(如SSH、Systemd等)所需的資源。
- 運行Kubernetes代理程序(如Kubelet、容器運行時以及節(jié)點故障檢測程序等)所需的資源。
- 可用于Pod的資源。
- 為排空閾值(Eviction threshold)保留的資源。
Kubernetes節(jié)點中分配和保留的資源
如果集群運行了DaemonSet(如kube-proxy),那么可用內(nèi)存和CPU數(shù)量還將進一步減少。
那么我們不妨降低需求,以確保能將所有Pod都放入同一個節(jié)點中:
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: podinfo
spec:
replicas: 4
selector:
matchLabels:
app: podinfo
template:
metadata:
labels:
app: podinfo
spec:
containers:
- name: podinfo
image: stefanprodan/podinfo
ports:
- containerPort: 9898
resources:
requests:
memory: 0.8G # <- lower memory
cpu: 200m # <- lower CPU
我們可以使用下列命令修改這個部署:
bash
$ kubectl apply -f podinfo.yaml
選擇恰當數(shù)量的CPU和內(nèi)存以優(yōu)化實例的運行,這是個充滿挑戰(zhàn)的工作。Learnk8s計算器工具可以幫助我們更快速地完成這項工作。
一個問題解決了,但是創(chuàng)建新節(jié)點花費的時間呢?
遲早我們會需要四個以上的副本,我們是否真的需要等待好幾分鐘,隨后才能創(chuàng)建新的Pod?
簡單來說:是的!Linode必須從頭開始創(chuàng)建和配置新虛擬機,隨后將其連接到集群。這個過程經(jīng)常會超過兩分鐘。
但其實還有替代方案:我們可以在需要時主動創(chuàng)建已經(jīng)配置好的節(jié)點。
例如:我們可以配置讓Autoscaler始終準備好一個備用節(jié)點。當Pod被部署到備用節(jié)點后,Autoscaler可以主動創(chuàng)建另一個備用節(jié)點。然而Autoscaler并沒有內(nèi)置這樣的功能,但我們可以很容易地重新創(chuàng)建。
我們可以創(chuàng)建一個請求數(shù)與節(jié)點資源相等的Pod:
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: overprovisioning
spec:
replicas: 1
selector:
matchLabels:
run: overprovisioning
template:
metadata:
labels:
run: overprovisioning
spec:
containers:
- name: pause
image: k8s.gcr.io/pause
resources:
requests:
cpu: 900m
memory: 3.8G
用下列命令將資源提交至集群:
bash
kubectl apply -f placeholder.yaml
這個Pod完全不執(zhí)行任何操作。
用占位Pod保護節(jié)點上的所有資源
該節(jié)點的作用只是確保節(jié)點能夠被充分使用起來。
隨后還需要確保當工作負載需要擴展時,這個占位Pod能夠被快速清除。為此我們可以使用Priority Class。
yaml
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: overprovisioning
value: -1
globalDefault: false
description: "Priority class used by overprovisioning."
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: overprovisioning
spec:
replicas: 1
selector:
matchLabels:
run: overprovisioning
template:
metadata:
labels:
run: overprovisioning
spec:
priorityClassName: overprovisioning # <--
containers:
- name: pause
image: k8s.gcr.io/pause
resources:
requests:
cpu: 900m
memory: 3.8G
用下列命令將其提交至集群:
bash
kubectl apply -f placeholder.yaml
至此,配置工作已全部完成。
我們可能需要等待一會讓Autoscaler創(chuàng)建節(jié)點,隨后我們將有兩個節(jié)點:
- 一個包含四個Pod的節(jié)點
- 一個包含一個占位Pod的節(jié)點
如果將部署擴展為5個副本會怎樣?是否要等待Autoscaler創(chuàng)建另一個新節(jié)點?
用下列命令測試看看吧:
bash
kubectl scale deployment/podinfo --replicas=5
我們將會看到:
- 第五個Pod會立即創(chuàng)建出來,并在10秒內(nèi)變?yōu)椤罢谶\行”的狀態(tài)。
- 占位Pod會被清除,以便為第五個Pod騰出空間。
占位Pod會被清除,以便為常規(guī)Pod騰出空間
隨后:
- Cluster autoscaler會注意到未決的占位Pod并配置一個新的節(jié)點。
- 占位Pod會被部署到新創(chuàng)建的節(jié)點中。
未決的Pod觸發(fā)了Cluster autoscaler新建節(jié)點
在可以有更多節(jié)點時,為何又要主動創(chuàng)建出一個節(jié)點?
我們可以將占位Pod擴展到多個副本,每個副本都會預配置一個Kubernetes節(jié)點,準備接受標準工作負載。然而這些節(jié)點雖然是閑置的,但它們產(chǎn)生的費用依然會計入云服務賬單。因此一定要慎重,不要創(chuàng)建太多節(jié)點。
將Cluster Autoscaler與Horizontal Pod Autoscaler配合使用
為理解這項技術的含義,我們可以將Cluster autoscaler和Horizontal Pod Autoscaler(HPA)結(jié)合在一起來看。HPA可用于提高部署中的副本數(shù)量。
隨著應用程序收到越來越多流量,我們可以讓Autoscaler調(diào)整處理請求的副本數(shù)量。當Pod耗盡所有可用資源后,會觸發(fā)Cluster autoscaler新建一個節(jié)點,這樣HPA就可以繼續(xù)創(chuàng)建更多副本。
可以這樣新建一個集群來測試上述效果:
bash
$ linode-cli lke cluster-create \
--label learnk8s-hpa \
--region eu-west \
--k8s_version 1.23 \
--node_pools.count 1 \
--node_pools.type g6-standard-2 \
--node_pools.autoscaler.enabled enabled \
--node_pools.autoscaler.max 10 \
--node_pools.autoscaler.min 3 \
$ linode-cli lke kubeconfig-view "insert cluster id here" --text | tail +2 | base64 -d > kubeconfig-hpa
用下列命令驗證安裝過程已成功完成:
bash
$ kubectl get pods -A --kubecnotallow=kubeconfig-hpa
使用環(huán)境變量導出kubeconfig文件是一種方便的做法,為此我們可以運行:
bash
$ export KUBECONFIG=${PWD}/kubeconfig-hpa
$ kubectl get pods
接下來使用Helm安裝Prometheus并查看該部署的相關指標。我們可以在官網(wǎng)上了解安裝Helm的詳細方法。
bash
$ helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
$ helm install prometheus prometheus-community/prometheus
Kubernetes為HPA提供了一個控制器,借此可以動態(tài)增減副本數(shù)量。然
而HPA也有一些局限性:
- 無法拆箱即用。需要安裝Metrics Server來匯總并暴露出指標。
- PromQL查詢無法做到拆箱即用。
好在我們可以使用KEDA,它通過一些實用功能(包括從Prometheus讀取指標)擴展了HPA控制器的用法。KEDA是一種Autoscaler,可適用于下列三個組件:
- Scaler
- Metrics Adapter
- Controller
KEDA架構
我們可以通過Helm安裝KEDA:
bash
$ helm repo add kedacore https://kedacore.github.io/charts
$ helm install keda kedacore/keda
安裝好Prometheus和KEDA之后,來創(chuàng)建一個部署吧。
在這個實驗中,我們將使用一個每秒可以處理固定數(shù)量請求的應用。每個Pod每秒最多可以處理十個請求,如果Pod收到第11個請求,會將請求掛起,稍后再處理。
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: podinfo
spec:
replicas: 4
selector:
matchLabels:
app: podinfo
template:
metadata:
labels:
app: podinfo
annotations:
prometheus.io/scrape: "true"
spec:
containers:
- name: podinfo
image: learnk8s/rate-limiter:1.0.0
imagePullPolicy: Always
args: ["/app/index.js", "10"]
ports:
- containerPort: 8080
resources:
requests:
memory: 0.9G
---
apiVersion: v1
kind: Service
metadata:
name: podinfo
spec:
ports:
- port: 80
targetPort: 8080
selector:
app: podinfo
使用下列命令將資源提交至集群:
bash
$ kubectl apply -f rate-limiter.yaml
為了生成一些流量,我們可以使用Locust。下列YAML定義將創(chuàng)建一個分布式負載測試集群:
yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: locust-script
data:
locustfile.py: |-
from locust import HttpUser, task, between
class QuickstartUser(HttpUser):
@task
def hello_world(self):
self.client.get("/", headers={"Host": "example.com"})
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: locust
spec:
selector:
matchLabels:
app: locust-primary
template:
metadata:
labels:
app: locust-primary
spec:
containers:
- name: locust
image: locustio/locust
args: ["--master"]
ports:
- containerPort: 5557
name: comm
- containerPort: 5558
name: comm-plus-1
- containerPort: 8089
name: web-ui
volumeMounts:
- mountPath: /home/locust
name: locust-script
volumes:
- name: locust-script
configMap:
name: locust-script
---
apiVersion: v1
kind: Service
metadata:
name: locust
spec:
ports:
- port: 5557
name: communication
- port: 5558
name: communication-plus-1
- port: 80
targetPort: 8089
name: web-ui
selector:
app: locust-primary
type: LoadBalancer
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: locust
spec:
selector:
matchLabels:
app: locust-worker
template:
metadata:
labels:
app: locust-worker
spec:
containers:
- name: locust
image: locustio/locust
args: ["--worker", "--master-host=locust"]
volumeMounts:
- mountPath: /home/locust
name: locust-script
volumes:
- name: locust-script
configMap:
name: locust-script
運行下列命令將其提交至集群:
bash
$ kubectl locust.yaml
Locust會讀取下列l(wèi)ocustfile.py文件,該文件存儲在一個ConfigMap中:
py
from locust import HttpUser, task, between
class QuickstartUser(HttpUser):
@task
def hello_world(self):
self.client.get("/")
該文件并沒有什么特別的作用,只是向一個URL發(fā)出請求。若要連接至Locust儀表板,我們需要提供其負載均衡器的IP地址。為此可使用下列命令獲取地址:
bash
$ kubectl get service locust -o jsnotallow='{.status.loadBalancer.ingress[0].ip}'
隨后打開瀏覽器并訪問該IP地址即可。
此外還需要注意一個問題:Horizontal Pod Autoscaler。KEDA autoscaler會用一個名為ScaledObject的特殊對象來封裝Horizontal Autoscaler。
yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: podinfo
spec:
scaleTargetRef:
kind: Deployment
name: podinfo
minReplicaCount: 1
maxReplicaCount: 30
cooldownPeriod: 30
pollingInterval: 1
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus-server
metricName: connections_active_keda
query: |
sum(increase(http_requests_total{app="podinfo"}[60s]))
threshold: "480" # 8rps * 60s
KEDA可以連接由Prometheus收集的指標,并將其發(fā)送給Kubernetes。最后,它還將使用這些指標創(chuàng)建一個Horizontal Pod Autoscaler (HPA)。
我們可以用下列命令手工檢查HPA:
bash
$ kubectl get hpa
$ kubectl describe hpa keda-hpa-podinfo
并使用下列命令提交該對象:
bash
$ kubectl apply -f scaled-object.yaml
接下來可以測試擴展效果了。請在Locust儀表板中用下列設置啟動一項實驗:
- Number of users:300
- Spawn rate:0.4
- Host:http://podinfo
集群和Horizontal pod autoscaler的結(jié)合
可以看到,副本的數(shù)量增加了!
效果不錯,但有個問題不知道你是否注意到。
當該部署擴展到8個Pod后,需要等待幾分鐘,隨后才能在新節(jié)點中創(chuàng)建新的Pod。在這段時間里,每秒處理的請求數(shù)量也不再增加了,因為當前的8個副本每個都只能處理10個請求。
讓我們試試看收縮容量并重復該實驗:
bash
kubectl scale deployment/podinfo --replicas=4 # or wait for the autoscaler to remove pods
這次,我們將用一個占位Pod實現(xiàn)超量配置(Overprovision):
yaml
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: overprovisioning
value: -1
globalDefault: false
description: "Priority class used by overprovisioning."
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: overprovisioning
spec:
replicas: 1
selector:
matchLabels:
run: overprovisioning
template:
metadata:
labels:
run: overprovisioning
spec:
priorityClassName: overprovisioning
containers:
- name: pause
image: k8s.gcr.io/pause
resources:
requests:
cpu: 900m
memory: 3.9G
運行下列命令將其提交至集群:
bash
kubectl apply -f placeholder.yaml
打開Locust儀表板并用下列設置重復實驗:
- Number of users:300
- Spawn rate:0.4
- Host:http://podinfo
在超量配置的情況下進行集群和Horizontal pod autoscaler的結(jié)合
這一次,新節(jié)點將在后臺創(chuàng)建,每秒請求數(shù)量將持續(xù)增減,不會原地踏步。很棒!
總結(jié)
本文介紹了下列內(nèi)容:
- Cluster autoscaler并不追蹤CPU或內(nèi)存用量,而是會監(jiān)控未決的Pod。
- 我們可以用可用內(nèi)存和CPU的總量來創(chuàng)建一個Pod,從而主動配置Kubernetes節(jié)點。
- Kubernetes節(jié)點會為Kubelet、操作系統(tǒng)以及排空閾值保留一定的資源。
- 我們可以結(jié)合使用Prometheus和KEDA,從而通過PromQL查詢擴展自己的Pod。
這篇文章的內(nèi)容感覺還行吧?有沒有想要立即在Linode平臺上親自嘗試一下?別忘了,現(xiàn)在注冊可以免費獲得價值100美元的使用額度,快點自己動手體驗本文介紹的功能和服務吧↓↓↓
歡迎關注Akamai知乎機構號 ,第一時間了解高可用的MySQL/MariaDB參考架構,以及豐富的應用程序示例。