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 安装

1
2
3
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,能正常启动则算是安装成功。

1
2
3
4
[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 部署

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
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
1
kubectl apply -f metrics-server.yaml

测试使用正常运行

1
2
3
[root@demo ~]# kubectl top node
NAME   CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%
demo   585m         14%    4757Mi          60%

基于 CPU 使用率进行弹性伸缩

Deployment

部署一个 php-apache 服务作为工作负载

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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
1
kubectl apply -f deploy.yaml

ScaledObject

检测 cpu 压力进行扩缩容

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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
1
kubectl apply -f so.yaml

测试扩缩容

开始请求,增加压力

1
2
clusterIP=$(kubectl get svc php-apache -o jsonpath='{.spec.clusterIP}')
while sleep 0.01; do wget -q -O- http://$clusterIP; done

过一会就能看到 pod 数在增加

1
2
3
4
5
6
7
8
9
[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 对象出来

相关日志如下

1
2
3
[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

1
2
3
[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 数进行回收。

1
2
3
4
5
6
[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 字段修改一下即可,具体如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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-arch.png

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-arch.png

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 了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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 对象

1
2
3
[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 内容如下

省略其他内容

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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 对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
[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

访问一下试试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
[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

语法如下:

1
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,请求命令如下:

1
2
3
4
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 .

返回结果如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
[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 的工作流程之后就会比较清晰了,再贴一下这个图

keda-arch.png

在创建一个 ScalerObject 之后, KEDA controller 就会动态注册一个 handler 到 external.metrics.k8s.io 这个服务上,最终 HPA 请求指标时则会根据路由转发到具体的 Scaler 上,Scaler 再根据 ScalerObject 中的配置请求外部系统拿到数据并将其转换为 HPA 能够识别的格式,最终返回给 HPA。

0%