Kubernetes KEDA 事件驱动自动扩缩容:原理、选型与 KServe 实战
KEDA(Kubernetes Event-Driven Autoscaling)把消息积压、请求并发、数据库查询、Prometheus 指标和定时计划等业务信号转换为 Kubernetes 扩缩容决策。本文统一整理 KEDA 的原理、资源模型、与 HPA 的边界、Scale-to-Zero、常见事件源实战,以及 KServe + vLLM 的请求指标扩缩容案例。
示例版本
通用示例与 KServe 实测使用 KEDA 2.17.2;KEDA 与 Kubernetes 的字段、兼容矩阵和升级要求会变化,生产使用前应核对目标版本的 KEDA 文档 与 Release Notes。
原示例对应的版本化入口:KEDA 2.17 Getting Started 与 KEDA 2.17 Scalers。
为什么需要事件驱动扩缩容
CPU 和内存能反映资源压力,却不一定等价于业务工作量。CPU 升高可能来自异常循环、依赖故障或运行时抖动;盲目扩容既不一定解决问题,还可能放大故障。队列消费者、批处理 Worker 与模型推理服务更适合使用以下信号:
- 消息队列:Kafka lag、RabbitMQ 队列长度、Redis 列表或 Stream 积压;
- 请求压力:QPS、并发请求数、正在执行与等待中的请求数;
- 数据状态:SQL 查询得到的 pending 记录数;
- 自定义指标:Prometheus、Datadog、New Relic 等观测系统中的业务指标;
- 时间计划:Cron 触发器在固定时段扩缩容;
- 一次性任务:按事件数量创建 Job,处理完成后退出。
KEDA 的核心目标是“有工作时启动并扩展,无工作时缩减,必要时缩到 0”。这尤其适合异步任务和昂贵的 CPU/GPU 工作负载。
KEDA 架构与工作流

KEDA 不是 HPA 的替代实现,而是把外部事件源、认证和指标适配封装起来,并管理相应的 HPA。当前架构的职责边界可参考 KEDA Concepts:
- KEDA Operator 监听 ScaledObject/ScaledJob,轮询事件源并管理扩缩容资源;
- 对 ScaledObject,Operator 负责
0 ↔ 1的激活与缩零; - KEDA Metrics API Server 把外部指标暴露给 Kubernetes External Metrics API;
- HPA 读取指标并负责
1 ↔ N的副本计算; - Admission Webhook 在资源创建或更新时校验常见冲突与非法配置。
flowchart LR
S[外部事件源] --> O[KEDA Operator]
O -->|0 ↔ 1| W[Deployment / StatefulSet]
S --> M[KEDA Metrics API Server]
M --> A[External Metrics API]
A --> H[HPA]
H -->|1 ↔ N| W
激活阈值与扩缩容阈值
KEDA 将缩零/唤醒和 1~N 扩缩容分成两个阶段。支持的 Scaler 可用 activation* 参数控制激活阈值,常规阈值则交给 HPA 计算。若 minReplicaCount >= 1,激活阈值不会参与决策。
核心资源
| 资源 | 用途 |
|---|---|
ScaledObject |
为 Deployment、StatefulSet 或支持 /scale 子资源的对象配置持续扩缩容,并自动管理 HPA |
ScaledJob |
根据事件积压创建一次性 Job,适合图片、视频、批处理任务 |
TriggerAuthentication |
在命名空间内声明 Scaler 的认证来源 |
ClusterTriggerAuthentication |
跨命名空间复用认证配置 |
| KEDA Operator | 监听 KEDA CRD,轮询 Scaler,并负责 0↔1 |
| KEDA Metrics API Server | 向 HPA 提供 External Metrics |
| Admission Webhook | 校验 KEDA 资源与同一目标的控制冲突 |
KEDA 支持 Kafka、RabbitMQ、AWS SQS、Azure Service Bus、NATS、Redis、PostgreSQL、MySQL、MongoDB、Prometheus、Cron 等多类 Scaler。完整列表以 KEDA Scalers 为准。
认证信息不应直接写进 ScaledObject。TriggerAuthentication 可以引用 Kubernetes Secret、ConfigMap、环境变量或云身份;当前版本还支持从 GCP Secret Manager 等外部 Secret 服务获取认证材料。原对比稿把 GCP Secret Manager 与 ConfigMap 认证误标为 KEDA 2.16 新特性,实际上它们更早已经进入 KEDA,本文不再绑定错误版本。
HPA、原生 Scale-to-Zero 与 KEDA 的边界
HPA 的指标与计算
HPA 的基础计算公式是:
期望副本数 = ceil(当前副本数 × 当前指标值 / 目标指标值)
autoscaling/v2 支持 Resource、Pods、Object、External 等指标。Resource 指标通常来自 Metrics Server;自定义与外部指标需要相应 Adapter。HPA 可以同时配置多个指标,并采用各指标建议副本数中的最大值。
Kubernetes 1.37 原生 HPA Scale-to-Zero
Kubernetes 1.37 将 HPAScaleToZero 提升为 Beta 并默认启用,而不是原整理稿所写的 v1.36。原生 HPA 使用适合的 Object 或 External 指标时,可以把 minReplicas 设为 0,并在指标重新活跃时从 0 拉起。版本状态与升级边界见 Kubernetes Feature Gates 和 Kubernetes 1.37 Scale-to-Zero 公告。
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: batch-processor
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: batch-processor
minReplicas: 0
maxReplicas: 20
metrics:
- type: External
external:
metric:
name: queue_depth
target:
type: AverageValue
averageValue: "10"
硬性边界:
HPAScaleToZero必须在 kube-apiserver 和 kube-controller-manager 上同时可用;minReplicas: 0至少需要一个 Object 或 External 指标,不能只依赖 CPU/内存 Resource 指标;- 指标在 Pod 为 0 时仍必须可读取,否则 HPA 没有唤醒信号;
- Kubernetes Service 不会在后端为 0 时缓存 HTTP 请求;同步请求缩零需要网关、队列或 KEDA HTTP Add-on 等缓冲层;
- 指标源故障时,HPA 会报告
ScalingActive=False,应监控并准备人工或自动兜底。
KEDA 仍然解决什么问题
原生 HPA 1.37 解决了“能否缩到 0”,但没有替代事件源连接、认证管理和 Scaler 运维。KEDA 的价值仍在于:
- 对接大量事件源,无需为每个中间件自行开发 External Metrics Adapter;
- 分离激活阈值与扩缩容阈值;
- 在同一 ScaledObject 中组合多个 Trigger;
- 使用 TriggerAuthentication/ClusterTriggerAuthentication 管理认证;
- 通过 ScaledJob 把事件积压映射为一次性 Job;
- 为指标故障提供可配置的 fallback 副本策略。
选型矩阵
| 场景 | 推荐 | 原因 |
|---|---|---|
| 标准 HTTP 服务,CPU/内存与负载相关 | HPA | 原生、依赖少、排障链路短 |
| 已有稳定 Object/External Metrics Adapter,集群为 1.37+ | 原生 HPA | 可直接缩零,减少额外组件 |
| Kafka/RabbitMQ/Redis 等队列消费者 | KEDA | 事件源和认证开箱即用 |
| 多事件源组合触发 | KEDA | ScaledObject 支持多个 Trigger 与组合策略 |
| 一次性批处理 | KEDA ScaledJob | 按积压创建 Job,而不是维持常驻 Deployment |
| KServe/vLLM 请求压力 | KServe + Prometheus + KEDA | 直接以运行中和排队请求数驱动副本 |
| 同一系统的不同工作负载 | HPA 与 KEDA 分工 | HTTP Deployment 用 HPA,队列消费者用 KEDA |
单一控制器原则
不要让手工 HPA 与 KEDA ScaledObject 同时管理同一个 Deployment。KEDA 会自行创建并管理 HPA;双重控制会造成副本数争用和震荡。
KEDA 资源模型
ScaledObject
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: video-processing-scaledobject
namespace: default
spec:
scaleTargetRef:
name: order-processor # 目标deployment名称
pollingInterval: 30 # 每30s检查一次队列
cooldownPeriod: 300 # 扩缩容后等待5分钟。防止扩缩容抖动
minReplicaCount: 1
maxReplicaCount: 10
triggers:
- type: rabbitmq
metadata:
queueName: "video-processing-queue" # rabbitmq队列名称
mode: QueueLength # 监听类型
value: "50" # 当队列中每个实例平均消息50条及以上时触发扩容
authenticationRef: # 不推荐直接把用户名密码写在这里。都是用一个TriggerAuthentication绑定
name: keda-trigger-auth-rabbitmq-conn # TriggerAuthentication名称
ScaledJob
apiVersion: keda.sh/v1alpha1
kind: ScaledJob
metadata:
name: video-processing-scaledjob
namespace: default
spec:
jobTargetRef:
parallelism: 1 # 每次只创建一个job实例
completions: 1 # 每个Job只运行一次
template:
spec:
restartPolicy: Never
containers:
- name: video-processer
image: busybox:1.28
pollingInterval: 30 # 每30s检查一次队列
maxReplicaCount: 10 # 最多同时运行10个job
successfulJobsHistoryLimit: 3 # 保留3个成功job的历史
failedJobsHistoryLimit: 1 # 保留1个失败的job历史
triggers:
- type: rabbitmq
metadata:
queueName: "video-processing-queue" # rabbitmq队列名称
mode: QueueLength # 监听类型
value: "50" # 当队列中每个实例平均消息50条及以上时触发扩容
authenticationRef: # 不推荐直接把用户名密码写在这里。都是用一个TriggerAuthentication绑定
name: keda-trigger-auth-rabbitmq-conn # TriggerAuthentication名称
TriggerAuthentication
连接基础组件的时候需要认证,KEDA设计了单独的资源来保存认证信息。
TriggerAuthentication直接去k8s secret里面读,他自己并不保存认证信息。
apiVersion: v1
kind: Secret
metadata:
name: mysql-secrets
namespace: default
type: Opaque
stringData:
mysql_conn_str: user:password@tcp(mysql:3306)/status_db
---
apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
name: keda-trigger-auth-mysql-conn
namespace: default
spec:
secretTargetRef:
- parameter: connectionString # Scaler参数名字。固定值不能改。应该填啥去keda官网查。
name: mysql-secret # secret名称
key: mysql_conn_str # secret的key名称
# 也支持基于configMap加载
# configMapTargetRef:
# - parameter: connectionString # Scaler参数名字。固定值不能改。应该填啥去keda官网查。
# name: keda-cm-name # cm名称
# key: azure-storage-connectionstring # cm的key名称
# 也支持基于env加载
# - parameter: region # Scaler参数名字。固定值不能改。应该填啥去keda官网查。
# name: my-env-car # 取哪个环境变量的值
# containerName: my-container # 从哪个容器中取(keda管理的deployment中的pod中的某个container)
部署 KEDA
本文实测版本为 2.17.2。可以直接从仓库安装:
helm repo add kedacore https://kedacore.github.io/charts
helm repo update
helm upgrade --install keda kedacore/keda \
-n keda --create-namespace \
--version 2.17.2 \
--wait
需要离线审阅或自定义 values 时,先下载 Chart:
helm pull kedacore/keda --version 2.17.2
tar xzf keda-2.17.2.tgz
helm upgrade --install keda ./keda \
-n keda --create-namespace \
-f values.yaml \
--wait
安装后同时检查三个组件与聚合 API:
kubectl get pod -n keda
kubectl get crd | grep keda.sh
kubectl get apiservice v1beta1.external.metrics.k8s.io
预期 Operator、Admission Webhook、Metrics API Server 均为 Running,且 v1beta1.external.metrics.k8s.io 的 AVAILABLE=True。只有 CRD 可见但 External Metrics API 不可用时,ScaledObject 可以创建,HPA 仍然读不到 KEDA 指标。
通用场景实战
周期性扩缩容
KEDA支持周期性弹性收缩服务,且支持缩容至0。使用场景:
- 比如有很多任务,占用大量资源,不能在白天跑,需要在晚上跑。
- 比如有个服务只有每天早上7-9点属于业务高峰,就可以利用KEDA实现在7-9点扩展服务,除此之外的时间缩减副本,以节省资源。
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: cron-scaledobject
namespace: default
spec:
scaleTargetRef:
name: nginx-hpa # 扩容对象是deployment
minReplicaCount: 0 # 最低副本数
cooldownPeriod: 300 # 冷却期:到end时间后,过多久再缩容
triggers:
- type: cron
metadata:
timezone: Asia/Shanghai
# 分钟 小时 日 月 星期
start: 45 10 * * * # 每天北京时间10:45扩容
end: 55 10 * * * # 每天北京时间10:55缩容
desiredReplicas: "3" # 扩容后的副本数
基于 RabbitMQ 消息队列扩缩容
KEDA 支持基于 RabbitMQ、Kafka、Redis 等消息队列的积压或消费速率扩缩容,使消费者数量与待处理工作量匹配。
创建测试RabbitMQ
apiVersion: v1
kind: Service
metadata:
name: rabbitmq
spec:
type: NodePort
selector:
app: rabbitmq
ports:
- name: web
port: 5672
protocol: TCP
targetPort: 5672
- name: http
port: 15672
protocol: TCP
targetPort: 15672
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: rabbitmq
name: rabbitmq
spec:
replicas: 1
selector:
matchLabels:
app: rabbitmq
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
type: RollingUpdate
template:
metadata:
labels:
app: rabbitmq
spec:
containers:
- env:
- name: TZ
value: Asia/Shanghai
- name: LANG
value: C.UTF-8
- name: RABBITMQ_DEFAULT_USER
value: user
- name: RABBITMQ_DEFAULT_PASS
value: password
image: registry.cn-beijing.aliyuncs.com/dotbalo/rabbitmq:4.0.5-management-alpine
imagePullPolicy: IfNotPresent
livenessProbe:
failureThreshold: 2
initialDelaySeconds: 30
periodSeconds: 10
successThreshold: 1
tcpSocket:
port: 5672
timeoutSeconds: 2
name: rabbitmq
ports:
- containerPort: 5672
name: web
protocol: TCP
readinessProbe:
failureThreshold: 2
initialDelaySeconds: 30
periodSeconds: 10
successThreshold: 1
tcpSocket:
port: 5672
timeoutSeconds: 2
通过NodePort访问RabbitMQ UI,注意用的是name为http的端口
模拟消息写入
apiVersion: batch/v1
kind: Job
metadata:
name: rabbitmq-publish
spec:
backoffLimit: 4
template:
spec:
restartPolicy: Never
containers:
- name: rabbitmq-client
image: registry.cn-beijing.aliyuncs.com/dotbalo/rabbitmq-publish:v1.0
imagePullPolicy: IfNotPresent
command:
- "send"
- "amqp://user:password@rabbitmq.default.svc.cluster.local:5672"
- "100"
模拟消费者
apiVersion: apps/v1
kind: Deployment
metadata:
name: rabbitmq-consumer
namespace: default
labels:
app: rabbitmq-consumer
spec:
selector:
matchLabels:
app: rabbitmq-consumer
template:
metadata:
labels:
app: rabbitmq-consumer
spec:
containers:
- name: rabbitmq-consumer
image: registry.cn-beijing.aliyuncs.com/dotbalo/rabbitmq-consumer:v1.0
imagePullPolicy: IfNotPresent
command:
- receive
args:
- "amqp://user:password@rabbitmq.default.svc.cluster.local:5672"
创建 TriggerAuthentication
这里用secret保存连接字符串,也可以直接引用RabbitMQ中的用户名密码env
apiVersion: v1
kind: Secret
metadata:
name: keda-rabbitmq-secret
stringData: # stringData 便于直接填写明文;API Server 最终仍会把 Secret 数据编码后存入 data。
host: amqp://user:password@rabbitmq.default.svc.cluster.local:5672 # amqp地址
---
apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
name: keda-trigger-auth-rabbitmq-conn
spec:
secretTargetRef:
- parameter: host
name: keda-rabbitmq-secret
key: host
创建 ScaledObject
配置参数参考:RabbitMQ Queue Scaler
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: rabbitmq-scaledobject
spec:
scaleTargetRef:
name: rabbitmq-consumer
pollingInterval: 5 # 将默认 30 秒轮询缩短为 5 秒;生产环境按事件延迟和上游压力调整
cooldownPeriod: 30 # 默认 300 秒;只控制 KEDA 从 1 缩到 0 的等待时间
minReplicaCount: 1
maxReplicaCount: 5 # 最大副本数
triggers:
- type: rabbitmq
metadata:
protocol: amqp # 这是最常用的连接协议
queueName: hello
mode: QueueLength # 监听模式,队列长度(QueueLength)或消息速率(MessageRate)。一般用队列长度来做判断
value: "50" # 消息数或每秒速率
authenticationRef:
name: keda-trigger-auth-rabbitmq-conn
基于数据库扩缩容
例如工单系统可查询数据库中的待处理记录数:当 pending 工单达到阈值时扩容处理程序,积压清空后再缩容。
模拟数据库实例
kubectl create deployment mysql --image=registry.cn-beijing.aliyuncs.com/dotbalo/mysql:8.0.20
kubectl set env deploy mysql MYSQL_ROOT_PASSWORD=password
kubectl expose deploy mysql --port 3306
# 进入数据库创建测试的表
kubectl exec -ti mysql-6d8bd866d8-q89nj -- bash
mysql -uroot -hmysql -p
create database dukuan;
use dukuan;
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
customer_name VARCHAR(100),
order_amount DECIMAL(10, 2),
status ENUM('pending', 'processed') DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
模拟数据写入程序
kubectl create job insert-orders-job --image=registry.cn-beijing.aliyuncs.com/dotbalo/mysql:insert
再次进入数据库查看数据
kubectl exec -ti mysql-6d8bd866d8-7qjx9 -- bash
use dukuan;
select * from orders;
结果会出现工单状态,有一些pending状态的即为未处理完的工单。需要创建数据处理程序。
创建数据处理程序
kubectl create deploy update-orders --image=registry.cn-beijing.aliyuncs.com/dotbalo/mysql:process
再次进入数据库查看数据:
kubectl exec -ti mysql-6d8bd866d8-7qjx9 -- bash
use dukuan;
select * from orders;
工单状态会变成processed表示处理完成。
后面我们创建scaledObject,用SQL语句查询数据库中对应未处理的工单数量,当未处理的工单数量大于指定的值时,触发工单处理的deployment的扩容。
创建TriggerAuthentication
apiVersion: v1
kind: Secret
metadata:
name: keda-mysql-secret
stringData:
mysql_conn_str: root:password@tcp(mysql.default.svc.cluster.local:3306)/dukuan
---
apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
name: keda-trigger-auth-mysql-conn
spec:
secretTargetRef:
- parameter: connectionString
name: keda-mysql-secret
key: mysql_conn_str
创建 ScaledObject
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: mysql-scaledobject
spec:
scaleTargetRef:
name: update-orders
pollingInterval: 5 # 将默认 30 秒轮询缩短为 5 秒
cooldownPeriod: 30 # 冷却时间,默认300秒
minReplicaCount: 0
maxReplicaCount: 5 # 最大副本数
triggers:
- type: mysql
metadata:
queryValue: "4.4" # 触发扩缩容的平均值
query: "SELECT COUNT(*) FROM orders WHERE status='pending'"
authenticationRef:
name: keda-trigger-auth-mysql-conn
再次触发数据插入:
kubectl delete job insert-orders-job
kubectl create job insert-orders-job --image=registry.cn-beijing.aliyuncs.com/dotbalo/mysql:insert
可以观察到在insert-order-jobs完成后,立刻触发了updat-orders deployment的扩容,数据处理的很快,迅速就低于设定值了。30s之后就缩容到0了。
ScaledJob 任务处理
KEDA可以使用ScaledJob实现单次或者临时的任务处理,用来处理一些数据,比如图片、视频等。
基于 Redis 扩缩容
假设有一个需求,需要从Redis队列获取数据,然后进行处理,就可以使用ScaledJob实现。
首先创建一个Redis实例:
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update bitnami
helm upgrade -i redis bitnami/redis \
--set global.imageRegistry=docker.kubeasy.com \
--set global.redis.password=dukuan \
--set architecture=standalone \
--set master.persistence.enabled=false \
--version 20.1.6
创建TriggerAuthentication
apiVersion: v1
kind: Secret
metadata:
name: redis-so-secret
type: Opaque
stringData:
redis_username: ""
redis_password: "dukuan"
---
apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
name: redis-so-ta
spec:
secretTargetRef:
- parameter: username
name: redis-so-secret
key: redis_username
- parameter: password
name: redis-so-secret
key: redis_password
测试写入数据
kubectl exec -ti redis-master-0 -- bash
redis-cli -h redis-master -a dukuan
# 写入队列数据
LPUSH test_list "t1" "t2" "t3"
LRANGE test_list 0 2
# 读取数据
RPOP test_list
LPOP test_list
创建 ScaledJob 监听队列
apiVersion: keda.sh/v1alpha1
kind: ScaledJob
metadata:
name: redis-queue-scaledjob
spec:
jobTargetRef:
parallelism: 1 # 每次只启动一个 Job 实例
completions: 1 # 每个 Job 只需要完成一次
backoffLimit: 4 # 最大重试次数
template:
spec:
containers:
- name: redis-queue-consumer
image: registry.cn-beijing.aliyuncs.com/dotbalo/redis:process
pollingInterval: 30 # 每 30 秒检查一次队列中的消息数量
successfulJobsHistoryLimit: 3 # 保留最近 3 个成功的 Job
failedJobsHistoryLimit: 3 # 保留最近 3 个失败的 Job
maxReplicaCount: 5 # 最多同时运行 5 个 Job
triggers:
- type: redis
metadata:
address: redis-master.default.svc.cluster.local:6379
listName: test_list
listLength: "5"
authenticationRef:
name: redis-so-ta
写入测试数据
LPUSH test_list "t1" "t2" "t3"
LPUSH test_list "t1" "t2" "t3"
LPUSH test_list "t1" "t2" "t3"
LRANGE test_list 0 8
查看redis-queue-scaledjob的日志,就可以看到job扩容出新的pod处理数据。
Kafka ScaledObject 示例
Kafka 消费者通常以 consumer group lag 作为扩缩容信号:
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: order-consumer
spec:
scaleTargetRef:
name: order-consumer
pollingInterval: 30
cooldownPeriod: 300
minReplicaCount: 0
maxReplicaCount: 50
triggers:
- type: kafka
metadata:
bootstrapServers: kafka-broker:9092
consumerGroup: order-processor
topic: orders
lagThreshold: "100"
offsetResetPolicy: latest
lagThreshold 应按单 Pod 消费吞吐量、消息处理时限和分区数压测确定;副本数还受 Kafka 分区可并行度约束。
KServe + KEDA:基于请求指标的模型服务实战
模型服务运行后,固定副本数很难同时兼顾突发请求和资源利用率。本文通过一个 Demo,为 KServe 模型服务接入 KEDA,根据 vLLM 的请求指标自动调整副本数,并验证服务从 1 -> 2 -> 1 的扩缩容过程。
环境准备
实现思路
需要额外安装 KEDA 和 Prometheus,工作流程如下:
- KServe 创建 Predictor Deployment 启动模型服务,底层推理引擎 vLLM 通过
/metrics暴露运行指标。 - Prometheus 采集
vllm:num_requests_running和vllm:num_requests_waiting等指标。 - KEDA 查询 Prometheus,并通过 External Metrics API 将查询结果暴露为 HPA 可以使用的外部指标。
- HPA 比较当前指标值与目标值,计算期望副本数并调整 Predictor Deployment。
KEDA 版本与组件前提
本实验实测 KEDA 2.17.2。安装方式见本文前面的“部署 KEDA”;在进入模型服务配置前,应确认 Operator、Admission Webhook 和 Metrics API Server 均正常,尤其是 v1beta1.external.metrics.k8s.io 必须处于 Available=True。只有 CRD 就绪并不代表 HPA 能读取 KEDA 提供的指标。
部署 Prometheus
使用 kube-prometheus-stack 部署 Prometheus 和 Prometheus Operator,关闭 Grafana、Alertmanager、Exporter 和默认告警规则,仅保留指标采集所需组件:
helm upgrade --install prometheus \
oci://ghcr.io/prometheus-community/charts/kube-prometheus-stack \
-n monitoring --create-namespace \
--version 88.2.0 \
--set grafana.enabled=false \
--set alertmanager.enabled=false \
--set kubeStateMetrics.enabled=false \
--set nodeExporter.enabled=false \
--set defaultRules.create=false \
--set kubeApiServer.enabled=false \
--set kubelet.enabled=false \
--set kubeControllerManager.enabled=false \
--set coreDns.enabled=false \
--set kubeDns.enabled=false \
--set kubeEtcd.enabled=false \
--set kubeScheduler.enabled=false \
--set kubeProxy.enabled=false \
--set prometheusOperator.admissionWebhooks.enabled=false \
--set prometheusOperator.tls.enabled=false \
--wait
先确认 Prometheus Service 已创建。后续 InferenceService 和查询都使用这个 Service;如果没有输出,应先检查 Helm Release 和 Prometheus Pod,不要继续创建 InferenceService:
kubectl get svc prometheus-kube-prometheus-prometheus -n monitoring
kubectl get pod -n monitoring
kubectl get pod,svc -n monitoring
当前配置不创建 PrometheusRule,因此一并关闭了只用于校验规则的 Admission Webhook。安装完成后,集群中应保留 Prometheus Operator 和 Prometheus 两个 Pod。
配置自动扩缩容
创建 InferenceService
测试节点只有一张 GPU,而 Demo 需要把服务扩到两个副本,因此使用 HAMi DRA 共享 GPU。创建 ResourceClaimTemplate 和完整的 InferenceService,同时配置每个副本的 GPU 配额、模型和 KEDA 自动扩缩容:
cat <<'EOF' > qwen-llm.yaml
apiVersion: resource.k8s.io/v1
kind: ResourceClaimTemplate
metadata:
name: qwen-hami-gpu
namespace: kserve-test
spec:
spec:
devices:
requests:
- name: gpu
exactly:
deviceClassName: hami-core-gpu.project-hami.io
allocationMode: ExactCount
count: 1
capacity:
requests:
memory: 3Gi
cores: "20"
---
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: qwen-llm
namespace: kserve-test
annotations:
serving.kserve.io/deploymentMode: Standard
serving.kserve.io/autoscalerClass: keda
spec:
predictor:
minReplicas: 1
maxReplicas: 2
resourceClaims:
- name: gpu
resourceClaimTemplateName: qwen-hami-gpu
autoScaling:
metrics:
- type: External
external:
metric:
backend: prometheus
serverAddress: http://prometheus-kube-prometheus-prometheus.monitoring.svc:9090
query: >-
sum(vllm:num_requests_running{namespace="kserve-test",pod=~"qwen-llm-predictor-.*"})
+
sum(vllm:num_requests_waiting{namespace="kserve-test",pod=~"qwen-llm-predictor-.*"})
target:
type: Value
value: "1"
model:
modelFormat:
name: huggingface
image: docker.m.daocloud.io/kserve/huggingfaceserver:v0.18.0-gpu
storageUri: pvc://qwen-model
args:
- --model_name=qwen
- --max_model_len=4096
- --max-num-seqs=32
- --gpu-memory-utilization=0.8
resources:
requests:
cpu: "1"
memory: 4Gi
limits:
cpu: "2"
memory: 6Gi
claims:
- name: gpu
EOF
kubectl apply -f qwen-llm.yaml
kubectl wait --for=condition=Ready \
inferenceservice/qwen-llm -n kserve-test --timeout=10m
每个 Predictor 副本通过 qwen-hami-gpu 申请 3Gi 显存和 20% GPU 核心,因此单 GPU 节点可以同时运行两个副本。KServe 创建 Predictor Deployment 和 ScaledObject,KEDA 再创建 HPA 管理副本数。
扩缩容配置解析
这份 YAML 中的扩缩容配置如下:
metadata:
annotations:
serving.kserve.io/autoscalerClass: keda
spec:
predictor:
minReplicas: 1
maxReplicas: 2
autoScaling:
metrics:
- type: External
external:
metric:
backend: prometheus
serverAddress: http://prometheus-kube-prometheus-prometheus.monitoring.svc:9090
query: >-
sum(vllm:num_requests_running{namespace="kserve-test",pod=~"qwen-llm-predictor-.*"})
+
sum(vllm:num_requests_waiting{namespace="kserve-test",pod=~"qwen-llm-predictor-.*"})
target:
type: Value
value: "1"
各参数含义:
autoscalerClass: keda:使用 KEDA 进行扩缩容。minReplicas、maxReplicas:限制最小和最大副本数。serverAddress:Prometheus 地址。query:使用running + waiting,同时统计正在执行和已经进入 vLLM 队列的请求。target.value:每个副本期望承载的目标值。
Prometheus 查询得到的是所有 Predictor Pod 的请求总数:
Query = running 请求数 + waiting 请求数
target.value: 1 表示希望每个副本平均处理 1 个请求。当前副本范围是 1~2:
- 当前只有 1 个副本时,如果
Query = 0或1,保持 1 个副本。 - 当前只有 1 个副本时,如果
Query >= 2,说明请求超过单个副本的目标值,扩到 2 个副本。 - 扩容后即使请求继续增加,也不会超过
maxReplicas: 2。 - 负载停止后,
Query回到0或1,等待 300 秒缩容稳定窗口结束,再缩回 1 个副本。
配置 ServiceMonitor
Prometheus Stack 不会自动采集 qwen-llm。创建 ServiceMonitor,通过 InferenceService Label 选择 KServe 生成的 Predictor Service:
cat <<'EOF' > qwen-llm-monitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: qwen-llm
namespace: monitoring
labels:
release: prometheus
spec:
namespaceSelector:
matchNames:
- kserve-test
selector:
matchLabels:
serving.kserve.io/inferenceservice: qwen-llm
endpoints:
- port: qwen-llm-predictor
path: /metrics
interval: 5s
EOF
kubectl apply -f qwen-llm-monitor.yaml
ServiceMonitor 不会通过 Service 的 ClusterIP 随机抓取一个后端。Prometheus Operator 会读取 Service 对应的 EndpointSlice,并把每个 Predictor Pod 都作为独立 Target。
检查指标和扩缩容资源
通过 Kubernetes Service Proxy 检查 Target。当前只有一个 Predictor,应看到一行 qwen-llm-predictor-*,状态为 up:
kubectl get --raw \
'/api/v1/namespaces/monitoring/services/http:prometheus-kube-prometheus-prometheus:http-web/proxy/api/v1/targets?state=active' | \
jq -r '.data.activeTargets[] | select(.labels.namespace == "kserve-test") | [.labels.pod, .health, .scrapeUrl] | @tsv'
检查空闲时的指标:
kubectl get --raw \
'/api/v1/namespaces/monitoring/services/http:prometheus-kube-prometheus-prometheus:http-web/proxy/api/v1/query?query=sum(vllm:num_requests_running%7Bnamespace=%22kserve-test%22,pod=~%22qwen-llm-predictor-.*%22%7D)%2Bsum(vllm:num_requests_waiting%7Bnamespace=%22kserve-test%22,pod=~%22qwen-llm-predictor-.*%22%7D)' | \
jq -r '[.status, .data.resultType, (.data.result | length), .data.result[0].value[1]] | @tsv'
实测输出:
success vector 1 0
KServe 创建 Deployment 和 ScaledObject,KEDA 再创建并管理 HPA:
kubectl get inferenceservice qwen-llm -n kserve-test
kubectl get scaledobject qwen-llm-predictor -n kserve-test
kubectl get hpa keda-hpa-qwen-llm-predictor -n kserve-test
kubectl get deployment qwen-llm-predictor -n kserve-test
kubectl get pod -n kserve-test \
-l serving.kserve.io/inferenceservice=qwen-llm
空闲状态下,各条命令输出中的关键字段如下:
qwen-llm-predictor READY=True ACTIVE=False MIN=1 MAX=2
keda-hpa-qwen-llm-predictor TARGETS=0/1 REPLICAS=1
qwen-llm-predictor READY=1/1
直接读取 HPA,确认缩容稳定窗口:
kubectl get hpa keda-hpa-qwen-llm-predictor -n kserve-test \
-o jsonpath='{.spec.behavior.scaleDown.stabilizationWindowSeconds}{"\n"}'
当前结果为 300。
验证自动扩缩容
验证扩容
先查询 Envoy Gateway 的地址,发送一次推理请求确认服务正常:
ENVOY_SERVICE=$(kubectl get service -n envoy-gateway-system \
-l gateway.envoyproxy.io/owning-gateway-name=kserve-ingress-gateway \
-o jsonpath='{.items[0].metadata.name}')
NODE_PORT=$(kubectl get service -n envoy-gateway-system "$ENVOY_SERVICE" \
-o jsonpath='{.spec.ports[?(@.port==80)].nodePort}')
NODE_IP=$(kubectl get node \
-o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}')
GATEWAY_ADDR="${NODE_IP}:${NODE_PORT}"
curl -H 'Host: qwen-llm-kserve-test.example.com' \
-H 'Content-Type: application/json' \
"http://${GATEWAY_ADDR}/openai/v1/chat/completions" \
-d '{
"model": "qwen",
"messages": [{"role": "user", "content": "Answer only with the number: 2+3"}],
"max_tokens": 8,
"temperature": 0
}'
产生持续负载
BODY='{"model":"qwen","messages":[{"role":"user","content":"Write a detailed numbered list from 1 to 300, with a complete sentence for every item."}],"max_tokens":768,"temperature":0.7}'
export BODY GATEWAY_ADDR
seq 12 | xargs -n 1 -P 12 bash -c '
while true; do
curl -sS --max-time 180 \
-H "Host: qwen-llm-kserve-test.example.com" \
-H "Content-Type: application/json" \
"http://${GATEWAY_ADDR}/openai/v1/chat/completions" \
-d "$BODY" >/dev/null
done
'
这条命令会一直占用当前终端,并将并发数保持为 12 持续发送请求。
观察扩容结果
另开两个终端,同时观察 Prometheus 指标以及扩缩容资源:
watch -n 5 'kubectl get --raw "/api/v1/namespaces/monitoring/services/http:prometheus-kube-prometheus-prometheus:http-web/proxy/api/v1/query?query=sum(vllm:num_requests_running%7Bnamespace=%22kserve-test%22,pod=~%22qwen-llm-predictor-.*%22%7D)%2Bsum(vllm:num_requests_waiting%7Bnamespace=%22kserve-test%22,pod=~%22qwen-llm-predictor-.*%22%7D)" | jq -r ".data.result[0].value[1]"'
watch -n 5 'kubectl get scaledobject,hpa,deployment,pod -n kserve-test'
本次实测时间线:
启动负载后 Prometheus 查询值升到 12
ScaledObject ACTIVE=True
HPA TARGETS=6/1,Deployment REPLICAS=2
约 250 秒后 第二个 vLLM 完成模型加载,两个 Pod 均为 1/1 Running
指标值是整个 Deployment 的 running + waiting 总数。两个副本、总值 12 时,平均每个副本为 6,因此 HPA 的目标列显示约 6/1。
扩容期间再次调用 Chat Completions 仍返回正确结果,说明 KEDA 已根据 vLLM 指标把服务从 1 个副本扩到 2 个。
验证缩容
回到运行压测的终端,按 Ctrl+C 停止全部请求。
先重复 Prometheus 查询,确认结果回到 0,再检查 ScaledObject 变成 ACTIVE=False。Deployment 不会立即缩容,因为当前生成的 HPA 带有 300 秒缩容稳定窗口:
behavior:
scaleDown:
stabilizationWindowSeconds: 300
实际缩容时间还会受到 KEDA 轮询、HPA 同步周期和 Pod 终止时间影响。使用以下命令等待 Deployment 回到一个 Ready 副本:
kubectl wait deployment/qwen-llm-predictor -n kserve-test \
--for=jsonpath='{.status.replicas}'=1 --timeout=8m
kubectl wait deployment/qwen-llm-predictor -n kserve-test \
--for=jsonpath='{.spec.replicas}'=1 --timeout=2m
kubectl wait deployment/qwen-llm-predictor -n kserve-test \
--for=jsonpath='{.status.readyReplicas}'=1 --timeout=2m
kubectl get hpa keda-hpa-qwen-llm-predictor -n kserve-test
kubectl get deployment qwen-llm-predictor -n kserve-test \
-o custom-columns=NAME:.metadata.name,SPEC:.spec.replicas,STATUS:.status.replicas,READY:.status.readyReplicas
kubectl get pod -n kserve-test \
-l serving.kserve.io/inferenceservice=qwen-llm
本次停止负载后,Prometheus 查询值回到 0,ScaledObject 变成 ACTIVE=False;约 299 秒后,HPA 将 Deployment 从 2 缩回 1。最后再次执行 Chat Completions 请求,仍然返回 5。
总结
- KServe 创建 Predictor Deployment 启动模型服务,vLLM 通过
/metrics暴露运行指标。 - Prometheus 采集
vllm:num_requests_running和vllm:num_requests_waiting。 - KEDA 查询 Prometheus,并通过 External Metrics API 将结果暴露为 HPA 可用的外部指标。
- HPA 根据指标与目标值计算期望副本数,调整 Predictor Deployment。
这套方案将模型服务的扩缩容信号从 CPU/内存利用率转为请求压力,更接近推理服务的实际容量瓶颈。生产落地前还应根据模型加载时间、GPU 可用性、指标延迟和缩容稳定窗口进行压测与参数校准。
生产治理与排障
冷启动
从 0 恢复到可处理第一条消息,包含 KEDA 轮询、HPA 同步、调度、镜像拉取和应用初始化。原整理稿给出的经验范围为 30~90 秒;模型服务可能因权重加载耗时更久。
- 延迟敏感服务将
minReplicaCount设为 1; - 预拉镜像并使用本地缓存;
- 在事件源可承受的前提下把
pollingInterval调至 10~15 秒; - 在 readinessProbe 通过后再接流,并为消息配置重试与死信队列;
- 同步 HTTP 缩零必须配置请求缓冲层。
认证与密钥
优先使用云 Workload Identity/IRSA 等短期身份,不把静态密钥直接写入 ScaledObject。必须使用 Secret 时,通过 TriggerAuthentication 引用并定期轮换;跨命名空间复用时使用 ClusterTriggerAuthentication,并严格限制 RBAC。
指标故障与 fallback
Prometheus 或云 API 不可用时,默认可能无法给 HPA 提供有效指标。KEDA 的 fallback 属于 ScaledObject 能力,不是所谓“Kubernetes v1.36 External Metrics Fallback”:
spec:
fallback:
failureThreshold: 3
replicas: 2
behavior: static
应按目标 KEDA 版本核对 fallback 支持的指标类型和行为,并分别告警“事件源无数据”“Scaler 读取失败”“External Metrics API 不可用”。
控制器冲突
每个可伸缩目标只保留一个副本控制入口:
kubectl get hpa -A
kubectl get scaledobject -A
kubectl describe scaledobject <name> -n <namespace>
kubectl describe hpa <name> -n <namespace>
若从 KEDA 迁移到原生 HPA,先用影子 Deployment 验证指标、缩零和冷启动,再切流并删除旧 ScaledObject。
升级
KEDA 升级可能包含 CRD、RBAC、API 与生成资源行为变化。升级前应:
- 阅读目标版本 Release Notes 和 Upgrade Notes;
- 比较 CRD 与 Helm values 差异;
- 在 staging 重放 ScaledObject、ScaledJob 和认证配置;
- 验证
external.metrics.k8s.io、Scaler 指标与回退策略; - 再按集群逐步升级。
不要假设 helm upgrade 会自动完成所有跨版本 CRD 迁移,具体步骤以目标版本说明为准。
Prometheus 告警示例
groups:
- name: autoscaling
rules:
- alert: HPAMetricsUnavailable
expr: kube_hpa_status_condition{condition="AbleToScale",status="false"} == 1
for: 5m
labels:
severity: warning
- alert: KEDAScalerErrors
expr: sum(rate(keda_scaler_errors[5m])) by (scaledObject, scaler) > 0
for: 5m
labels:
severity: warning
- alert: HPANearMaxReplicas
expr: kube_hpa_status_current_replicas / kube_hpa_spec_max_replicas > 0.8
for: 10m
labels:
severity: warning
指标名和 Label 取决于 kube-state-metrics、KEDA 与监控栈版本,部署前先在 Prometheus 中确认实际时序。
总结
- HPA 适合 CPU/内存或已有稳定指标适配链路的标准服务;
- Kubernetes 1.37 原生 HPA 已支持基于 Object/External 指标缩到 0;
- KEDA 的核心优势是事件源连接、认证、0↔1 激活、ScaledJob 与故障回退;
- 队列消费者、批处理和模型推理应优先选择能表达业务压力的指标;
- KServe + vLLM 场景中,
running + waiting比 CPU/内存更接近请求容量瓶颈; - 同一 Deployment 只能由一套扩缩容控制链管理。