Kubernetes教程(十八)--- KEDA:基于事件驱动的自动弹性伸缩
本文主要记录了如何使用 KEDA 对应用进行弹性伸缩,同时分析了 KEDA 工作原理及其大致实现。
1. 什么是 KEDA
KEDA:Kubernetes-based Event Driven Autoscaler.
KEDA是一种基于事件驱动对 K8S 资源对象扩缩容的组件。非常轻量、简单、功能强大,不仅支持基于 CPU / MEM 资源和基于 Cron 定时 HPA 方式,同时也支持各种事件驱动型 HPA,比如 MQ 、Kafka 等消息队列长度事件,Redis 、URL Metric 、Promtheus 数值阀值事件等等事件源(Scalers)。
2. 为什么需要 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。
3. Demo
需要一个 k8s 集群,没有的话可以参考 Kubernetes教程(十一)—使用 KubeClipper 通过一条命令快速创建 k8s 集群 快速创建一个。
KEDA 安装
这里使用 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,能正常启动则算是安装成功。
[root@mcs-1 ~]# 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
metrics-server
由于 KEDA 也需要 HPA 配合使用,因此需要安装 metrics-server。
apply 以下 yaml 即可完成 metrics-server 部署
cat > metrics-server.yaml << EOF
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
k8s-app: metrics-server
name: metrics-server
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
k8s-app: metrics-server
rbac.authorization.k8s.io/aggregate-to-admin: "true"
rbac.authorization.k8s.io/aggregate-to-edit: "true"
rbac.authorization.k8s.io/aggregate-to-view: "true"
name: system:aggregated-metrics-reader
rules:
- apiGroups:
- metrics.k8s.io
resources:
- pods
- nodes
verbs:
- get
- list
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
k8s-app: metrics-server
name: system:metrics-server
rules:
- apiGroups:
- ""
resources:
- nodes/metrics
verbs:
- get
- apiGroups:
- ""
resources:
- pods
- nodes
verbs:
- get
- list
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
labels:
k8s-app: metrics-server
name: metrics-server-auth-reader
namespace: kube-system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: extension-apiserver-authentication-reader
subjects:
- kind: ServiceAccount
name: metrics-server
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
labels:
k8s-app: metrics-server
name: metrics-server:system:auth-delegator
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator
subjects:
- kind: ServiceAccount
name: metrics-server
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
labels:
k8s-app: metrics-server
name: system:metrics-server
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:metrics-server
subjects:
- kind: ServiceAccount
name: metrics-server
namespace: kube-system
---
apiVersion: v1
kind: Service
metadata:
labels:
k8s-app: metrics-server
name: metrics-server
namespace: kube-system
spec:
ports:
- name: https
port: 443
protocol: TCP
targetPort: https
selector:
k8s-app: metrics-server
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
k8s-app: metrics-server
name: metrics-server
namespace: kube-system
spec:
selector:
matchLabels:
k8s-app: metrics-server
strategy:
rollingUpdate:
maxUnavailable: 0
template:
metadata:
labels:
k8s-app: metrics-server
spec:
containers:
- args:
- --cert-dir=/tmp
- --secure-port=4443
- --kubelet-insecure-tls
- --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname
- --kubelet-use-node-status-port
- --metric-resolution=15s
image: dyrnq/metrics-server:v0.6.1
imagePullPolicy: IfNotPresent
livenessProbe:
failureThreshold: 3
httpGet:
path: /livez
port: https
scheme: HTTPS
periodSeconds: 10
name: metrics-server
ports:
- containerPort: 4443
name: https
protocol: TCP
readinessProbe:
failureThreshold: 3
httpGet:
path: /readyz
port: https
scheme: HTTPS
initialDelaySeconds: 20
periodSeconds: 10
resources:
requests:
cpu: 100m
memory: 200Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
volumeMounts:
- mountPath: /tmp
name: tmp-dir
nodeSelector:
kubernetes.io/os: linux
priorityClassName: system-cluster-critical
serviceAccountName: metrics-server
volumes:
- emptyDir: {}
name: tmp-dir
---
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
labels:
k8s-app: metrics-server
name: v1beta1.metrics.k8s.io
spec:
group: metrics.k8s.io
groupPriorityMinimum: 100
insecureSkipTLSVerify: true
service:
name: metrics-server
namespace: kube-system
version: v1beta1
versionPriority: 10
EOF
kubectl apply -f metrics-server.yaml
测试使用正常运行
[root@demo ~]# kubectl top node
NAME CPU(cores) CPU% MEMORY(bytes) MEMORY%
demo 585m 14% 4757Mi 60%
基于 CPU 使用率进行弹性伸缩
Deployment
部署一个 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
kubectl apply -f deploy.yaml
ScaledObject
检测 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
kubectl apply -f so.yaml
测试扩缩容
开始请求,增加压力
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
基于Kafka 消息数进行弹性伸缩
上一个 Demo 非常简单,其实直接用 HPA 也能实现,并不能完全体现出 KEDA 的作用。
假设,现在我们有这么一个场景,运行的业务是生产者消费者模型,消费者从 Kafka 里拿消息进行消费,然后我们希望根据 Kafka 里堆积的消息数来对消费者 做一个弹性伸缩。
这种场景下再基于 CPU 进行扩缩容可能就不够准确了,而直接基于堆积的消息数来扩缩容可能是最好的选择。
如果使用 HPA 的话也可以实现,但是很麻烦,而 KEDA 则非常简单了,KEDA 内置了 kakfa scaler,直接使用即可。
KEDA 的 kafka scaler 工作流程如下:
- 当没有待处理的消息时,KEDA 可以根据 minReplica 集将部署缩放到 0 或 1。
- 当消息到达时,KEDA 会检测到此事件并激活部署。
- 当部署开始运行时,其中一个容器连接到 Kafka 并开始拉取消息。
- 随着越来越多的消息到达 Kafka Topic,KEDA 可以将这些数据提供给 HPA 以推动横向扩展。
- 当消息被消费完之后,KEDA 可以根据 minReplica 集将部署缩放到 0 或 1。
使用起来也非常简单,只需要将 ScaledObject 中的 triggers 字段修改一下即可,具体如下:
triggers:
- type: kafka
metadata:
bootstrapServers: kafka.svc:9092
consumerGroup: my-group
topic: test-topic
lagThreshold: '100'
activationLagThreshold: '3'
offsetResetPolicy: latest
allowIdleConsumers: false
scaleToZeroOnInvalidOffset: false
excludePersistentLag: false
version: 1.0.0
partitionLimitation: '1,2,10-20,31'
tls: enable
sasl: plaintext
暂时关注前面几个参数即可:
- 其中 bootstrapServers、consumerGroup、topic 都是 kafka 信息
- lagThreshold 则是扩容的阈值,当消息延迟大于这个阈值时就会对 deployment 进行扩容。
配置后,keda 内置的 kafka scaler 就会根据提供的 auth 信息访问 kafka,拿到指定 topic 中的消息堆积数量,然后根据 lagThreshold 值计算出需要扩容的应用副本数。
这里有一个 Kafka-go 的教程:kafka-go-example
4. KEDA 是怎么工作的
架构
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
语法如下:
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 则根据当前指标和阈值判断扩容。
5. 小结
本文主要演示了 KEDA 的使用,同时也分析了其具体的工作流程。
KEDA 主要是为了解决 HPA 无法基于灵活的外部事件来实现弹性伸缩这个问题。
KEDA 内置了多种 Scaler 以满足不同场景的使用需求,简化了 基于 HPA + Prometheus Adaptor + XXExporter 的自定义流程。
理解 KEDA 的工作流程之后就会比较清晰了,再贴一下这个图
在创建一个 ScalerObject 之后, KEDA controller 就会动态注册一个 handler 到 external.metrics.k8s.io 这个服务上,最终 HPA 请求指标时则会根据路由转发到具体的 Scaler 上,Scaler 再根据 ScalerObject 中的配置请求外部系统拿到数据并将其转换为 HPA 能够识别的格式,最终返回给 HPA。