KEDA:基于事件驅(qū)動(dòng)擴(kuò)展K8S應(yīng)用的深度實(shí)踐
為什么我們要自動(dòng)擴(kuò)展應(yīng)用程序?
作為 SRE,需要保證應(yīng)用彈性和高可用性。因此,自動(dòng)縮放是我們需要的必須功能。通過自動(dòng)縮放,我們能確保工作負(fù)載能夠高效的地處理業(yè)務(wù)流量。
在本文中,我們將詳細(xì)描述如何使用 KEDA 以事件驅(qū)動(dòng)的方式自動(dòng)擴(kuò)展 Kubernetes 應(yīng)用程序。
什么是KEDA?
KEDA 是一個(gè)輕量級(jí)的開源 Kubernetes 事件驅(qū)動(dòng)的自動(dòng)縮放器,DevOps、SRE 和 Ops 團(tuán)隊(duì)使用它來根據(jù)外部事件或觸發(fā)器水平擴(kuò)展 Pod。KEDA 有助于擴(kuò)展本機(jī) Kubernetes 自動(dòng)縮放解決方案的功能,這些解決方案依賴于標(biāo)準(zhǔn)資源指標(biāo),如 CPU 或內(nèi)存。我們可以將 KEDA 部署到 Kubernetes 集群中,并使用自定義資源定義 (CRD) 管理 Pod 的擴(kuò)展。
KEDA 基于 Kubernetes HPA 構(gòu)建,根據(jù)來自 AWS SQS、Kafka、RabbitMQ 等事件源的信息擴(kuò)展 Pod。這些事件源使用縮放程序進(jìn)行監(jiān)視,縮放程序根據(jù)為其設(shè)置的規(guī)則激活或停用部署。KEDA 縮放器還可以為特定事件源提供自定義指標(biāo),幫助 DevOps 團(tuán)隊(duì)觀察與其相關(guān)的指標(biāo)
我們唯一要做的就是通過選擇要用于自動(dòng)擴(kuò)展應(yīng)用程序的縮放器以及一些參數(shù)來配置 ScaledObject (KEDA CRD),KEDA 將完成剩下的工作:
- 監(jiān)視事件源
- 創(chuàng)建和管理 HPA 生命周期
截至目前,有 62 個(gè)內(nèi)置縮放器和 4 個(gè)外部縮放器可用。 KEDA 之所以好,是因?yàn)槭褂幂p量級(jí)組件以及原生 Kubernetes 組件,例如 HorizontalPodAutoscaler ,更重要的是“即插即用”。
部署KEDA
那么,部署 KEDA 的最簡單方法是使用官方 Helm,安裝如下所示:
helm repo add kedacore
helm repo update
helm install keda kedacore/keda --namespace keda --create-namespace
?? 如果使用 ArgoCD 部署 KEDA,您可能會(huì)遇到有關(guān) CRD 注釋長度的問題。我們可以使用選項(xiàng) ServerSideApply=true 來解決 template.sped.syncPolicy.syncOptions 。此外,還可以在 helm中設(shè)置參數(shù),從而 禁用 CRD 部署,當(dāng)然,這種情況下也必須找到另一種方法來部署 KEDA CRD。
基于本機(jī)Cron Scaler自動(dòng)縮放Web應(yīng)用
下面讓我們深度實(shí)踐一下KEDA!
部署我們的Web應(yīng)用
對于demo,將使用一個(gè)建的 Golang Web 應(yīng)用程序。使用以下清單部署:
---
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配置為僅在工作時(shí)間自動(dòng)擴(kuò)展Web應(yīng)用
如果,我們希望我們的應(yīng)用程序僅在工作時(shí)間可用。至于為何有如此要求,下面是一些場景。例如,在開發(fā)環(huán)境中,不一定需要保持應(yīng)用程序24小時(shí)啟動(dòng)和運(yùn)行。在云環(huán)境中,它可以為您節(jié)省大量成本,具體取決于用戶實(shí)際環(huán)境的應(yīng)用程序/計(jì)算實(shí)例的數(shù)量。 為了實(shí)現(xiàn)這一點(diǎn),我們將使用KEDA的原生Cron scaler。由于 Cron scaler 支持 Linux 格式的 cron,它甚至允許我們在工作日擴(kuò)展我們的應(yīng)用程序 要配置 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 必須與應(yīng)用程序位于同一命名空間中!讓我們深入了解一下這個(gè)配置:
- spec.scaleTargetRef 是 Kubernetes Deployment/StatefulSet 或其他自定義資源的引用
name (必填): Kubernetes 資源的名稱
kind (可選):Kubernetes 資源的種類,默認(rèn)值為 Deployment
- spec.triggers 是用于激活目標(biāo)資源縮放的觸發(fā)器列表
- type (必填):縮放器名稱
- metadata (必需):Cron 縮放器所需的配置參數(shù),使用此配置,我的應(yīng)用程序?qū)⒃谥芤坏街芪宓囊恢苤忻刻斓?08:00 到 18:00 之間啟動(dòng)并運(yùn)行兩個(gè)副本。
基于HTTP事件自動(dòng)縮放Web應(yīng)用
(使用 KEDA HTTP 外部縮放器)
借助所有 KEDA 的縮放器,我們可以通過多種方式自動(dòng)擴(kuò)展 Web 應(yīng)用程序,例如,基于 AMQP 隊(duì)列中的消息進(jìn)行縮放應(yīng)用。
目前,我們了解了 KEDA 的工作原理。我下面?zhèn)儗⑻接?KEDA 如何通過基于 HTTP 事件自動(dòng)擴(kuò)展應(yīng)用程序來幫助我們處理流量高峰。為此,我們有兩個(gè)選擇:
- 使用Prometheus scaler
- 使用 KEDA HTTP 外部縮放器,它的工作方式類似于附加組件,由于演示集群上沒有安裝 Prometheus,因此我們將使用 KEDA HTTP 外部縮放器。
?? KEDA HTTP插件目前處于測試階段。它主要由KEDA團(tuán)隊(duì)維護(hù)。
解決方案概述
KEDA HTTP scaler是構(gòu)建在 KEDA 核心之上的附加組件,它擁有自己的組件:operator, scaler和 interceptor。如果你想進(jìn)一步了解它們的作用,請閱讀官方文檔??傊瑸榱藥椭蠹腋玫乩斫馑墓ぷ髟?,下面提供一個(gè)小案例:
圖片
安裝KEDA HTTP附加組件
由于這個(gè)縮放器不是內(nèi)置的,我們必須手工安裝。根據(jù)官方文檔的說明,我們可以使用 Helm 來進(jìn)行安裝:
helm install http-add-on kedacore/keda-add-ons-http --namespace keda
如果安裝順利,我們應(yīng)該會(huì)看到以下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應(yīng)用配置'HTTPScaledObject'
正如之前所說,KEDA HTTP 附加組件自帶組件,包括操作符,這也意味著它自帶 CRD。HTTPScaledObject 是由 KEDA HTTP 附加組件管理的 CRD。這就是我們在這里需要配置的。讓我們?yōu)?Web 應(yīng)用程序創(chuàng)建 HTTPScaledObject 資源: ?? HTTPScaleObject 必須在與 Web 應(yīng)用相同的命名空間中創(chuàng)建資源!
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
在這里,我們已經(jīng)配置了我們的 HTTPScaledObject 應(yīng)用程序,以便將我們的應(yīng)用程序 Deployment 從 0 個(gè)副本擴(kuò)展到 10 個(gè)副本。因?yàn)?,如果攔截器上有 10 個(gè)請求處于掛起狀態(tài)(應(yīng)用程序尚未接收的請求),則 KEDA 將添加一個(gè) pod。
調(diào)整我們的Web應(yīng)用程序的service和ingress
仔細(xì)觀察一下上面的圖,可以看到我們的 Web 應(yīng)用程序 ingress 需要引用 KEDA HTTP 附加組件的攔截器服務(wù),而不是 Web 應(yīng)用程序的攔截器服務(wù)。由于 ingress 無法引用另一個(gè)命名空間中的服務(wù),因此我們將在與 Web 應(yīng)用相同的命名空間 external 中創(chuàng)建類型服務(wù),該服務(wù)引用來自 keda 命名空間的攔截器服務(wù):
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
現(xiàn)在,我們需要重新配置 Web 應(yīng)用的入口,使其引用新創(chuàng)建的服務(wù):
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
?? 需要輸入新服務(wù)的名稱,但請注意端口,該端口也被攔截器的服務(wù)所取代
讓我們測試一下
為了保證我們的配置正常,本處將使用 [k6]工具 ,這是一個(gè)負(fù)載測試工具。如果想了解有關(guān)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 腳本,后續(xù)將使用它進(jìn)行測試:
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 個(gè) RPS 會(huì)發(fā)生什么:
? 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
?? 如果您想實(shí)時(shí)查看攔截器隊(duì)列中有多少請求,可以在兩個(gè)終端窗格中啟動(dòng)以下命令:
? 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 測試中,應(yīng)用程序沒有縱向擴(kuò)展,因?yàn)閿r截器隊(duì)列中的掛起請求數(shù)不超過 1。提醒一下,我們配置為 targetPendingRequests 10 。所以一切都很正常 ??下面讓我們將 RPS x10 ,看看會(huì)發(fā)生什么:
? 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
結(jié)果還不錯(cuò),我們有兩個(gè)請求 KO ,這是因?yàn)閼?yīng)用程序冷啟動(dòng)(從 0 開始),每個(gè)請求等待的時(shí)間不超過 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
應(yīng)用程序從 0 個(gè)副本擴(kuò)展到 5 個(gè)副本;直到 Web 應(yīng)用程序的掛起請求數(shù)少于 10。
縮放指令非??欤瑧?yīng)用程序很快達(dá)到了 5 個(gè)副本。
以下是 100 RPS 和 1k RPS 測試之間 http_req_duration k6 指標(biāo)的一些對比:
# 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
根據(jù)我們的需求(SLO,SLA等),我們也許可以稍微調(diào)整一下 Web應(yīng)用程序的 targetPendingRequestsHTTPScaledObject 參數(shù) 。
縮放到零!
通過本文介紹的兩個(gè)案例,我們已經(jīng)驗(yàn)證過了如何縮放到零的場景。但是,大家真的知道它是如何工作的嗎?
由于 KEDA 根據(jù)事件自動(dòng)縮放應(yīng)用程序,因此從收到事件的那一刻起,KEDA 會(huì)將應(yīng)用程序縮放到其最小副本。例如,如果我們以 HTTP 附加組件為例,KEDA 將在第一次收到請求時(shí)擴(kuò)展到最小副本。
總結(jié)
KEDA 提供了一個(gè)類似于 FaaS 的事件感知擴(kuò)展模型,在這種模型中,Kubernetes 部署可以基于需求和基于智能動(dòng)態(tài)地從零擴(kuò)展,而不會(huì)丟失數(shù)據(jù)和上下文。在業(yè)務(wù)請求量上來后,應(yīng)用程序?qū)⑦M(jìn)行自動(dòng)化的擴(kuò)容,當(dāng)業(yè)務(wù)低谷的時(shí)候,則會(huì)自動(dòng)的縮容。這可以在緩解很多生產(chǎn)環(huán)境下的手動(dòng)擴(kuò)/縮容操作,以保障用戶的服務(wù)體驗(yàn)。
KEDA 還為 Kubernetes 帶來了更多的事件源。隨著未來更多觸發(fā)器的加入,KEDA 有很大的潛力成為生產(chǎn)級(jí) Kubernetes 部署的必需品,從而使應(yīng)用程序自動(dòng)縮放成為應(yīng)用程序開發(fā)中的嵌入式組件。