Skip to main content

9. 弹性伸缩

手动扩缩的尽头​

你给服务配了 3 个 Pod。流量一上来,CPU 集体飙到 90%,响应时间炸了。你 kubectl scale deploy/nginx --replicas=10,然后盯了一小时监控,深夜再 kubectl scale deploy/nginx --replicas=3。

连干一个星期你就疯了。K8S 的弹性伸缩就是替你干这件事——你定义"什么时候要扩",K8S 不再被动等你。

三种伸缩维度,从内到外:

Pod 的资源调整 Pod
──→ 节点的数量调整
└── VPA ──┘ ┌──── Cluster Autoscaler ────┐
Pod 的数量调整 新增 Node
┌────── HPA ──────┐

HPA:根据指标增减 Pod 数量​

HPA(Horizontal Pod Autoscaler)是最常用的。你告诉它:监控某个指标,超出阈值就加 Pod。

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: nginx-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: nginx
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # 平均 CPU 超过 70% 就扩
behavior:
scaleDown:
stabilizationWindowSeconds: 300 # 降下来 5 分钟后才缩,防抖动

HPA 每 15 秒(默认)拉一次指标。源码核心逻辑在 k8s.io/kubernetes/pkg/controller/podautoscaler/horizontal.go:

func (a *HorizontalController) reconcileAutoscaler(hpa *autoscalingv2.HorizontalPodAutoscaler) error {
// 1. 获取当前 Pod 的指标
currentReplicas := scale.Status.Replicas
metrics := a.computeReplicasForMetrics(hpa, scale)

// 2. 计算期望的 Pod 数
// desiredReplicas = ceil(currentReplicas * (currentMetricValue / desiredMetricValue))
desiredReplicas := metrics.desiredReplicas

// 3. 如果不一样,调 Deployment 的 replicas
if desiredReplicas != currentReplicas {
scale.Spec.Replicas = desiredReplicas
a.scaleUpdater.Update(hpa.Namespace, scale)
}
}

计算公式直观得吓人:期望副本数 = 当前副本数 ×(当前指标值 / 目标指标值)。

当前 3 个 Pod,CPU 平均 90%,目标 50%:3 × (90/50) = 5.4 → 扩到 6。

思考题:HPA 只能根据"当前指标"扩缩——那如果我想在每天早上 9 点提前扩容,高峰期过了再缩回来,HPA 能做到吗?做不到的话,K8S 生态里有什么工具专门干这事?

HPA 需要 metrics 的接口:Metrics Server​

HPA 自己不采集指标。它需要 metrics API 告诉它 Pod 用了多少 CPU。Metrics Server 就是那个提供 metrics API 的组件——在 K8S 里这是一个必须额外安装的东西。

# Metrics Server 提供的 API 路径
# GET /apis/metrics.k8s.io/v1beta1/namespaces/default/pods/nginx-xxx

Metrics Server 从每个 Node 的 kubelet Summary API 拉数据(cAdvisor 内嵌在 kubelet 里),汇总后暴露给 API Server。

K3S 跟 K8S 在这里有差异:K3S 默认不带 Metrics Server,需要你 helm install 或 kubectl apply。K8S 也一样——minikube 和 kubeadm 装的集群都不会自动带。这是个常见的"为什么 HPA 不工作"的原因。

思考题:Metrics Server 只提供 CPU 和内存——但 HPA 的 metrics 字段里可以写 type: Object、type: External、type: Pods 这些非 Resource 的指标。这些指标谁提供?你装 Prometheus 能当 HPA 的数据源吗?怎么把 Prometheus 的指标"喂"给 HPA?

VPA:直接调容器资源​

HPA 加 Pod,VPA(Vertical Pod Autoscaler)加大单个 Pod 的 CPU 和内存 request。

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: nginx-vpa
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: nginx
updatePolicy:
updateMode: Auto # Auto: 直接改 → Pod 重启(资源变了得重建)
# Initial: 只在 Pod 创建时设初始值
# Off: 只给建议

VPA 不是 K8S 核心功能,是 kubernetes/autoscaler 仓库里单独维护的。

它要你接受一个事实:VPA 在 Auto 模式下会重建 Pod(资源改变了需要重建才能生效)。如果你的应用不支持随意重启,就别开 Auto,开 Off 模式看建议就行。

CA:节点不够了,加机器​

HPA 把 Pod 从 3 个扩到 10 个,但你的 Node 只有 3 台,每台最多放 3 个 Pod。Pod 卡在 Pending 状态。

Cluster Autoscaler 做的事情很简单:发现有 Pod 因为资源不足一直 Pending,就调云厂商 API 新增一个 Node。

// cluster-autoscaler 核心逻辑简化
func (a *StaticAutoscaler) RunOnce(currentTime time.Time) {
unscheduledPods := a.getUnscheduledPods()

if len(unscheduledPods) > 0 {
// 计算需要多少新节点
nodeCount := a.estimator.Estimate(unscheduledPods)
// 调云 API 创建节点
a.cloudProvider.AddNodes(nodeCount)
}

// 另一边:有空闲节点超过 10 分钟就回收
a.removeUnneededNodes()
}

CA 对云平台有强依赖——它在 AWS 上通过 ASG 扩缩,在 GCP 上通过 MIG。裸金属上基本不行,你没法从物理机柜里"新加一台"。这就是 K3S 在边缘场景很少跟 CA 搭配的原因。

三个的协作关系​

一个正常的自动伸缩流程:

流量上涨 → CPU 飙到 80%
│
▼
HPA: "Pod 从 3 扩到 8"
│ Pod #6 没法调度——三个 Node 都满了
│ Pod #6 Pending
▼
Cluster Autoscaler: "有个 Pod 等了 30 秒还 Pending,加一台 Node"
│ 新 Node Ready
▼
Scheduler: "把 Pod #6 分到新的 Node 上"
│ CPU 降回 50%
▼
HPA: "稳定了,不做操作"

小结​

  • HPA 根据 CPU/内存/自定义指标调 Pod 数量,公式简单粗暴
  • HPA 依赖 Metrics Server 提供指标 API
  • VPA 改单个 Pod 的资源 request,Auto 模式会重建 Pod
  • Cluster Autoscaler 发现 Pod 调不动就加 Node,依赖云 API
  • 三层伸缩的顺序:HPA → Pod 扩 → 满了 → CA 加 Node

解答​

1. HPA 能不能定时?​

HPA 本身不支持定时触发。 HPA 的控制循环只看"当前指标"——CPU 多少、内存多少、队列深度多少。它没有任何时间概念,不知道现在是早上 9 点还是凌晨 3 点。

但你可以用 KEDA 的 Cron Scaler 来做定时伸缩,或者用原生的 CronJob + kubectl scale 拼一个简单的临时方案。

KEDA 的 Cron Scaler:

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: nginx-cron-scaler
spec:
scaleTargetRef:
name: nginx
triggers:
- type: cron
metadata:
timezone: Asia/Shanghai
start: "0 9 * * 1-5" # 周一到周五,早上 9 点
end: "0 18 * * 1-5" # 下午 6 点
desiredReplicas: "10" # 这段时间维持 10 个副本

KEDA 在 start 时间把 replicas 拉到 10,在 end 时间恢复到正常 HPA 管控。余下时间 HPA 根据指标正常扩缩。

为什么不能用 CronJob + kubectl scale 凑合? 也能,但不优雅:

apiVersion: batch/v1
kind: CronJob
metadata:
name: scale-up
spec:
schedule: "0 9 * * 1-5"
jobTemplate:
spec:
template:
spec:
containers:
- name: kubectl
image: bitnami/kubectl
command: ["kubectl", "scale", "deployment/nginx", "--replicas=10"]
restartPolicy: OnFailure

问题:手动设了 10 之后 HPA 还在跑——如果 HPA 的目标还是 3 个副本,它会立刻把 10 缩回 3。你得配合一段停机窗口——用 kubectl patch hpa --patch '{"spec":{"minReplicas":10}}' 临时改 HPA 下限,过了高峰期再改回来。这也正是 KEDA Cron Scaler 替你做的。

一句话:HPA 管指标,KEDA 管时间——两者可以叠在一起用。HPA 维持基线伸缩,KEDA Cron trigger 覆盖定时任务。

2. HPA 的指标只能来自 Metrics Server 吗?​

不只是。谁实现了 metrics API,HPA 就能从谁那里拿数据。

K8S 的 HPA 通过三个 API 路径获取指标:

指标来源API 路径提供者
type: Resource/apis/metrics.k8s.io/v1beta1Metrics Server(CPU/内存)
type: Pods/apis/custom.metrics.k8s.io/v1beta1Prometheus Adapter 等
type: Object/apis/custom.metrics.k8s.io/v1beta1Prometheus Adapter 等
type: External/apis/external.metrics.k8s.io/v1beta1外部系统(如云监控)

约定很简单——谁在这几个 API 路径上注册并响应请求,HPA 就用谁的数据。

Prometheus 当 HPA 数据源的流程:

Prometheus(采集和存指标)
│
▼
Prometheus Adapter(把 PromQL 翻译成 K8S metrics API)
│ 暴露 /apis/custom.metrics.k8s.io/
▼
HPA Controller → GET /apis/custom.metrics.k8s.io/.../nginx_requests_per_second
│
▼
Prometheus Adapter → PromQL: rate(nginx_http_requests_total[2m]) → 返回值

装好 Prometheus Adapter 后,HPA 里可以写任意 Prometheus 指标:

metrics:
- type: Pods
pods:
metric:
name: nginx_requests_per_second # 自定义指标名
target:
type: AverageValue
averageValue: "1000"
- type: Object
object:
metric:
name: kafka_consumer_lag # Kafka 积压
target:
type: Value
value: "5000"

"Pods" 类型是"每个 Pod 的指标取平均","Object" 类型是"集群中某个全局对象的指标"(比如 Kafka 的消费延迟是一个 cluster-level 指标)。

Prometheus Adapter 里配一条 rule,把 PromQL 映射成 K8S metrics 路径:

# prometheus-adapter 的 ConfigMap
rules:
- seriesQuery: 'nginx_http_requests_total'
resources:
overrides:
namespace: {resource: "namespace"}
pod: {resource: "pod"}
name:
matches: "^(.*)_total"
as: "${1}_per_second"
metricsQuery: 'sum(rate(<<.Series>>{<<.LabelMatchers>>}[2m])) by (<<.GroupBy>>)'

除了 Prometheus Adapter,其他选择:Datadog Cluster Agent(自带 custom metrics API 实现)、AWS CloudWatch Metrics Adapter、Stackdriver Adapter。只要实现了 metrics API,HPA 不问来源。

下一章,资源不够用但你不想加机器——超卖。