HPA、VPA 与 KEDA 组合治理
大促预热刚开始,订单 API 的副本数就在 6、18、9 之间来回变化。HPA 事件显示 CPU 利用率超过目标,KEDA 生成的另一个 HPA 却根据队列长度要求缩容;GitOps 控制器又持续把 Deployment 的 spec.replicas 改回仓库中的 6。三个控制环都没有报错,Pod 也能陆续进入 Ready,但扩容速度抵不过请求增长,滚动发布还不断重启新副本。事故表面像指标抖动,实质是多个写入者争夺同一个 /scale 子资源。
值班人员临时删掉一个 HPA 后,振荡仍未完全停止。VPA 正以重建模式提高 CPU request,HPA 的 CPU 利用率分母随之变化:相同的实际 CPU 用量,因为 request 从 250m 变成 800m,在 HPA 看来从高负载变成低负载。与此同时,扩容出来的 Pod 因 request 增大而 Pending,节点伸缩器又开始申请新节点。这个现场说明,自动伸缩不是把三个控制器都安装上就结束了;副本、资源请求、指标、工作负载模板和交付声明必须各有唯一负责人。
三个控制器改变的不是同一种状态
HPA 是 Kubernetes 控制面中的水平伸缩控制器。它读取 Resource、Custom 或 External Metrics,为每个指标计算建议副本数,选择其中最大值,再通过目标对象的 /scale 子资源写入期望副本。autoscaling/v2 是稳定 API;CPU 利用率目标以容器 CPU request 为分母,没有对应 request 的 Pod 无法为该指标计算利用率。算法、缺失样本处理、稳定窗口与多指标规则可直接核对 Kubernetes HPA 概念和 autoscaling/v2 API。
VPA 是独立安装的控制器集合。Recommender 根据历史用量与 OOM 等信号生成 lowerBound、target、upperBound;Updater 决定哪些 Pod 需要被替换或调整;Admission Controller 在新 Pod 创建时修改容器 requests。VPA 1.7.0 使用 autoscaling.k8s.io/v1,Auto 已弃用,应显式选择 Off、Initial、Recreate、InPlaceOrRecreate 或 InPlace。Off 只给推荐,不改 Pod,是建立资源基线时风险最低的组合入口;具体安装和模式以 VPA 1.7.0 Quick Start为准。
KEDA 把队列长度、消费延迟或外部系统状态转换成 HPA 能消费的外部指标。对 ScaledObject,KEDA Operator 负责激活和缩零语义,并创建、维护一个 HPA;真正的 1 -> N 副本调整仍由 HPA 完成。KEDA 2.20 要求 Kubernetes 1.30+,CRD 仍为 keda.sh/v1alpha1。pollingInterval 决定 KEDA 查询事件源的频率,cooldownPeriod 主要约束回到零的等待,HPA 的 behavior 则控制非零区间的升降速率。两套时间参数不能混成一个“冷却时间”。
把运行链画开后,责任就清楚了:指标适配器拥有指标可用性,KEDA 拥有 ScaledObject 和生成的 HPA,HPA 拥有副本,VPA 在推荐模式只拥有 recommendation,在自动模式会拥有 Pod 的 resources 变更,GitOps 拥有其余声明。业务 owner 则拥有目标值、最大副本、冷启动预算和降级决策。
第一次启用前先固定版本和写入者
实验至少需要一个可销毁命名空间、Metrics Server、kubectl,以及允许安装 CRD、ClusterRole、APIService 和 admission webhook 的集群管理身份。HPA 本身随控制面启用;VPA 与 KEDA 是附加组件。不要在共享生产集群直接执行上游仓库 master 分支脚本,也不要把个人 kubeconfig 或云凭证写进 values。
先记录实际能力,而不是从控制台名称猜测:
kubectl version
kubectl api-resources | grep -E 'horizontalpodautoscalers|verticalpodautoscalers|scaledobjects'
kubectl get apiservice | grep -E 'metrics.k8s.io|external.metrics.k8s.io'
kubectl -n kube-system get deploy metrics-server若尚未安装 VPA,可从 vertical-pod-autoscaler-1.7.0 标签下载固定制品,审查 hack/vpa-up.sh 创建的 CRD、RBAC、Deployment、Service 与证书对象后再执行。脚本会写集群级对象,因此只能由平台管理员在隔离或获批集群运行:
git clone --depth 1 --branch vertical-pod-autoscaler-1.7.0 \
https://github.com/kubernetes/autoscaler.git vpa-1.7.0
cd vpa-1.7.0/vertical-pod-autoscaler
./hack/vpa-up.sh
kubectl -n kube-system get pods | grep vpa预期能看到 recommender、updater 和 admission-controller,CRD 为 Established;镜像拉取失败、证书 Job 失败或 webhook 没有 endpoints 时,不能继续创建自动更新模式的 VPA。KEDA 可使用官方 Helm Chart,并同时固定 Chart 与应用版本:
helm repo add kedacore https://kedacore.github.io/charts
helm repo update
helm upgrade --install keda kedacore/keda \
--namespace keda --create-namespace \
--version 2.20.1
kubectl -n keda rollout status deploy/keda-operator
kubectl -n keda rollout status deploy/keda-operator-metrics-apiserver
kubectl get crd scaledobjects.keda.sh verticalpodautoscalers.autoscaling.k8s.ioChart 字段会随发行线变化,执行前应在 KEDA 2.20 部署文档查看该 Chart 的实际 values。预期证据是三个 KEDA Deployment 完成 rollout、CRD Established、外部指标 API 可发现;只有 Pod Running 而 APIService 不可用时,KEDA 指标链仍然没有跑通。
用所有权合同约束 YAML 和 GitOps
一份可执行的所有权合同要精确到字段,而不是只写“平台负责伸缩”。下面的声明把副本交给 KEDA 生成的 HPA,把 CPU/内存 request 暂时保留在 Git,把 VPA 限制为推荐者:
示例沿用 Kubernetes 官方 HPA 演练镜像,仅用于可销毁环境验证控制链。团队模板必须将可变 tag 替换为经过扫描、签名校验和兼容验证的固定 digest,并通过组织镜像仓库分发。
apiVersion: apps/v1
kind: Deployment
metadata:
name: compose-api
namespace: autoscale-lab
spec:
replicas: 2 # 仅作首次创建值;调谐后不再由 GitOps 强制覆盖
selector:
matchLabels: {app: compose-api}
template:
metadata:
labels: {app: compose-api}
spec:
containers:
- name: app
image: registry.k8s.io/hpa-example:latest
resources:
requests: {cpu: 200m, memory: 128Mi}
limits: {cpu: 500m, memory: 256Mi}
---
apiVersion: v1
kind: Service
metadata:
name: compose-api
namespace: autoscale-lab
spec:
selector: {app: compose-api}
ports:
- port: 80
targetPort: 80
---
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: compose-api-recommendation
namespace: autoscale-lab
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: compose-api
updatePolicy:
updateMode: "Off"
resourcePolicy:
containerPolicies:
- containerName: app
controlledResources: [cpu, memory]
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: compose-api
namespace: autoscale-lab
spec:
scaleTargetRef:
name: compose-api
minReplicaCount: 2
maxReplicaCount: 12
pollingInterval: 15
cooldownPeriod: 300
advanced:
horizontalPodAutoscalerConfig:
name: compose-api-owner
behavior:
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 100
periodSeconds: 60
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 25
periodSeconds: 60
triggers:
- type: cpu
metricType: Utilization
metadata:
value: "60"这里的 maxReplicaCount 必须由下游容量反推:数据库连接、队列分区、第三方限流和节点最大供给中,最先耗尽的那一项决定上限。behavior.scaleUp 控制爆发速度,scaleDown.stabilizationWindowSeconds 保存窗口内较高的建议副本,避免刚过峰值就回收。KEDA 的 HPA 名称、所有权转移和恢复原副本行为可核对 ScaledObject 规范与工作负载伸缩说明。
GitOps 仓库可以保留首次 replicas,但同步策略必须忽略自动伸缩拥有的副本字段,或在创建后删除该字段。kubectl scale、发布脚本和人工控制台同样不得持续写它。忽略规则必须精确到这个 Deployment 的 /spec/replicas,不能忽略整个工作负载差异。
正向实验:让 KEDA 管副本,让 VPA 只给建议
在隔离集群创建命名空间并应用上面的三个对象,然后观察四层状态:
kubectl create namespace autoscale-lab
kubectl apply -f composition-safe.yaml
kubectl -n autoscale-lab get scaledobject,hpa,vpa,deploy
kubectl -n autoscale-lab describe scaledobject compose-api
kubectl -n autoscale-lab get hpa compose-api-owner -w预期 KEDA 创建且只创建一个名为 compose-api-owner 的 HPA;ScaledObject 的 Ready condition 为 True,HPA TARGETS 能读到 CPU 指标,VPA 逐渐产生 recommendation,但 Deployment 模板中的 request 不会被 VPA 改写。若 HPA TARGETS 长期是 <unknown>/60%,应先修复 Metrics API、容器 request 或访问权限,不要提高最大副本掩盖指标缺失。
启动一个临时负载发生器:
kubectl -n autoscale-lab run load --rm -it --restart=Never \
--image=busybox:1.36 -- \
/bin/sh -c 'while sleep 0.02; do wget -q -O- http://compose-api; done'清单中的同名 Service 把流量送到容器 80 端口。负载持续时,应看到 HPA 的当前利用率上升、DESIRED 副本增加、Deployment availableReplicas 随启动时间追上。停止发生器后,副本不应立即坠落,而应受稳定窗口和缩容速率限制。VPA recommendation 的 target 可能需要更长采样周期才出现;没有 recommendation 只能说明样本尚不足或链路异常,不能据此把 request 写成零。
把以下快照一起保存,才能解释一次扩缩:
kubectl -n autoscale-lab get hpa compose-api-owner -o yaml
kubectl -n autoscale-lab get vpa compose-api-recommendation -o yaml
kubectl -n autoscale-lab get deploy compose-api -o jsonpath='{.spec.replicas}{"\n"}{.status.availableReplicas}{"\n"}'
kubectl -n autoscale-lab get events --sort-by=.lastTimestamp
kubectl -n keda logs deploy/keda-operator --since=10m这个实验的判据不是“副本变多”,而是只有一个 HPA 写 /scale,KEDA condition、HPA 指标、Deployment 副本和 Pod Ready 时间能对上,VPA 只改变 recommendation,停止负载后控制环按策略回到稳定基线。
反向实验:让 request 漂移,观察分母改变决策
CPU 利用率 HPA 与会修改 CPU request 的 VPA 组合最容易产生隐蔽反馈。先保留 KEDA/HPA 的 60% 目标,把实验 VPA 改为 Recreate,并允许它控制 CPU:
kubectl -n autoscale-lab patch vpa compose-api-recommendation --type=merge -p '
{"spec":{"updatePolicy":{"updateMode":"Recreate"},"resourcePolicy":{"containerPolicies":[{"containerName":"app","controlledResources":["cpu"]}]}}}'
kubectl -n autoscale-lab get pods -w
kubectl -n autoscale-lab get hpa compose-api-owner -w继续施加相同负载,定期记录 Pod request、实际用量和 HPA 当前利用率:
kubectl -n autoscale-lab get pods -o custom-columns='POD:.metadata.name,CPU_REQUEST:.spec.containers[0].resources.requests.cpu'
kubectl -n autoscale-lab top pods
kubectl -n autoscale-lab get hpa compose-api-owner -o wide
kubectl -n autoscale-lab get events --sort-by=.lastTimestamp | tail -n 30当 VPA 替换 Pod 并提高 CPU request 时,即使实际用量接近不变,usage/request 也会降低,HPA 可能给出更小的期望副本。反过来,VPA 降低 request 会放大利用率并推动扩容。事件中还可能出现驱逐、重新调度和 Ready 空窗。实际幅度依赖采样与负载,不能预先承诺一定振荡;需要用同一时间轴上的 request、usage、HPA desired replicas 和 VPA recommendation 判断反馈是否存在。
恢复动作是先把 VPA 改回 Off,再将 Git 中经过容量评审的 requests 恢复并等待 Pod 稳定,而不是同时手工改副本:
kubectl -n autoscale-lab patch vpa compose-api-recommendation --type=merge \
-p '{"spec":{"updatePolicy":{"updateMode":"Off"}}}'
kubectl apply -f composition-safe.yaml
kubectl -n autoscale-lab rollout status deploy/compose-api如果业务必须同时水平和垂直自动调整,应让 HPA 使用与 request 无关的业务指标,例如每 Pod 并发或队列积压,并为 VPA 变更设置中断预算、request 上下界和观察窗口。即使如此,两条控制环仍通过调度容量耦合,需要分阶段上线。
冲突证据要从对象一直追到业务时间线
副本振荡先查写入者。managedFields 能看到谁更新 Deployment 资源,但 HPA 通过 /scale 写入时,GitOps、发布控制器和人工 patch 的时间线仍需结合审计日志与事件判断:
kubectl -n autoscale-lab get deploy compose-api -o json \
| jq '.metadata.managedFields[] | {manager,operation,subresource,time}'
kubectl -n autoscale-lab get hpa -o custom-columns='NAME:.metadata.name,TARGET:.spec.scaleTargetRef.name,MIN:.spec.minReplicas,MAX:.spec.maxReplicas'
kubectl -n autoscale-lab get scaledobject -o yaml常见分型有四种。ScalingActive=False 且指标 unknown,先查 Metrics API 或 scaler 凭证;多个 HPA 指向同一 target,是副本多写者;HPA 方向合理但 Pod Pending,是 requests、亲和性或节点供给问题;Pod 已 Ready 但业务延迟不降,则副本不是瓶颈,可能受连接池、分区数、锁或下游配额限制。
KEDA 还会暴露 Ready、Active、Fallback、Paused 等 condition。外部指标暂时失败时,是否维持、回退还是缩容取决于 scaler 与 fallback 配置。告警不能只看 HPA 副本,应关联指标新鲜度、KEDA reconcile 错误、HPA condition、Pending 数、Ready 延迟和业务 SLO。
RBAC、外部凭证与敏感容量数据
HPA 控制器需要读取指标并更新目标 /scale;VPA Admission Controller 能修改 Pod,Updater 能驱逐 Pod;KEDA Operator 创建 HPA、读取 TriggerAuthentication,Metrics API Server 对外提供指标。这些都是高影响权限,不能把三个 ServiceAccount 统一绑定 cluster-admin。
KEDA 连接消息队列、云监控或数据库时,优先使用 workload identity、短期令牌或命名空间内的 TriggerAuthentication。跨命名空间的 ClusterTriggerAuthentication 扩大了凭证影响面,应由平台 owner 审批。Secret 只保存引用值,不在 ScaledObject、日志、海报或工单里出现连接串。外部指标名称和标签也可能暴露租户、队列、订单量与业务峰值,应限制 external.metrics.k8s.io 的读取者并控制观测平台保留。
VPA recommendation 会暴露工作负载的 CPU、内存边界,HPA/KEDA 状态会暴露流量和积压。多租户平台要分别限制 CRD 的创建、更新和状态读取,审计谁改变了 maxReplicaCount、updateMode、认证引用和 fallback。离职或服务退出时,撤销云身份、队列只读账号和 KMS grant,不能只删除 Kubernetes Secret。
容量和成本必须沿整条控制链计算
水平扩容的成本不只是 Pod request。每个新副本还会增加 sidecar、日志、指标序列、连接池、缓存预热、镜像拉取和节点碎片。maxReplicaCount=100 若数据库只允许新增 200 个连接,而每 Pod 连接池上限为 20,真实安全上限可能不到 10。KEDA 的轮询频率也可能增加外部监控 API 调用费用或触发限流。
垂直调整会改变调度装箱。request 偏小导致节点容量看似充足却运行过载,偏大则让 Pod Pending、节点扩容和空闲成本同时增加。VPA 推荐应跨完整业务周期观察,并区分常驻基线、突发峰值、启动峰值与 OOM 保护;不能把一次 target 自动提交到所有环境。
容量评审至少保存四个量:业务单位负载所需副本、每 Pod 稳态与启动 requests、节点可供给上限、下游硬限制。扩容恢复时间还要拆成指标采样、控制器调谐、调度、镜像拉取、应用启动和 readiness 六段。若业务峰值上升速度快于这条链,保留最小热副本或定时预扩容通常比盲目提高最大副本更可靠。
架构选型从控制变量开始
只需按 CPU 或内存副本伸缩,且 requests 稳定时,原生 HPA 最简单,故障面也最小。需要队列、事件源、缩零或 scaler 认证时,由 KEDA 创建唯一 HPA;不要再为同一工作负载保留手写 HPA。需要校正 requests 时,先以 VPA Off 形成推荐流程,再决定人工合并、Initial 或受控自动模式。
在线低延迟服务通常优先 HPA/KEDA 管副本,VPA 只推荐;长生命周期、有连接或本地状态的 Pod,要把缩容、驱逐和启动代价纳入 PDB 与终止流程。批处理任务若“一条事件对应一个 Job”,应评估 KEDA ScaledJob,不要硬把它改造成常驻 Deployment。无法并行、受单分区或串行锁限制的服务,水平伸缩不会增加有效吞吐,应先修复并行模型。
选择的关键不是控制器数量,而是每个业务变量只有一个闭环:副本、requests、事件激活、节点供给分别有 owner,闭环之间通过明确指标和容量预算连接。任何新控制器上线前,都要能回答它写哪个对象、失败时保持什么状态、谁能暂停、怎样归还字段。
升级、回滚和退出要先归还所有权
升级顺序从 CRD 与 API 兼容开始,再升级控制器和 webhook,最后启用新字段。HPA autoscaling/v2beta2 已被移除,旧清单必须先迁移到 autoscaling/v2。KEDA 和 VPA 都应固定发行版本与镜像摘要,在隔离命名空间回放指标缺失、外部限流、Pod 重建和回滚。升级 webhook 时尤其要验证“不可用时是否阻塞 Pod 创建”。
暂停 KEDA 可以使用其支持的暂停注解或先把伸缩边界固定为当前安全副本;退出时先记录当前副本,再删除 ScaledObject,确认生成的 HPA 是否按预期移除。restoreToOriginalReplicaCount 默认不是恢复开关:默认删除 ScaledObject 会保留删除时的副本数,设为 true 才尝试回到创建时副本,因此退出值应由当下容量事实决定。
退出 VPA 时先切到 Off,等待自动驱逐停止,再删除 VPA;删除 admission webhook 前确认没有其他 VPA 依赖。退出 HPA 时先把 Deployment 固定到经评审的副本并恢复 Git 对 replicas 的所有权,随后删除 HPA。最后撤销 TriggerAuthentication、云身份、RBAC 和指标适配器权限,检查孤儿 HPA、webhook、APIService 和 Secret 引用归零。
团队应为每个工作负载保存一张所有权记录:目标对象、唯一副本写入者、资源 request 写入者、指标 owner、最大副本依据、暂停入口、回滚值、告警与审批人。它比“自动伸缩已开启”的标签更重要,因为控制环真正安全的标志不是动作频繁,而是在指标异常、发布、升级和退出时仍能解释并收敛。
清理隔离实验并证明没有残留写入者
实验结束先停止负载,再删除控制对象,最后删除工作负载和命名空间:
kubectl -n autoscale-lab delete scaledobject compose-api --ignore-not-found
kubectl -n autoscale-lab delete vpa compose-api-recommendation --ignore-not-found
kubectl -n autoscale-lab get hpa
kubectl delete namespace autoscale-lab
kubectl get hpa,vpa,scaledobject -A若最后一条仍看到指向实验目标的 HPA,先查 KEDA finalizer、Operator 日志与对象 ownerReference,不要直接删除 CRD。共享集群中的 KEDA、VPA 和 Metrics Server 属于平台组件,不能因单篇实验而卸载。只有专用临时集群才可以按相反安装顺序卸载 Chart、控制器与 CRD,并在删除 CRD 前确认没有其他命名空间对象。清理完成的证据是实验命名空间消失、没有孤儿伸缩对象、没有外部凭证引用、GitOps 不再尝试恢复实验资源。
