KEDA 提供了一个类似于 FaaS 的事件感知扩展模型,在这种模型中,Kubernetes 部署可以基于需求和基于智能动态地从零扩展,而不会丢失数据和上下文。
为什么我们要自动扩展应用程序?
作为 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。提醒一下,我们配置为targetPendingRequests10。所以一切都很正常 😁下面让我们将 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_durationk6 指标的一些对比:
# 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 部署的必需品,从而使应用程序自动缩放成为应用程序开发中的嵌入式组件。
©本文为清一色官方代发,观点仅代表作者本人,与清一色无关。清一色对文中陈述、观点判断保持中立,不对所包含内容的准确性、可靠性或完整性提供任何明示或暗示的保证。本文不作为投资理财建议,请读者仅作参考,并请自行承担全部责任。文中部分文字/图片/视频/音频等来源于网络,如侵犯到著作权人的权利,请与我们联系(微信/QQ:1074760229)。转载请注明出处:清一色财经