第04篇:K8s 弹性伸缩实战:HPA、VPA、KEDA——Java SaaS 应对流量洪峰的秘密武器

前言:弹性伸缩是 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 错误。

解决方案

  1. readinessProbe 精确配置(第 01 篇已讲):确保 Spring Boot 完全启动才接收流量
  2. 预热 + 提前扩容:在可预期的大促前,提前手动扩容到高水位,不依赖 HPA 的被动响应
  3. 使用 GraalVM Native Image:彻底解决 JVM 冷启动问题(启动时间 <1 秒),但编译复杂
  4. 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 分钟!)。

解决方案

  1. 请求缓冲:在 Java API 层做请求队列,收到 AI 请求后立刻返回任务 ID(异步模式),客户端轮询结果,这样扩容时间对用户无感
  2. 预热调度:基于历史数据,在业务高峰前(如每天早上 8:45)提前用 CronJob 触发扩容
  3. 保留最小副本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 从代码提交到生产部署一键直达

📝 系列文章持续更新中,欢迎关注、收藏、点赞三连支持!

相关推荐
石山代码5 小时前
ArrayList / HashMap / ConcurrentHashMap
java·开发语言
AskHarries6 小时前
系统提示词、开发者指令和用户输入的优先级
java·前端·数据库
gsls2008087 小时前
JVM 堆内存参数 & Docker 容器适配,一次讲清楚
jvm·docker·容器
daidaidaiyu7 小时前
ThingsBoard 规则链系统源码分析和自定义定时器
java
小毛驴8507 小时前
spring-boot-maven-plugin,maven-compiler-plugin 功能对比
java·python·maven
csdn_aspnet8 小时前
Java 霍尔分区算法(Hoare‘s Partition Algorithm)
java·开发语言·算法
霸道流氓气质8 小时前
通义灵码 IDEA 插件完全使用指南
java·ide·intellij-idea
诸葛务农8 小时前
道路行驶条件下电动汽车永磁电机的有效使用寿命及永磁体的失效和回收再利用(下)
java·开发语言·算法
Percep_gan8 小时前
Java8中的stream的测试使用
java