前言:弹性伸缩是 SaaS 的生命线
对于 Java SaaS 产品,流量从来不是匀速的:工作日早 9 点打卡高峰、月底账单批处理、突发的营销活动......固定资源配置意味着要么高峰期撑不住,要么平峰期白白烧钱。
K8s 提供了三种层次的弹性伸缩机制:
- HPA(Horizontal Pod Autoscaler):横向扩容,增减 Pod 副本数
- VPA(Vertical Pod Autoscaler):纵向扩容,调整单个 Pod 的 CPU/Memory 配额
- KEDA(Kubernetes Event-Driven Autoscaling):事件驱动伸缩,基于 Kafka 消息积压、Redis 队列深度等自定义指标扩容
三者关注点不同,实际生产中往往组合使用。本文逐一拆解,并结合 Java SaaS 和 AI 推理服务的真实场景给出配置方案。
一、HPA:基于负载自动横向扩容
1.1 HPA 工作原理
HPA 控制器每隔 15 秒(默认)查询一次 Metrics Server 获取 Pod 的实时指标,与目标值对比,计算期望副本数:
bash
期望副本数 = ceil(当前副本数 × (当前指标值 / 目标指标值))
例:当前 3 个 Pod,CPU 平均使用率 80%,目标 50%
期望副本数 = ceil(3 × 80/50) = ceil(4.8) = 5
K8s 内置了"冷却时间"机制防止抖动:扩容冷却 0 秒(快速响应),缩容冷却 300 秒(避免频繁缩容)。
bash
流量上升
│
▼ CPU > 目标值
HPA 触发扩容 → 新 Pod 启动 → readinessProbe 通过 → Service 纳入负载
│
流量下降
│
▼ CPU < 目标值(持续 5 分钟)
HPA 触发缩容 → Pod 优雅终止 → 副本数降低
1.2 安装 Metrics Server
HPA 依赖 Metrics Server 采集 Pod 的 CPU/Memory 实时数据,很多集群默认未安装:
bash
# 安装 Metrics Server
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
# 如果集群没有有效 TLS 证书(如 minikube/kubeadm),需要加 --kubelet-insecure-tls
kubectl patch deployment metrics-server -n kube-system \
--type json \
-p '[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]'
# 验证安装(需等约 1 分钟)
kubectl top nodes
kubectl top pods -n production
1.3 为 Java SaaS API 配置 HPA
最基础的 CPU 指标 HPA:
yaml
# hpa-java-saas-api.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: java-saas-api-hpa
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: java-saas-api
minReplicas: 2 # 最少保持 2 个副本(高可用底线)
maxReplicas: 20 # 最多扩到 20 个副本(成本上限)
metrics:
# 指标 1:CPU 使用率(最常用)
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60 # 目标 CPU 使用率 60%(留 40% buffer)
# 指标 2:内存使用率(Java 应用内存增长也是扩容信号)
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 70
behavior:
scaleUp:
stabilizationWindowSeconds: 0 # 扩容无冷却,快速响应洪峰
policies:
- type: Percent
value: 100
periodSeconds: 60 # 每 60 秒最多扩容 100%(副本数翻倍)
- type: Pods
value: 4
periodSeconds: 60 # 或每 60 秒最多增加 4 个 Pod
selectPolicy: Max # 取两个策略中扩容量较大的
scaleDown:
stabilizationWindowSeconds: 300 # 缩容等待 5 分钟,防止抖动
policies:
- type: Percent
value: 25
periodSeconds: 60 # 每 60 秒最多缩容 25%
⚠️ 踩坑记录 #1:HPA 扩容了但请求还是超时
这是 Java 应用特有的问题:JVM 冷启动慢(尤其是 Spring Boot,启动可能需要 30-60 秒),HPA 触发扩容后新 Pod 进入 Running 状态,但 Spring Boot 还没初始化完,流量已经打进来,导致大量 503 错误。
解决方案:
- readinessProbe 精确配置(第 01 篇已讲):确保 Spring Boot 完全启动才接收流量
- 预热 + 提前扩容:在可预期的大促前,提前手动扩容到高水位,不依赖 HPA 的被动响应
- 使用 GraalVM Native Image:彻底解决 JVM 冷启动问题(启动时间 <1 秒),但编译复杂
- StartupProbe 配置:给 Java 应用配置 startupProbe,告诉 K8s "我需要时间启动,别急着判断失败"
配置 startupProbe 解决 Java 慢启动问题:
yaml
spec:
containers:
- name: spring-boot-app
startupProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
# failureThreshold × periodSeconds = 最长允许启动时间
# 10 × 10 = 100 秒,给 Spring Boot 足够的启动时间
failureThreshold: 10
periodSeconds: 10
initialDelaySeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
periodSeconds: 5 # startupProbe 通过后,readinessProbe 才开始检测
1.4 基于自定义指标的 HPA
CPU/Memory 指标并不总是能反映真实负载。对于 Java SaaS API 服务,更好的扩容指标可能是"每秒请求数(RPS)"或"请求队列深度"。这需要配合 Prometheus Adapter:
yaml
# 使用 Prometheus 自定义指标(需安装 prometheus-adapter)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: java-saas-api-hpa-custom
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: java-saas-api
minReplicas: 2
maxReplicas: 20
metrics:
# 基于 RPS 扩容:每个 Pod 处理 100 RPS,超出则扩容
- type: Pods
pods:
metric:
name: http_requests_per_second # Prometheus 中的指标名
target:
type: AverageValue
averageValue: "100" # 每个 Pod 目标 100 RPS
二、VPA:自动调整 Pod 的资源配额
2.1 VPA 解决什么问题?
HPA 解决的是"几个 Pod"的问题,VPA 解决的是"每个 Pod 给多少资源"的问题。
Java 应用的资源配额很难一开始就设准------设少了 OOM Kill,设多了浪费。VPA 通过观察历史资源使用情况,自动推荐或应用最优的 requests/limits 配置。
VPA 有三种模式:
| 模式 | 行为 | 适用场景 |
|---|---|---|
Off |
只提供推荐值,不自动修改 | 用于观察和手动调优 |
Initial |
只在 Pod 创建时应用推荐值 | 适合批处理 Job |
Auto |
自动修改并重启 Pod | 资源浪费严重时使用 |
⚠️ 踩坑记录 #2:VPA Auto 模式与 HPA 冲突
VPA 和 HPA 不能同时基于 CPU/Memory 指标运行。VPA 修改 Pod 的 requests,会影响 HPA 的计算基准,导致两者互相干扰、副本数不稳定。
正确做法:如果同时使用,HPA 用自定义指标(RPS、队列深度),VPA 负责调整资源配额。或者只用其中一个。
2.2 配置 VPA 观察模式
在生产环境,先用 Off 模式观察推荐值,再手动采纳,更稳妥:
yaml
# 安装 VPA
kubectl apply -f https://github.com/kubernetes/autoscaler/releases/download/vpa-1.0.0/vertical-pod-autoscaler.yaml
# vpa-java-saas.yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: java-saas-api-vpa
namespace: production
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: java-saas-api
updatePolicy:
updateMode: "Off" # 观察模式,不自动修改
resourcePolicy:
containerPolicies:
- containerName: spring-boot-app
minAllowed:
cpu: 100m # 资源配额下限
memory: 256Mi
maxAllowed:
cpu: 4 # 资源配额上限(防止 VPA 推荐过高)
memory: 8Gi
查看 VPA 推荐值:
bash
kubectl describe vpa java-saas-api-vpa -n production
# 输出示例:
# Recommendation:
# Container Recommendations:
# Container Name: spring-boot-app
# Lower Bound:
# Cpu: 200m
# Memory: 512Mi
# Target: ← 建议值
# Cpu: 450m
# Memory: 768Mi
# Upper Bound:
# Cpu: 2
# Memory: 2Gi
三、KEDA:事件驱动的弹性伸缩
3.1 KEDA 的核心价值
HPA 依赖 CPU/Memory 这类系统级指标,但很多场景的真实负载信号来自业务层:
- Kafka Topic 有 10 万条消息积压 → 需要增加消费者 Pod
- Redis 任务队列长度超过阈值 → 需要扩容 Worker
- 外部 AI 推理请求队列堆积 → 需要扩容 GPU Pod
- 凌晨 2 点没有任何请求 → 可以缩容到 0(节省成本)
KEDA(Kubernetes Event-Driven Autoscaling)正是为这类场景设计的,它支持 60+ 种触发器,包括 Kafka、RabbitMQ、Redis、Prometheus、Azure Queue 等。
KEDA 相比原生 HPA 最大的差异 :支持缩容到 0!这意味着凌晨无流量时,你的 AI 推理服务可以完全停止,到来请求时再冷启动,大幅节省 GPU 成本。
3.2 安装 KEDA
bash
helm repo add kedacore https://kedacore.github.io/charts
helm repo update
helm install keda kedacore/keda --namespace keda --create-namespace
3.3 场景一:基于 Kafka 消息积压的 Java 消费者伸缩
这是 Java SaaS 最常见的场景:AI 任务异步处理,任务发到 Kafka,Consumer 根据积压量自动扩缩容:
yaml
# keda-kafka-java-consumer.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: java-ai-worker-scaledobject
namespace: production
spec:
scaleTargetRef:
name: java-ai-worker # 目标 Deployment
pollingInterval: 15 # 每 15 秒检查一次触发器
cooldownPeriod: 60 # 缩容冷却 60 秒
minReplicaCount: 1 # 最少 1 个(0 会导致冷启动延迟)
maxReplicaCount: 30 # 最多 30 个消费者
triggers:
- type: kafka
metadata:
bootstrapServers: kafka-service.production.svc.cluster.local:9092
consumerGroup: java-ai-worker-group
topic: ai-task-queue
# lagThreshold: 每个 Pod 处理的消息积压阈值
# 积压 1000 条 → 需要 1 个 Pod
# 积压 5000 条 → 需要 5 个 Pod
lagThreshold: "1000"
offsetResetPolicy: latest
对应的 Java Kafka 消费者 Deployment:
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: java-ai-worker
namespace: production
spec:
replicas: 1 # KEDA 接管后,这个值会被覆盖
selector:
matchLabels:
app: java-ai-worker
template:
metadata:
labels:
app: java-ai-worker
spec:
# 优雅关闭:让当前处理中的消息处理完再终止
terminationGracePeriodSeconds: 120
containers:
- name: ai-worker
image: your-registry/java-ai-worker:latest
env:
- name: KAFKA_BOOTSTRAP_SERVERS
value: "kafka-service:9092"
- name: KAFKA_GROUP_ID
value: "java-ai-worker-group"
- name: JAVA_OPTS
value: "-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "2000m"
Java 消费者的优雅关闭配置(Spring Kafka):
java
// Spring Boot 配置类
@Configuration
public class KafkaConsumerConfig {
@Bean
public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory(
ConsumerFactory<String, String> consumerFactory) {
ConcurrentKafkaListenerContainerFactory<String, String> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory);
// 关键:设置停止时等待当前消息处理完成
factory.getContainerProperties()
.setShutdownTimeout(Duration.ofSeconds(100).toMillis());
return factory;
}
}
3.4 场景二:AI 推理服务按请求队列扩缩容(含缩容到 0)
对于 GPU 密集型的 AI 推理服务,成本敏感是第一位的。使用 KEDA + Redis 队列,实现请求来了再启动 GPU Pod:
yaml
# keda-ai-inference-scaledobject.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: ai-inference-scaledobject
namespace: ai-platform
spec:
scaleTargetRef:
name: llm-inference-service
pollingInterval: 10
cooldownPeriod: 300 # GPU Pod 缩容冷却 5 分钟(冷启动慢,别频繁缩)
minReplicaCount: 0 # 关键:允许缩容到 0,无请求时节省 GPU 成本
maxReplicaCount: 4 # 最多 4 个 GPU Pod(GPU 资源有限)
triggers:
- type: redis
metadata:
address: redis-service.production.svc.cluster.local:6379
listName: ai-inference-queue # Redis List 作为任务队列
listLength: "5" # 每个 Pod 处理 5 个排队请求
authenticationRef:
name: redis-trigger-auth # Redis 密码认证
---
# Redis 认证配置
apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
name: redis-trigger-auth
namespace: ai-platform
spec:
secretTargetRef:
- parameter: password
name: java-saas-secret
key: REDIS_PASSWORD
⚠️ 踩坑记录 #3:缩容到 0 后第一个请求超时
minReplicaCount: 0意味着无流量时 GPU Pod 完全停止。当第一个请求到来,KEDA 触发扩容,Pod 从 0→1 需要时间(GPU 节点调度 + vLLM 模型加载,可能需要 3-5 分钟!)。解决方案:
- 请求缓冲:在 Java API 层做请求队列,收到 AI 请求后立刻返回任务 ID(异步模式),客户端轮询结果,这样扩容时间对用户无感
- 预热调度:基于历史数据,在业务高峰前(如每天早上 8:45)提前用 CronJob 触发扩容
- 保留最小副本 :
minReplicaCount: 1,始终保留 1 个待命 GPU Pod(成本换体验)
3.5 场景三:定时伸缩(Cron 触发器)
SaaS 系统的流量高峰往往是可预测的,用 Cron 触发器提前扩容,比被动响应更优雅:
yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: java-saas-cron-scaler
namespace: production
spec:
scaleTargetRef:
name: java-saas-api
triggers:
# 工作日早 8:30 提前扩容(早于用户高峰)
- type: cron
metadata:
timezone: Asia/Shanghai
start: "30 8 * * 1-5" # 周一到周五早 8:30
end: "0 20 * * 1-5" # 周一到周五晚 20:00
desiredReplicas: "10" # 工作时间保持 10 个副本
# 月末账单高峰(每月最后 3 天全天高配)
- type: cron
metadata:
timezone: Asia/Shanghai
start: "0 0 28 * *" # 每月 28 号 0 点
end: "0 0 1 * *" # 下月 1 号 0 点
desiredReplicas: "20" # 月末保持 20 个副本
四、Cluster Autoscaler:节点级弹性伸缩
Pod 层面的 HPA/KEDA 扩容到一定程度,Node 资源不够了怎么办?Cluster Autoscaler 会自动向云厂商申请新的 Node:
bash
HPA/KEDA 触发扩容
│
▼ 新 Pod 因资源不足无法调度 (Pending 状态)
Cluster Autoscaler 检测到 Pending Pod
│
▼ 向云厂商 API 申请新 Node(弹性伸缩组)
新 Node 加入集群(约 2-3 分钟)
│
▼ Pending Pod 被调度到新 Node
对于云厂商托管 K8s(阿里云 ACK、AWS EKS、Google GKE),Cluster Autoscaler 通常开箱即用,在节点池配置中开启"自动伸缩"即可。
五、常见问题 FAQ
Q1:HPA 的 averageUtilization 目标值设多少合适?
Java 应用建议设在 60-70%。留 30-40% buffer 的原因:① Java GC 会瞬间拉高 CPU;② 扩容有时延,高峰期需要 buffer 撑过扩容窗口。太低(<50%)会频繁扩容浪费资源,太高(>80%)在扩容完成前容易崩溃。
Q2:KEDA 和 HPA 能同时给一个 Deployment 用吗?
可以共存,但要避免指标冲突。KEDA 本质上是在 K8s HPA 之上封装的(KEDA 会创建一个 HPA 对象),建议一个 Deployment 只配置一个 ScaledObject,避免多个 HPA 互相覆盖。
Q3:如何防止大促期间 HPA 缩容把刚扩出来的 Pod 又缩回去?
临时关闭 HPA 的缩容行为:
bash
kubectl patch hpa java-saas-api-hpa -n production \
-p '{"spec":{"behavior":{"scaleDown":{"policies":[{"type":"Pods","value":0,"periodSeconds":3600}]}}}}'
# 大促结束后恢复
kubectl apply -f hpa-java-saas-api.yaml
Q4:Pod 扩容后 JVM 堆没扩,还是 OOM 怎么办?
HPA 扩容增加的是 Pod 数量,不改变单个 Pod 的内存 limits。如果单 Pod 的 JVM 堆确实不够,要通过 VPA 或手动修改 Deployment 的 resources.limits.memory 来解决,同时调整 -XX:MaxRAMPercentage 让 JVM 自动适配新的内存上限。
Q5:KEDA 缩容到 0 后,流量来了请求会丢失吗?
会!从 0 扩容到 1 需要时间,这期间到来的请求会因为 Service 后端无 Pod 而返回 503。必须配合异步处理模式(把请求存入队列再返回任务 ID)或 KEDA 的 ScaledJob(每个请求创建一个 Job 而非长运行服务)来解决。
总结
| 工具 | 维度 | 触发指标 | 典型场景 |
|---|---|---|---|
| HPA | Pod 数量 | CPU、Memory、自定义 | API 服务流量波动 |
| VPA | Pod 资源配额 | 历史用量 | 资源配额调优 |
| KEDA | Pod 数量(支持到 0) | Kafka、Redis、Cron 等 60+ | 异步任务、AI 推理、定时高峰 |
| Cluster Autoscaler | Node 数量 | Pending Pod | 弹性扩缩节点 |
💬 一句话总结:HPA 是"CPU 表压高了加缸",KEDA 是"订单堆多了加人",VPA 是"给每个员工配好合适的工作台"------三层弹性伸缩协同工作,让 Java SaaS 既能从容应对洪峰,又能在低谷时精打细算。
上一篇 :K8s 网络深度解析:Ingress、Service Mesh 与 CoreDNS------Java 微服务通信全链路剖析(生产级实战)
下一篇 :K8s CI/CD 全流程:GitOps × ArgoCD × Harbor------Java SaaS 从代码提交到生产部署一键直达
📝 系列文章持续更新中,欢迎关注、收藏、点赞三连支持!