# 用 Keda 根据工作负载进行快速扩容

**太长不看版**：用单一指标指导单一工作负载进行扩缩容实在是太低效了。

## 历史问题

众所周知，Kubernetes 有个亲生的 HPA 组件，在云原生早期，这个名义上的自动扩缩容的能力给 Kubernetes 赢得了不少掌声。当然现在回头看看，仅仅根据 CPU 和内存这样“贫瘠”的指标，不论是用于判断负载水平，还是用于计算扩容目标，都不是很够用的。这个阶段里，HPA 的扩缩容效率也是广受诟病的一个问题，在一个多级微服务调用的业务场景里，压力是**逐级传递**的，下图展示了一个常见情况：

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1745947893420/413a73c2-58ee-4ade-a0a3-09e25eb12252.png align="center")

如上图，用户流量进入集群之后：

1. 首先在 `Deploy A` 造成负载，指标变化迫使 `Deploy A` 扩容
    
2. A 扩容之后，吞吐量变大，B 受到压力，再次采集到指标变化，扩容 `Deploy B`
    
3. B 吞吐变大，C ..
    

这个逐级传递的过程不仅缓慢，而且可以说是步步惊心——每一级的扩容都是直接被 CPU 或内存的飙高触发的，被“冲垮”的可能性是普遍存在的。这种被动、滞后的方式，很明显是有问题的。

## 推陈出新

造成 HPA 窘境的原因之一，就是“自扫门前雪”，每个 Pod 都只能根据自身负载情况来进行扩缩容决策。如果能够直接根据业务流量的变化进行决策，并且将流量流经的所有微服务进行扩缩容，看起来情况就会好很多了。 HPA 的[自定义指标](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/)支持，给这个问题了一个可行的方案。该能力让 HPA 可以用其它的指标来作为扩缩容的触发器，例如我们可以用 Promethues 采集消息中间件的深度或者负载均衡器的队列长度，作为一个更能如实反映业务流量的指标，直接用来触发相关的多个微服务的扩缩容，如下图所示：

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1745947901700/dc9c8318-b49e-4073-bc0a-b6c56c98db49.png align="center")

在上图中：

1. Prometheus 采集消息队列和负载均衡等更能反映业务流量的指标
    
2. 使用 Prometheus Adapter 将 Promethues Metrics 转换为 Kubernetes 的 Aggregated API
    
3. HPA 使用自定义指标，**同时**对多个应用进行扩缩容。
    

这中间涉及到的 Prometheus Adapter，通过配置文件完成步骤 2 的转换：

```yaml
- seriesQuery: '{__name__=~"^container_.*_total",container!="POD",namespace!="",pod!=""}'
  resources:
    overrides:
      namespace: {resource: "namespace"}
      pod: {resource: "pod"}
  seriesFilters:
  # since this is a superset of the query above, we introduce an additional filter here
  - isNot: "^container_.*_seconds_total$"
  name: {matches: "^container_(.*)_total$"}
  metricsQuery: "sum(rate(<<.Series>>{<<.LabelMatchers>>,container!="POD"}[2m])) by (<<.GroupBy>>)"
```

当然，完全可以自行实现 Aggregated API 来支持这种指标的采集和呈现工作。Prometheus 所提供的大量 Exporter 是吸引我们写这种古怪语法的最大动力。

那么如果是 KEDA 的话，这个问题又如何呢？KEDA 提供了几十个被称为 Scaler 的东西，其中除了 Promethues 之外，还包括 Kafka、Redis、PostgreSQL 等多种选择。所以在很多场景中，无需 Promethues，也能使用 Scaler 完成对输入指标的读取和判断。下面用 KEDA 为例，看看这种伸缩方法的具体实现。

## KEDA

假设一个容器化应用由多个工作负载组成：

* Ingress：负责接收业务流量
    
* Backend 1、Backend 2：负责处理 Ingress 发来的任务
    
* Database：数据库
    

我们希望达成的效果是 —— Ingress、Backend 1、Backend 2、Database，实例数量保持在 `1:2:1.5:2` 的关系，Keda 的大致流程如下图所示：

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1745947914894/aec5ab57-7f95-4210-98d7-3f61c7864536.png align="center")

首先使用 Helm 安装 KEDA：

```bash
$ helm repo add kedacore https://kedacore.github.io/charts
$ helm install keda kedacore/keda --namespace default
NAME: keda
LAST DEPLOYED: Wed Nov 29 18:56:36 2023
NAMESPACE: default
STATUS: deployed
REVISION: 1
...
```

随便创建几个工作负载，冒充微服务：

```bash
$ kubectl create deploy ingress --image=nginx
deployment.apps/ingress created
$ kubectl create deploy backend1 --image=nginx
deployment.apps/backend1 created
$ kubectl create deploy backend2 --image=nginx
deployment.apps/backend2 created
$ kubectl create deploy database --image=nginx
deployment.apps/database created
$ kubectl get pods | cut -d - -f 1 | grep -v keda | sort
...
backend1
backend2
database
ingress
```

运行成功后，我们可以看到，四个微服务，每个微服务都有一个实例。

按照刚才瞎掰的比例，编写一个 `ScaleObject`：

```yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: bk1
spec:
  scaleTargetRef:
    name: backend1
  triggers:
  - type: kubernetes-workload
    metadata: 
      podSelector: 'app=ingress'
      value: '0.5'
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: bk2
spec:
  scaleTargetRef:
    name: backend2
  triggers:
  - type: kubernetes-workload
    metadata: 
      podSelector: 'app=ingress'
      value: '0.67'      
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: db
spec:
  scaleTargetRef:
    name: database
  triggers:
  - type: kubernetes-workload
    metadata: 
      podSelector: 'app=ingress'
      value: '0.5'
```

上述代码引入了 `kubernetes-workload` 类型的触发器，他会监控 `app=ingress` 的容器，并对 `scaleTargetRef` 中提到的工作负载数量比例进行扩缩容。

提交到集群之后，会看到实例数量数量发生了变化：

```bash
$ kubectl get pods | cut -d - -f 1 | sort | uniq --count
...
   2 backend1
   2 backend2
   2 database
   1 ingress
   3 keda
```

我们把 Ingress 扩容到 2 实例，再次统计：

```bash
$ kubectl scale deployment ingress --replicas=2
deployment.apps/ingress scaled
$ kubectl get pods | cut -d - -f 1 | sort | uniq --count
...
   4 backend1
   3 backend2
   4 database
   2 ingress
   3 keda
```

可以看到，的确是按照我们设定的比例，同步产生了缩放。如果缩减 Ingress 服务实例数，几分钟之后，其它工作负载也会随之缩容。

```bash
$ kubectl scale deployment ingress --replicas=1
deployment.apps/ingress scaled
$ kubectl get pods | cut -d - -f 1 | sort | uniq --count                                                         \
...
   2 backend1
   2 backend2
   2 database
   1 ingress
```

## 结论

虽说云原生架构的复杂性问题越来越被强调，但是这一生态的宗旨应该还是没有变化——用简单的透明的手段解决复杂问题。
