弹性伸缩原理

业务流量不是一条平滑的曲线,而是峰谷分明的波浪。大促高峰可能是日常的 10 倍甚至 100 倍。如果按照峰值准备资源,平时就是巨大的浪费;如果按照平时准备资源,大促就会崩溃。弹性伸缩,就是让系统「能屈能伸」的技术。

基于指标的扩缩容

弹性伸缩的核心是「根据指标自动调整实例数量」。常见的触发指标有三类:资源利用率、请求特征、业务指标。

资源利用率指标

CPU 利用率:最常用的扩容指标。当 CPU 利用率超过阈值(如 70%),说明计算资源紧张,需要扩容;当利用率持续低于阈值(如 30%),说明资源过剩,可以缩容。

Kubernetes
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-server
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-server
  minReplicas: 3
  maxReplicas: 100
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70

内存利用率:适合内存密集型应用。但内存不像 CPU 那样能快速释放,扩容决策需要更谨慎。

磁盘 I/O:适合 I/O 密集型应用,如日志处理、数据导入导出。

请求特征指标

每秒请求数(QPS):直接反映负载水平。缺点是需要预估每实例的 QPS 上限,这个值在不同业务场景下差异巨大。

请求等待队列长度:比 QPS 更敏感。当队列开始积压时,说明处理能力已经不足,比 CPU 利用率更早发出预警。

连接数:数据库连接池、HTTP 连接数等。连接数接近上限时必须扩容。

业务指标

订单量:电商大促场景,订单量是最真实的负载指标。但这个指标通常需要业务系统提供,延迟较高。

活跃用户数:游戏、社交场景。活跃用户数突增时,需要预留更多资源。

消息队列积压量:消息队列的积压量直接反映下游消费能力。积压超过阈值时扩容消费者。

冷却时间与震荡问题

弹性伸缩看似简单,但实际使用中最大的坑是「震荡」——系统不断在扩容和缩容之间来回切换,像一个不稳定的天平。

冷却时间

冷却时间(Cooldown Period)是防止震荡的关键配置。扩容后,即使指标回落,也需要等待冷却时间结束才开始缩容;缩容后,即使指标上升,也需要等待冷却时间结束才开始扩容。

HPA
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-server
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-server
  minReplicas: 3
  maxReplicas: 100
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300  # 缩容窗口:5分钟
      policies:
      - type: Percent
        value: 10
        periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 0  # 扩容无需等待
      policies:
      - type: Percent
        value: 100
        periodSeconds: 15

震荡的根因

震荡通常由以下原因导致:

指标选取不当:使用 CPU 作为扩容指标,但业务特点是「请求来了 CPU 飙升,处理完立刻下降」。扩容需要时间(30-60 秒),等实例启动好,CPU 已经降下来了,于是开始缩容。周而复始。

阈值设置不合理:扩容阈值太低(如 50%),导致频繁扩容;缩容阈值太低(如 20%),导致频繁缩容。

实例启动时间过长:实例启动需要 2-3 分钟,这段时间内指标已经回落,缩容决策会撤销刚完成的扩容。

解决方案

使用更平滑的指标:不用瞬时值,改用一段时间内的平均值。Kubernetes HPA 支持 targetAverageValuetargetAverageUtilization

合理设置阈值和窗口:扩容阈值和缩容阈值之间保持足够距离(如 50% 扩容、20% 缩容),给系统留出缓冲空间。

设置扩容上限和冷却时间:防止单次扩容幅度过大,设置 scaleUp.policies 限制扩容速率。

预热机制:对于已知的流量高峰(如大促),提前手动扩容,而不是等待指标触发。

容量规划与弹性上限

弹性伸缩不是万能的,它有一个「弹性上限」。这个上限由两部分构成:系统容量上限和业务约束。

系统容量上限

物理资源上限:云环境下是账户配额,私有环境是机器数量。HPA 的 maxReplicas 应该设置在这个上限之内。

依赖系统上限:应用实例扩容了,但数据库连接池只有 100 个。如果每个实例需要 10 个连接,10 个实例就满了。扩容再多也没用。

带宽上限:公网带宽、负载均衡器吞吐量、消息队列消费能力,都可能成为瓶颈。

业务约束

冷启动延迟:Serverless 环境下,冷启动可能需要几秒钟。对于延迟敏感的业务,这个代价太高。

数据一致性:扩容意味着更多实例处理请求。如果涉及分布式事务,更多的参与者意味着更高的冲突概率和更长的等待时间。

成本约束:弹性伸缩是为了省钱,不是为了烧钱。如果业务量稳定,固定容量的成本可能更低。

容量规划建议

建立容量模型:基于业务指标(DAU、转化率、峰值 QPS)和系统指标(CPU、内存、I/O)的关联关系,建立容量规划模型。这个模型能回答「DAU 100 万时需要多少实例」。

设置弹性边界:在容量模型的基础上,设置合理的 minReplicasmaxReplicas。最小副本数保证基础可用性,最大副本数防止失控扩容。

保留应急储备:预留 20-30% 的余量应对突发流量。不等到资源耗尽才扩容。

Kubernetes HPA/VPA 实战

Kubernetes 提供了两种自动扩缩容机制:HPA(Horizontal Pod Autoscaler)和 VPA(Vertical Pod Autoscaler)。

HPA 详解

HPA 是最常用的弹性伸缩机制,通过调整 Pod 副本数来应对负载变化。

基础配置

# 创建 HPA,目标 Deployment 最小 2 个副本,最大 10 个副本
kubectl autoscale deployment api-server \
  --min=2 --max=10 \
  --cpu-percent=70

基于多指标的配置

hpa-multiple-metrics.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-server
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-server
  minReplicas: 2
  maxReplicas: 50
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80
  - type: Pods
    pods:
      metric:
        name: http_requests_per_second
      target:
        type: AverageValue
        averageValue: "1000"

VPA 详解

VPA 通过调整 Pod 的资源请求(requests)来优化调度,适合内存或 CPU 需求随时间变化的场景。

vpa.yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: api-server-vpa
spec:
  targetRef:
    apiVersion: "apps/v1"
    kind: Deployment
    name: api-server
  updatePolicy:
    updateMode: "Auto"  # Auto 会 evict 并重建 Pod
  resourcePolicy:
    containerPolicies:
    - containerName: '*'
      minAllowed:
        cpu: 100m
        memory: 128Mi
      maxAllowed:
        cpu: 4
        memory: 8Gi

HPA vs VPA

维度HPAVPA
调整对象Pod 副本数Pod 资源请求
适用场景无状态服务、流量波动有状态服务、资源需求稳定
扩容速度快(启动新 Pod)慢(需要驱逐重建)
最小实例数受 minReplicas 限制可能缩容到 1 个 Pod
冷启动问题新 Pod 需要时间启动无冷启动问题

实际生产中,HPA 使用更广泛,VPA 通常用于辅助调优或特定场景。

CronHPA:定时扩缩容

对于可预测的流量波动(如每天的早高峰、每周的大促),可以使用 CronHPA 实现定时扩缩容。

cronhpa.yaml
apiVersion: autoscaling.cronhpa.io/v1
kind: CronHorizontalPodAutoscaler
metadata:
  name: api-server-cronhpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-server
  excludeDates:
  - "2024-12-25"  # 圣诞节不扩容
  crons:
  - name: "morning-peak"
    at: "0 8 * * 1-5"  # 工作日早上 8 点
    targetSize: 20
  - name: "night-offpeak"
    at: "0 22 * * 1-5"  # 工作日晚上 10 点
    targetSize: 5

常见误区

误区一:弹性伸缩能解决所有性能问题

弹性伸缩只能解决「容量不足」的问题。如果性能问题是代码 Bug、数据库慢查询、架构设计缺陷导致的,扩容只会让问题更快暴露。

误区二:minReplicas 设置为 1

单副本意味着单点故障。任何一次滚动更新或节点故障都会导致服务不可用。生产环境建议 minReplicas >= 2

误区三:maxReplicas 设置过大

maxReplicas 不仅是容量上限,也是成本上限和风险上限。如果配置错误(如内存泄漏),自动扩容可能耗尽整个集群资源。

误区四:忽视缩容的影响

缩容时,正在处理的请求会被中断。如果使用的是无状态服务,负载均衡器会把请求路由到其他实例;如果是有状态服务,需要先处理完正在执行的请求。

延伸思考

弹性伸缩的核心价值不是「省钱」,而是「用合适的资源应对变化的负载」。省钱的副作用是,在流量低谷期释放资源;在流量高峰期获取资源。但实现这个目标,需要对业务有足够的理解——知道什么时候会涨、涨多少、涨多久。

盲目依赖自动弹性伸缩,可能导致成本不可预测、扩缩容震荡、甚至在关键时刻扩容不及时。更健康的做法是:先用容量模型预估基线,用 CronHPA 处理可预测的波动,用 HPA 处理意外情况。三者结合,才能真正做到「能屈能伸」。