KEDA:基于事件驅動擴展K8S應用的深度實踐
為什么我們要自動擴展應用程序?
作為 SRE,需要保證應用彈性和高可用性。因此,自動縮放是我們需要的必須功能。通過自動縮放,我們能確保工作負載能夠高效的地處理業務流量。
在本文中,我們將詳細描述如何使用 KEDA 以事件驅動的方式自動擴展 Kubernetes 應用程序。
什么是KEDA?
KEDA 是一個輕量級的開源 Kubernetes 事件驅動的自動縮放器,DevOps、SRE 和 Ops 團隊使用它來根據外部事件或觸發器水平擴展 Pod。KEDA 有助于擴展本機 Kubernetes 自動縮放解決方案的功能,這些解決方案依賴于標準資源指標,如 CPU 或內存。我們可以將 KEDA 部署到 Kubernetes 集群中,并使用自定義資源定義 (CRD) 管理 Pod 的擴展。
KEDA 基于 Kubernetes HPA 構建,根據來自 AWS SQS、Kafka、RabbitMQ 等事件源的信息擴展 Pod。這些事件源使用縮放程序進行監視,縮放程序根據為其設置的規則激活或停用部署。KEDA 縮放器還可以為特定事件源提供自定義指標,幫助 DevOps 團隊觀察與其相關的指標
我們唯一要做的就是通過選擇要用于自動擴展應用程序的縮放器以及一些參數來配置 ScaledObject (KEDA CRD),KEDA 將完成剩下的工作:
- 監視事件源
- 創建和管理 HPA 生命周期
截至目前,有 62 個內置縮放器和 4 個外部縮放器可用。 KEDA 之所以好,是因為使用輕量級組件以及原生 Kubernetes 組件,例如 HorizontalPodAutoscaler ,更重要的是“即插即用”。
部署KEDA
那么,部署 KEDA 的最簡單方法是使用官方 Helm,安裝如下所示:
helm repo add kedacore
helm repo update
helm install keda kedacore/keda --namespace keda --create-namespace?? 如果使用 ArgoCD 部署 KEDA,您可能會遇到有關 CRD 注釋長度的問題。我們可以使用選項 ServerSideApply=true 來解決 template.sped.syncPolicy.syncOptions 。此外,還可以在 helm中設置參數,從而 禁用 CRD 部署,當然,這種情況下也必須找到另一種方法來部署 KEDA CRD。
基于本機Cron Scaler自動縮放Web應用
下面讓我們深度實踐一下KEDA!
部署我們的Web應用
對于demo,將使用一個建的 Golang Web 應用程序。使用以下清單部署:
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: go-helloworld
name: go-helloworld
spec:
selector:
matchLabels:
app: go-helloworld
template:
metadata:
labels:
app: go-helloworld
spec:
containers:
- image: rg.fr-par.scw.cloud/novigrad/go-helloworld:0.1.0
name: go-helloworld
resources:
requests:
cpu: "50m"
memory: "64Mi"
limits:
memory: "128Mi"
cpu: "100m"
---
apiVersion: v1
kind: Service
metadata:
name: go-helloworld
spec:
selector:
app: go-helloworld
ports:
- protocol: TCP
port: 8080
name: http
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt
name: go-helloworld
spec:
rules:
- host: helloworld.jourdain.io
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: go-helloworld
port:
number: 8080
tls: # < placing a host in the TLS config will indicate a certificate should be created
- hosts:
- helloworld.jourdain.io
secretName: go-helloworld-tls-cert將KEDA配置為僅在工作時間自動擴展Web應用
如果,我們希望我們的應用程序僅在工作時間可用。至于為何有如此要求,下面是一些場景。例如,在開發環境中,不一定需要保持應用程序24小時啟動和運行。在云環境中,它可以為您節省大量成本,具體取決于用戶實際環境的應用程序/計算實例的數量。 為了實現這一點,我們將使用KEDA的原生Cron scaler。由于 Cron scaler 支持 Linux 格式的 cron,它甚至允許我們在工作日擴展我們的應用程序 要配置 Cron scaler,我們將按如下方式使用 [ScaledObject] CRD:
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: go-helloworld
spec:
scaleTargetRef:
name: go-helloworld
triggers:
- type: cron
metadata:
timezone: Europe/Paris
start: 00 08 * * 1-5
end: 00 18 * * 1-5
desiredReplicas: "2"?? ScaledObject 必須與應用程序位于同一命名空間中!讓我們深入了解一下這個配置:
- spec.scaleTargetRef 是 Kubernetes Deployment/StatefulSet 或其他自定義資源的引用
name (必填): Kubernetes 資源的名稱
kind (可選):Kubernetes 資源的種類,默認值為 Deployment
- spec.triggers 是用于激活目標資源縮放的觸發器列表
- type (必填):縮放器名稱
- metadata (必需):Cron 縮放器所需的配置參數,使用此配置,我的應用程序將在周一到周五的一周中每天的 08:00 到 18:00 之間啟動并運行兩個副本。
基于HTTP事件自動縮放Web應用
(使用 KEDA HTTP 外部縮放器)
借助所有 KEDA 的縮放器,我們可以通過多種方式自動擴展 Web 應用程序,例如,基于 AMQP 隊列中的消息進行縮放應用。
目前,我們了解了 KEDA 的工作原理。我下面們將探討 KEDA 如何通過基于 HTTP 事件自動擴展應用程序來幫助我們處理流量高峰。為此,我們有兩個選擇:
- 使用Prometheus scaler
- 使用 KEDA HTTP 外部縮放器,它的工作方式類似于附加組件,由于演示集群上沒有安裝 Prometheus,因此我們將使用 KEDA HTTP 外部縮放器。
?? KEDA HTTP插件目前處于測試階段。它主要由KEDA團隊維護。
解決方案概述
KEDA HTTP scaler是構建在 KEDA 核心之上的附加組件,它擁有自己的組件:operator, scaler和 interceptor。如果你想進一步了解它們的作用,請閱讀官方文檔??傊?,為了幫助大家更好地理解它的工作原理,下面提供一個小案例:
圖片
安裝KEDA HTTP附加組件
由于這個縮放器不是內置的,我們必須手工安裝。根據官方文檔的說明,我們可以使用 Helm 來進行安裝:
helm install http-add-on kedacore/keda-add-ons-http --namespace keda如果安裝順利,我們應該會看到以下pod:
? k get pods -l app=keda-add-ons-http -o name
pod/keda-add-ons-http-controller-manager-5c8d895cff-7jsl8
pod/keda-add-ons-http-external-scaler-57889786cf-r45lj
pod/keda-add-ons-http-interceptor-5bf6756df9-wwff8
pod/keda-add-ons-http-interceptor-5bf6756df9-x8l58
pod/keda-add-ons-http-interceptor-5bf6756df9-zxvw為我們的Web應用配置'HTTPScaledObject'
正如之前所說,KEDA HTTP 附加組件自帶組件,包括操作符,這也意味著它自帶 CRD。HTTPScaledObject 是由 KEDA HTTP 附加組件管理的 CRD。這就是我們在這里需要配置的。讓我們為 Web 應用程序創建 HTTPScaledObject 資源: ?? HTTPScaleObject 必須在與 Web 應用相同的命名空間中創建資源!
kind: HTTPScaledObject
apiVersion: http.keda.sh/v1alpha1
metadata:
name: go-helloworld
spec:
host: "helloworld.jourdain.io"
targetPendingRequests: 10
scaledownPeriod: 300
scaleTargetRef:
deployment: go-helloworld
service: go-helloworld
port: 8080
replicas:
min: 0
max: 10在這里,我們已經配置了我們的 HTTPScaledObject 應用程序,以便將我們的應用程序 Deployment 從 0 個副本擴展到 10 個副本。因為,如果攔截器上有 10 個請求處于掛起狀態(應用程序尚未接收的請求),則 KEDA 將添加一個 pod。
調整我們的Web應用程序的service和ingress
仔細觀察一下上面的圖,可以看到我們的 Web 應用程序 ingress 需要引用 KEDA HTTP 附加組件的攔截器服務,而不是 Web 應用程序的攔截器服務。由于 ingress 無法引用另一個命名空間中的服務,因此我們將在與 Web 應用相同的命名空間 external 中創建類型服務,該服務引用來自 keda 命名空間的攔截器服務:
kind: Service
apiVersion: v1
metadata:
name: keda-add-ons-http-interceptor-proxy
spec:
type: ExternalName
externalName: keda-add-ons-http-interceptor-proxy.keda.svc.cluster.local現在,我們需要重新配置 Web 應用的入口,使其引用新創建的服務:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt
name: go-helloworld
spec:
rules:
- host: helloworld.jourdain.io
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: keda-add-ons-http-interceptor-proxy
port:
number: 8080
tls: # < placing a host in the TLS config will indicate a certificate should be created
- hosts:
- helloworld.jourdain.io
secretName: go-helloworld-tls-cert?? 需要輸入新服務的名稱,但請注意端口,該端口也被攔截器的服務所取代
讓我們測試一下
為了保證我們的配置正常,本處將使用 [k6]工具 ,這是一個負載測試工具。如果想了解有關k6的更多信息,以下是Padok博客中的一些介紹:
- [How to do distributed load testing using K6 & Kubernetes?] (https://www.padok.fr/en/blog/k6-load-testing)
- [k6 description from the Padok tech radar] (https://www.padok.fr/en/tech-radar-resilient?category=resilient&rank=6)
下面本次使用的 k6 腳本,后續將使用它進行測試:
import { check } from 'k6';
import http from 'k6/http';
export const options = {
scenarios: {
constant_request_rate: {
executor: 'constant-arrival-rate',
rate: 100,
timeUnit: '1s', // 100 iterations per second, i.e. 100 RPS
duration: '30s',
preAllocatedVUs: 50, // how large the initial pool of VUs would be
maxVUs: 50, // if the preAllocatedVUs are not enough, we can initialize more
},
},
};
export function test(params) {
const res = http.get('');
check(res, {
'is status 200': (r) => r.status === 200,
});
}
export default function () {
test();
}首先,讓我們看看 100 個 RPS 會發生什么:
? k6 run k6/script.js
/\\ | ̄ ̄| / ̄ ̄/ / ̄ ̄/
/\\ / \\ | |/ / / /
/ \\/ \\ | ( /  ̄ ̄\\
/ \\ | |\\ \\ | ( ̄) |
/ __________ \\ |__| \\__\\ \\_____/ .io
execution: local
script: k6/script.js
output: -
scenarios: (100.00%) 1 scenario, 50 max VUs, 1m0s max duration (incl. graceful stop):
* constant_request_rate: 100.00 iterations/s for 30s (maxVUs: 50, gracefulStop: 30s)
? is status 200
checks.........................: 100.00% ? 3001 ? 0
data_received..................: 845 kB 28 kB/s
data_sent......................: 134 kB 4.5 kB/s
http_req_blocked...............: avg=792.54μs min=0s med=1μs max=137.85ms p(90)=2μs p(95)=2μs
http_req_connecting............: avg=136.6μs min=0s med=0s max=17.67ms p(90)=0s p(95)=0s
http_req_duration..............: avg=11.38ms min=7.68ms med=10.68ms max=100.96ms p(90)=12.78ms p(95)=14.33ms
{ expected_response:true }...: avg=11.38ms min=7.68ms med=10.68ms max=100.96ms p(90)=12.78ms p(95)=14.33ms
http_req_failed................: 0.00% ? 0 ? 3001
http_req_receiving.............: avg=89.68μs min=8μs med=64μs max=6.35ms p(90)=112μs p(95)=134μs
http_req_sending...............: avg=152.31μs min=14μs med=137μs max=2.57ms p(90)=274μs p(95)=313μs
http_req_tls_handshaking.......: avg=587.62μs min=0s med=0s max=74.46ms p(90)=0s p(95)=0s
http_req_waiting...............: avg=11.14ms min=7.62ms med=10.48ms max=100.92ms p(90)=12.47ms p(95)=13.96ms
http_reqs......................: 3001 99.983105/s
iteration_duration.............: avg=12.37ms min=7.73ms med=10.88ms max=194.89ms p(90)=13.07ms p(95)=14.99ms
iterations.....................: 3001 99.983105/s
vus............................: 1 min=1 max=1
vus_max........................: 50 min=50 max=50
running (0m30.0s), 00/50 VUs, 3001 complete and 0 interrupted iterations
constant_request_rate ? [======================================] 00/50 VUs 30s 100.00 iters/s?? 如果您想實時查看攔截器隊列中有多少請求,可以在兩個終端窗格中啟動以下命令:
? kubectl proxy
Starting to serve on 127.0.0.1:8001以及:
? watch -n '1' curl --silent localhost:8001/api/v1/namespaces/keda/services/keda-add-ons-http-interceptor-admin:9090/proxy/queue
{"default/go-helloworld":0}在 100 RPS 測試中,應用程序沒有縱向擴展,因為攔截器隊列中的掛起請求數不超過 1。提醒一下,我們配置為 targetPendingRequests 10 。所以一切都很正常 ??下面讓我們將 RPS x10 ,看看會發生什么:
? k6 run k6/script.js
/\\ | ̄ ̄| / ̄ ̄/ / ̄ ̄/ /\\ / \\ | |/ / / / / \\/ \\ | ( /  ̄ ̄\\ / \\ | |\\ \\ | ( ̄) | / __________ \\ |__| \\__\\ \\_____/ .io
execution: local script: k6/script.js output: -
scenarios: (100.00%) 1 scenario, 50 max VUs, 1m0s max duration (incl. graceful stop): * constant_request_rate: 1000.00 iterations/s for 30s (maxVUs: 50, gracefulStop: 30s)
? is status 200 ? 99% — ? 11642 / ? 2
checks.........................: 99.98% ? 11642 ? 2 data_received..................: 2.6 MB 86 kB/s data_sent......................: 446 kB 15 kB/s dropped_iterations.............: 18356 611.028519/s http_req_blocked...............: avg=1.07ms min=0s med=0s max=408.06ms p(90)=1μs p(95)=1μs http_req_connecting............: avg=43.12μs min=0s med=0s max=11.05ms p(90)=0s p(95)=0s http_req_duration..............: avg=120.09ms min=8.14ms med=74.77ms max=6.87s p(90)=189.49ms p(95)=250.21ms { expected_response:true }...: avg=120.01ms min=8.14ms med=74.76ms max=6.87s p(90)=189.41ms p(95)=249.97ms http_req_failed................: 0.01% ? 2 ? 11642 http_req_receiving.............: avg=377.61μs min=5μs med=32μs max=27.32ms p(90)=758.1μs p(95)=2.49ms http_req_sending...............: avg=61.57μs min=9μs med=45μs max=9.99ms p(90)=102μs p(95)=141μs http_req_tls_handshaking.......: avg=626.79μs min=0s med=0s max=297.82ms p(90)=0s p(95)=0s http_req_waiting...............: avg=119.65ms min=7.95ms med=74.32ms max=6.87s p(90)=188.95ms p(95)=249.76ms http_reqs......................: 11644 387.60166/s iteration_duration.............: avg=121.26ms min=8.32ms med=74.87ms max=7.07s p(90)=189.62ms p(95)=250.28ms iterations.....................: 11644 387.60166/s vus............................: 44 min=25 max=50 vus_max........................: 50 min=50 max=50
running (0m30.0s), 00/50 VUs, 11644 complete and 0 interrupted iterationsconstant_request_rate ? [======================================] 00/50 VUs 30s 1000.00 iters/s結果還不錯,我們有兩個請求 KO ,這是因為應用程序冷啟動(從 0 開始),每個請求等待的時間不超過 1/2 秒。以下是部署歷史記錄:
? k get deployments.apps -w
NAME READY UP-TO-DATE AVAILABLE AGE
go-helloworld 0/0 0 0 36m
go-helloworld 0/1 0 0 36m
go-helloworld 1/1 1 1 36m
go-helloworld 1/4 1 1 36m
go-helloworld 2/4 4 2 36m
go-helloworld 3/4 4 3 36m
go-helloworld 4/4 4 4 36m
go-helloworld 4/5 4 4 37m
go-helloworld 5/5 5 5 37m應用程序從 0 個副本擴展到 5 個副本;直到 Web 應用程序的掛起請求數少于 10。
縮放指令非???,應用程序很快達到了 5 個副本。
以下是 100 RPS 和 1k RPS 測試之間 http_req_duration k6 指標的一些對比:
# 100 RPS
http_req_duration: avg=11.38ms min=7.68ms med=10.68ms max=100.96ms p(90)=12.78ms p(95)=14.33ms
# 1k RPS
http_req_duration: avg=120.09ms min=8.14ms med=74.77ms max=6.87s p(90)=189.49ms p(95)=250.21ms根據我們的需求(SLO,SLA等),我們也許可以稍微調整一下 Web應用程序的 targetPendingRequestsHTTPScaledObject 參數 。
縮放到零!
通過本文介紹的兩個案例,我們已經驗證過了如何縮放到零的場景。但是,大家真的知道它是如何工作的嗎?
由于 KEDA 根據事件自動縮放應用程序,因此從收到事件的那一刻起,KEDA 會將應用程序縮放到其最小副本。例如,如果我們以 HTTP 附加組件為例,KEDA 將在第一次收到請求時擴展到最小副本。
總結
KEDA 提供了一個類似于 FaaS 的事件感知擴展模型,在這種模型中,Kubernetes 部署可以基于需求和基于智能動態地從零擴展,而不會丟失數據和上下文。在業務請求量上來后,應用程序將進行自動化的擴容,當業務低谷的時候,則會自動的縮容。這可以在緩解很多生產環境下的手動擴/縮容操作,以保障用戶的服務體驗。
KEDA 還為 Kubernetes 帶來了更多的事件源。隨著未來更多觸發器的加入,KEDA 有很大的潛力成為生產級 Kubernetes 部署的必需品,從而使應用程序自動縮放成為應用程序開發中的嵌入式組件。

































