首页 > 数据库 > 数据库开发技术 > 超越CPU和内存:KEDA实现更智能的弹性伸缩
点击上方蓝字 👆关注【云原生百宝箱】公众号,别让消息溜走
后台回复【加群】,与牛人互动不停
Kubernetes现有的弹性伸缩方式过于刚性?基于CPU和内存的指标不够智能?自定义业务监控配置复杂?
不用担心,KEDA来拯救您!基于事件的弹性伸缩,满足精确业务需求,轻松、智能、高效。

目前在Kubernetes中做POD弹性伸缩HPA有基于CPU、memory这种资源指标实现和通过custom-metrics-apiserver和Prometheus-adapter实现的自定义业务监控指标的弹性伸缩,但这些都不是很灵活有效,基于cpu、memory实际对业务带来价值并不大,并不能很准确代表业务实际是否需要扩缩容。基于自定义业务监控指标配置起来相当繁琐。所以社区这些问题背景下开发了KEDA,基于事件驱动的伸缩。
k8s 官方早就推出了 HPA,为什么我们还需要 KEDA 呢?
HPA 在经过三个大版本的演进后当前支持了Resource、Object、External、Pods 等四种类型的指标,演进历程如下:
• autoscaling/v1:只支持基于CPU指标的缩放
• autoscaling/v2beta1:支持Resource Metrics(资源指标,如pod的CPU)和Custom Metrics(自定义指标)的缩放
• autoscaling/v2beta2:支持Resource Metrics(资源指标,如pod的CPU)和Custom Metrics(自定义指标)和 ExternalMetrics(额外指标)的缩放。
如果需要基于其他地方如 Prometheus、Kafka、云供应商或其他事件上的指标进行伸缩,那么可以通过 v2beta2 版本提供的 external metrics 来实现,具体如下:
• 1)通过 Prometheus-adaptor 将从 prometheus 中拿到的指标转换为 HPA 能够识别的格式,以此来实现基于 prometheus 指标的弹性伸缩
• 2)然后应用需要实现 metrics 接口或者对应的 exporter 来将指标暴露给 Prometheus
可以看到, HPA v2beta2 版本就可以实现基于外部指标弹性伸缩,只是实现上比较麻烦,KEDA 的出现主要是为了解决 HPA 无法基于灵活的事件源进行伸缩的这个问题。
毕竟 KEDA 从名字上就体现出了事件驱动。
KEDA 则可以简化这个过程,使用起来更加方便,而且 KEDA 已经内置了几十种常见的 Scaler 可以直接使用。
注意:KEDA 的出现是为了增强 HPA,而不是替代 HPA。
KEDA是红帽和微软联合开发的一个开源项目,后面贡献给了CNCF基金会,目前处于sandbox阶段,实现KEDA的目的主要为了更好和更加灵活使用HPA。
举个简单例子,常见的消息队列系统,有生产者和消费者,生产者往消息队列中吐数据,消费者从消息队列中消费数据,当出现消息堆积时,此时是消费者处理不过来了,应该及时扩容消费者。这就是keda可以实现的,通过对应的接口去查看消息队列系统的情况,进行判断,调用HPA进行扩缩容。
KEDA 监控定义的事件源,并定期检查是否有任何事件。达到设置的阈值后,KEDA 会根据部署的复制件计数设置为 1 或 0,根据配置最小副本计数,激活或停用POD,具体取决于是否有任何事件。
通过上面两幅架构图可以看见keda和HPA是相辅相成的,keda实现的是事件收集和pod副本数0->1,1->0的调整,HPA实现1->n,n->1的POD副本调整。
这里使用 Helm 安装
helm repo add kedacore https://kedacore.github.io/charts
helm repo update
helm install keda kedacore/keda --namespace keda --create-namespace --version v2.9
默认会安装最新版本,对 k8s 版本有要求,当前最新 2.10 需要 k8s 1.24,这边 k8s 是 1.23.6 因此手动安装 KEDA 2.9 版本。
安装完成后会启动两个 pod,能正常启动则算是安装成功。
# kubectl -n keda get po
NAME READY STATUS RESTARTS AGE
keda-operator-94b754f55-4tzqm 1/1 Running 0 21m
keda-operator-metrics-apiserver-655d49f694-7wsww 1/1 Running 0 21m
由于 KEDA 也需要 HPA 配合使用,因此需要安装 metrics-server。
apply 以下 yaml 即可完成 metrics-server 部署
kubectl apply -f metrics-server.yaml
测试使用正常运行
[root@demo ~]# kubectl top node
NAME CPU(cores) CPU% MEMORY(bytes) MEMORY%
demo 585m 14% 4757Mi 60%
部署一个 php-apache 服务作为工作负载
cat > deploy.yaml << EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: php-apache
spec:
selector:
matchLabels:
run: php-apache
replicas: 1
template:
metadata:
labels:
run: php-apache
spec:
containers:
- name: php-apache
image: deis/hpa-example
ports:
- containerPort: 80
resources:
limits:
cpu: 100m
requests:
cpu: 20m
---
apiVersion: v1
kind: Service
metadata:
name: php-apache
labels:
run: php-apache
spec:
ports:
- port: 80
selector:
run: php-apache
EOF
检测 cpu 压力进行扩缩容
cat > so.yaml <<EOF
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: cpu
namespace: default
spec:
scaleTargetRef:
name: php-apache
apiVersion: apps/v1
kind: Deployment
triggers:
- type: cpu
metadata:
type: Utilization
value: "50"
EOF
开始请求,增加压力
clusterIP=$(kubectl get svc php-apache -o jsonpath='{.spec.clusterIP}')
while sleep 0.01; do wget -q -O- http://$clusterIP; done
过一会就能看到 pod 数在增加
[root@mcs-1 keda]# kubectl get po
NAME READY STATUS RESTARTS AGE
php-apache-95cc776df-5grp6 1/1 Running 0 2m52s
php-apache-95cc776df-85jpd 1/1 Running 0 33s
php-apache-95cc776df-cxdlr 1/1 Running 0 48s
php-apache-95cc776df-gr4nh 1/1 Running 0 33s
php-apache-95cc776df-hs5xs 1/1 Running 0 48s
php-apache-95cc776df-jrt7h 1/1 Running 0 48s
php-apache-95cc776df-sddpq 1/1 Running 0 33s
实际上并不是 KEDA 直接调整了 pod 的数量,KEDA 只是创建了一个 HPA 对象出来
相关日志如下
[root@test ~]#kubectl -n keda logs -f keda-operator-94b754f55-4tzqm
2023-05-15T08:53:57Z INFO Creating a new HPA {"controller": "scaledobject", "controllerGroup": "keda.sh", "controllerKind": "ScaledObject", "ScaledObject": {"name":"cpu","namespace":"default"}, "namespace": "default", "name": "cpu", "reconcileID": "8bd04ba9-f100-49c1-9f28-ae28e55ae9eb", "HPA.Namespace": "default", "HPA.Name": "keda-hpa-cpu"}
2023-05-15T08:53:57Z INFO cpu_memory_scaler trigger.metadata.type is deprecated in favor of trigger.metricType {"type": "ScaledObject", "namespace": "default", "name": "cpu"}
查看创建出来的 HPA
[root@mcs-1 ~]# kubectl get hpa NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE keda-hpa-cpu Deployment/php-apache 151%/50% 1 100 6 37m
然后停止请求,等压力下降后,hpa 会自动将 pod 数进行回收。
[root@mcs-1 ~]# kubectl get po
NAME READY STATUS RESTARTS AGE
php-apache-57fcc894d5-6ssc7 1/1 Running 0 78m
[root@mcs-1 ~]# kubectl get hpa
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
keda-hpa-cpu Deployment/php-apache 0%/50% 1 100 1 65m
上一个 Demo 非常简单,其实直接用 HPA 也能实现,并不能完全体现出 KEDA 的作用。
假设,现在我们有这么一个场景,运行的业务是Web应用,使用的是Mysql数据库,然后我们希望根据SQL 查询的结果进行 做一个弹性伸缩。
基于MySQL 进行弹性伸缩示例[1]:https://github.com/turbaszek/keda-example
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
ports:
- port: 3306
targetPort: 3306
selector:
app: mysql
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-pv-claim
labels:
app: mysql
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
name: mysql
labels:
app: mysql
spec:
replicas: 1
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
dnsPolicy: ClusterFirst
containers:
- image: mysql
name: mysql
env:
- name: MYSQL_ROOT_PASSWORD
value: "keda-talk"
ports:
- containerPort: 3306
volumeMounts:
- name: mysql-persistent-storage
mountPath: /var/lib/mysql
volumes:
- name: mysql-persistent-storage
persistentVolumeClaim:
claimName: mysql-pv-claim
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
name: dummy
labels:
app: dummy
spec:
selector:
matchLabels:
app: dummy
strategy:
type: Recreate
template:
metadata:
labels:
app: dummy
spec:
terminationGracePeriodSeconds: 5
containers:
- name: dummy
image: local/dummy
imagePullPolicy: Never
lifecycle:
preStop:
exec:
command:
- pkill -f "sleep"
env:
- name: REDIS_ADDRESS
value: "redis.default:6379"
- name: MYSQL_PASSWORD
value: "keda-talk"
- name: MYSQL_CONN
value: "root@tcp(mysql:3306)/mysql"
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: mysql-scaledobject
namespace: default
labels:
deploymentName: dummy
spec:
maxReplicaCount: 4
pollingInterval: 3 # Optional. Default: 30 seconds
cooldownPeriod: 5 # Optional. Default: 300 seconds
scaleTargetRef:
name: dummy
triggers:
- type: mysql
metadata:
username: "root"
passwordFromEnv: MYSQL_PASSWORD
host: "mysql.default.svc.cluster.local"
dbName: "mysql"
port: "3306"
queryValue: "5"
query: "SELECT CEIL(COUNT(*) / 10) FROM test WHERE state='QUEUED'"
---
• spec 部分包含了 ScaledObject 的规范,定义了如何执行自动扩展。
• type 设置为 mysql,表示使用 MySQL 数据库触发自动扩展。
• metadata 部分包含了与 MySQL 数据库连接相关的信息,如用户名、密码、主机、数据库名、端口等。
• queryValue 和 query 用于定义触发条件。这个示例中,queryValue 设置为 5,query 包含了 SQL 查询,它将返回满足触发条件的结果。在这里,它表示当查询返回值大于等于 5 时触发自动扩展。
• maxReplicaCount 指定了最大副本数。在此示例中,最大副本数为 4。
• pollingInterval 和 cooldownPeriod 分别指定了轮询间隔和冷却期间隔。pollingInterval 表示 KEDA 控制器将多久检查一次触发器以决定是否需要缩放,单位是秒。cooldownPeriod 表示在缩放操作之间需要等待的时间,单位是秒。
• scaleTargetRef 指定了要扩展的目标资源的名称,这里是 dummy。
• triggers 列表定义了触发自动扩展的规则,本示例中只有一个触发器:
这个配置文件告诉 KEDA 控制器如何自动扩展与 MySQL 数据库相关的资源,根据 SQL 查询的结果进行自动伸缩。你可以根据自己的需求自定义触发器和扩展规则,以实现更高效的自动扩展。
KEDA 在 Kubernetes 中扮演三个关键角色:
1. 代理— KEDA 激活和停用 Kubernetes Deployment在没有事件的情况下缩放到零或从零缩放。这是安装 KEDA 时运行的keda-operator容器的主要角色之一。
2. 指标— KEDA 充当Kubernetes 指标服务器[2]它将丰富的事件数据(例如队列长度或流延迟)公开给 Horizontal Pod Autoscaler 以驱动横向扩展。由Deployment直接从源消费事件。指标服务是安装 KEDA 时运行的keda-operator-metrics-apiserver容器的主要角色。
3. 准入 Webhooks - 自动验证资源更改以防止错误配置并通过使用准入控制器实施最佳实践[3]。例如,它将阻止多个 ScaledObject 瞄准相同的缩放目标。
安装 KEDA 时,它会创建四个自定义资源:
1. scaledobjects.keda.sh
2. scaledjobs.keda.sh
3. triggerauthentications.keda.sh
4. clustertriggerauthentications.keda.sh
这些自定义资源使您能够将事件源(以及对该事件源的身份验证)映射到 Deployment、StatefulSet、自定义资源或作业以进行扩展。
• ScaledObjects:表示事件源(例如 Rabbit MQ)和 Kubernetes Deployment、StatefulSet 或定义子资源的任何自定义/scale资源之间所需的映射。
• ScaledJobs:表示事件源和 Kubernetes Job 之间的映射。
• ScaledObject/ScaledJob:还可以引用TriggerAuthentication或 ,ClusterTriggerAuthentication其中包含用于监视事件源的身份验证配置或密钥。
KEDA 由以下组件组成:
• Scaler:连接到外部组件(例如 Prometheus 或者 RabbitMQ) 并获取指标(例如,待处理消息队列大小) )获取指标
• Metrics Adapter:将 Scaler 获取的指标转化成 HPA 可以使用的格式并传递给 HPA
• Controller:负责创建和更新一个 HPA 对象,并负责扩缩到零
• keda operator:负责创建维护 HPA 对象资源,同时激活和停止 HPA 伸缩。在无事件的时候将副本数降低为 0 (如果未设置 minReplicaCount 的话)
• metrics server: 实现了 HPA 中 external metrics,根据事件源配置返回计算结果。
HPA 控制了副本 1->N 和 N->1 的变化。keda 控制了副本 0->1 和 1->0 的变化(起到了激活和停止的作用,对于一些消费型的任务副本比较有用,比如在凌晨启动任务进行消费)
KEDA 工作流程如下:
KEDA 主要工作分为两个部分:
• 1)根据 ScaledObject 对象动态创建并维护一个 HPA 对象以及动态更新 External Metrics 服务里的 http handler
• 2)提供 Scaler,处理由 External Metrics 服务转发过来的指标查询请求,并真正的查询外部系统拿到指标
• 比如指标名为 queueLen 即队列长度,然后 Scaler type 为 kafaka,那么这个 Scaler 可能需要用户提供 kafka endpoint 及认证信息,然后 Scaler 在收到请求时就要根据 ScaledObject name + namespace 拿到 ScaledObject,并提取到相关信息,最终请求 kafka 拿到队列长度返回。
首先创建 ScaledObject 对象,里面会指定 scaler 的类型以及一些访问凭证,例如:下面这个 ScaledObject 就指定了一个 prometheus 类型的 scaler,同时通过 serverAddress 字段指定了 prometheus 的访问地址。
在具体的 scaler 逻辑里,就可以根据这 url 去访问 prometheus 了。
cat > so-prom.yaml << EOF
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: prometheus-core
spec:
scaleTargetRef:
name: php-apache
apiVersion: apps/v1
kind: Deployment
triggers:
- type: prometheus
metadata:
metricName: machine_cpu_cores
serverAddress: http://prometheus-service.monitoring:8080
threshold: '4'
query: machine_cpu_cores
EOF
ScaledObject 对象创建之后 KEDA 会做两件事情:
• 1)根据 ScaledObject 对象中的信息创建一个 HPA 对象
• 2)根据 triggers 里指定的信息,注册一个 External metrics 到 k8s apiserver
具体如下,可以看到创建了一个 HPA 对象
[root@keda ~]# kubectl get hpa
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
keda-hpa-prometheus-core Deployment/php-apache 4/4 (avg) 1 100 1 41m
HPA 内容如下
spec:
maxReplicas: 100
metrics:
- external:
metric:
name: s0-prometheus-machine_cpu_cores
selector:
matchLabels:
scaledobject.keda.sh/name: prometheus-core
target:
averageValue: "4"
type: AverageValue
type: External
minReplicas: 1
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: php-apache
可以看到,所有内容都来源于前面的 ScaledObject 对象。
同时还会注册一个 external metrics 对象
[root@keda ~]# kubectl get apiservices | grep external.metrics.k8s.io
v1beta1.external.metrics.k8s.io keda/keda-operator-metrics-apiserver True 123m
[root@keda ~]# kubectl get apiservice v1beta1.external.metrics.k8s.io -oyaml
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
annotations:
meta.helm.sh/release-name: keda
meta.helm.sh/release-namespace: keda
creationTimestamp: "2023-05-17T08:13:18Z"
labels:
app.kubernetes.io/component: operator
app.kubernetes.io/instance: keda
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: v1beta1.external.metrics.k8s.io
app.kubernetes.io/part-of: keda-operator
app.kubernetes.io/version: 2.9.3
helm.sh/chart: keda-2.9.4
name: v1beta1.external.metrics.k8s.io
resourceVersion: "9353"
uid: 5981633e-3e8a-437f-8b01-533a7ae50502
spec:
group: external.metrics.k8s.io
groupPriorityMinimum: 100
insecureSkipTLSVerify: true
service:
name: keda-operator-metrics-apiserver
namespace: keda
port: 443
version: v1beta1
versionPriority: 100
status:
conditions:
- lastTransitionTime: "2023-05-17T08:33:18Z"
message: all checks passed
reason: Passed
status: "True"
type: Available
访问一下试试
[root@keda ~]# kubectl get --raw "/apis/external.metrics.k8s.io/v1beta1"|jq .
{
"kind": "APIResourceList",
"apiVersion": "v1",
"groupVersion": "external.metrics.k8s.io/v1beta1",
"resources": [
{
"name": "s0-prometheus-machine_cpu_cores",
"singularName": "",
"namespaced": true,
"kind": "ExternalMetricValueList",
"verbs": [
"get"
]
}
]
}
可以看到 resources 里面已经正好就是我们刚才创建的 ScaledObject 对象,访问一下看能拿到具体指标不。
实际上 KEDA 有一个MetricsScaledObjectReconciler 会根据创建的 ScaledObject 对象动态注册 metrics。
具体访问路径见官方文档:keda.sh[4]
语法如下:
kubectl get --raw "/apis/external.metrics.k8s.io/v1beta1/namespaces/YOUR_NAMESPACE/YOUR_METRIC_NAME?labelSelector=scaledobject.keda.sh%2Fname%3D{SCALED_OBJECT_NAME}"
对于上面的 ScaledObject,请求命令如下:
kubectl get --raw "/apis/external.metrics.k8s.io/v1beta1/namespaces/default/s0-prometheus-machine_cpu_cores?labelSelector=scaledobject.keda.sh%2Fname%3Dprometheus-core" | jq .
kubectl get --raw "/apis/external.metrics.k8s.io/v1beta1/namespaces/default/s0-default-hpa-scale-demo?labelSelector=scaledobject.keda.sh%2Fname%3Dhpa-scale-demo" | jq .
返回结果如下
[root@keda ~]# kubectl get --raw "/apis/external.metrics.k8s.io/v1beta1/namespaces/default/s0-prometheus-machine_cpu_cores?labelSelector=scaledobject.keda.sh%2Fname%3Dprometheus-core" | jq .
{
"kind": "ExternalMetricValueList",
"apiVersion": "external.metrics.k8s.io/v1beta1",
"metadata": {},
"items": [
{
"metricName": "s0-prometheus-machine_cpu_cores",
"metricLabels": null,
"timestamp": "2023-05-17T10:42:42Z",
"value": "4"
}
]
}
请求过来的时候 keda 会将请求转发到具体的 scaler 里,scaler 就根据参数请求外部系统拿到真正的指标并返回到 HPA,HPA 则根据当前指标和阈值判断扩容。
在指标达到阈值时,我们希望能够快速加副本,而不用长时间等待。
在指标低于阈值时,我们希望能够延缓缩容副本,避免因指标抖动,导致副本同步抖动。
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: nginx-deployment-scaledobject
namespace: default
spec:
scaleTargetRef:
name: nginx-deployment
pollingInterval: 15
cooldownPeriod: 30
minReplicaCount: 0
maxReplicaCount: 20
advanced:
horizontalPodAutoscalerConfig:
behavior:
scaleUp:
stabilizationWindowSeconds: 15
policies:
- type: Pods
value: 5
periodSeconds: 15
scaleDown:
stabilizationWindowSeconds: 60
policies:
- type: Pods
value: 5
periodSeconds: 15
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus-server.monitor.svc:80
metricName: nginx_server_requests
threshold: '5'
query: sum (irate(nginx_server_requests{code="total", host="*"}[1m]))/60
在 advanced 参数中有:
• stabilizationWindowSeconds: 指标稳定时间,也就是指标达到阈值后,需要持续多久才会触发伸缩
• scaleUp: 扩容策略
• scaleDown: 缩容策略
• policies: 策略列表,periodSeconds 指的是每隔多久执行一次策略,value 指的是每次执行策略时,增加或减少的 Pod 数量
这里的意思是: 扩容时,持续 15s 就会触发伸缩,每隔 15s 扩容 5 个 Pod;缩容时,持续 60s 才会触发伸缩,每隔 15s 缩容 5 个 Pod。
最终的效果如下:
扩容时,指标与 Pod 数量同步增加;缩容时,指标先下降,然后 Pod 数量才下降。
虽然我们设置了 minReplicaCount 为 0,但从监控数据看到 Pod 副本数并没有降为 0,
这其实就涉及到指标可能的误差。监控系统的可用性优先级是高于一致性、准确性的,是容忍一定的误差的。
如下图,我们可以看到在没有请求时,指标也不是 0,才导致 Pod 副本数没有降为 0。
解决办法很简单,就是在指标中减去误差值即可。
sum (irate(nginx_server_requests{code="total", host="*"}[1m]))/60 - 0.1
指标查询的结果为 0 或者负数时,KEDA 会将 Pod 副本数设置为 minReplicaCount 值。
总的感受下来,还是非常好用的,像之前配一些业务自定义指标的HPA监控还需要配置Prometheus-adapter 配置规则,但通过keda直接可以对接Prometheus,配置对应表达式尽快。其他一些集成的事件触发器也直接使用就可以了。
keda支持的业务事件触发器
本文主要演示了 KEDA 的使用,同时也分析了其具体的工作流程。
KEDA 主要是为了解决 HPA 无法基于灵活的外部事件来实现弹性伸缩这个问题。
KEDA 内置了多种 Scaler 以满足不同场景的使用需求,简化了 基于 HPA + Prometheus Adaptor + XXExporter 的自定义流程。
理解 KEDA 的工作流程之后就会比较清晰了,再贴一下这个图
在创建一个 ScalerObject 之后, KEDA controller 就会动态注册一个 handler 到 external.metrics.k8s.io 这个服务上,最终 HPA 请求指标时则会根据路由转发到具体的 Scaler 上,Scaler 再根据 ScalerObject 中的配置请求外部系统拿到数据并将其转换为 HPA 能够识别的格式,最终返回给 HPA。
[1] 基于MySQL 进行弹性伸缩示例: https://github.com/turbaszek/keda-example[2] Kubernetes 指标服务器: https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/#support-for-custom-metrics[3] 准入控制器实施最佳实践: https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/[4] keda示例: https://github.com/kedacore/samples
原文作者:云原生AI百宝箱;如有侵权,请联系管理员删除。