K8s Lab 把当前仓库文档整理成一个可阅读的网页站点

Repository Reading Site

本轮操作记录:HPA 自动扩缩容实验

这一轮我要把自动扩缩容最核心的几件事做成真实证据: 1. HPA 的指标到底从哪里来 2. CPU 型 HPA 为什么必须依赖 `requests.cpu` 3. 为什么 HPA 刚建出来时常常会先显示 `<unknown>` 4. 为什么高 CPU 压力会触发扩容 5. 为什么低负载后不会立刻缩容 6. Deployment 滚动更新和 HPA 缩放是怎样

Markdown12-操作记录-HPA-自动扩缩容实验.md2026年4月10日 05:54

本轮操作记录:HPA 自动扩缩容实验

本轮目标

这一轮我要把自动扩缩容最核心的几件事做成真实证据:

  1. HPA 的指标到底从哪里来
  2. CPU 型 HPA 为什么必须依赖 requests.cpu
  3. 为什么 HPA 刚建出来时常常会先显示 <unknown>
  4. 为什么高 CPU 压力会触发扩容
  5. 为什么低负载后不会立刻缩容
  6. Deployment 滚动更新和 HPA 缩放是怎样叠加的

Step 0:先修正 kubectl 上下文,确认自己连的是哪套集群

实际命令

echo "KUBECONFIG=$KUBECONFIG"

kubectl config current-context

KUBECONFIG=~/.kube/config-k8s-lab kubectl config current-context

KUBECONFIG=~/.kube/config-k8s-lab kubectl get nodes -o wide

为什么第一步必须先做这个

这是平台工程里最容易被忽略、但事故率极高的一步。

因为本机 kubectl 默认会去读:

  • ~/.kube/config

如果你没有显式指定 KUBECONFIG,你以为自己在操作实验集群,实际上可能连的是另一套环境。

这次就真实碰到了这个问题:

  • 直接执行 kubectl 时,默认上下文是 kind-mylab
  • 而且它指向一个失效的本地端口 127.0.0.1:52382
  • 所以一开始报的是 The connection to the server 127.0.0.1:52382 was refused

我看到的结果

KUBECONFIG=

说明当前 shell 里没有显式设置 KUBECONFIG

kubectl config current-context
kind-mylab

说明默认指向的是本地 kind 环境,不是这次实验集群。

KUBECONFIG=~/.kube/config-k8s-lab kubectl config current-context
kubernetes-admin@kubernetes

这才是本课实验要操作的真实集群。

节点核对结果:

  • us480851516617a:control-plane,10.10.0.1
  • us590068728056:worker,10.10.0.2
  • cp-3:worker,10.10.0.3
  • hk652699382121:worker,10.10.0.4
  • wk-1:worker,10.10.0.5

原理解释

这一步你要掌握的是 kubeconfig 选择逻辑:

  1. 如果设置了 KUBECONFIG,优先按它指定的文件
  2. 否则默认读 ~/.kube/config
  3. current-context 决定当前连接哪一个 cluster/user 组合

所以以后做生产操作时,第一反应应该不是直接 kubectl apply,而是先确认:

  • 我连的是哪套集群
  • 当前用户是谁
  • 这个 kubeconfig 是不是临时代理环境

Step 1:验证 HPA 的指标链路,而不是假设它“应该可用”

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl api-resources --api-group=metrics.k8s.io

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n kube-system get deploy metrics-server -o yaml | sed -n '1,220p'

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl get --raw '/apis/metrics.k8s.io/v1beta1/namespaces/autoscaling-lab/pods' | sed -n '1,120p'

ssh root@107.148.176.193 \
  'sudo sed -n "1,260p" /etc/kubernetes/manifests/kube-controller-manager.yaml'

为什么要先查这一层

很多人看见 HPA 不工作,就直接改 YAML。

这是不对的。

HPA 是否能工作,至少依赖三层事实:

  1. 集群是否暴露 metrics.k8s.io
  2. metrics-server 是否在正常抓取指标
  3. controller-manager 是否正常运行 HPA controller,且没有奇怪的全局覆盖参数

如果不先确认这一层,你后面的现象解释全部可能是错的。

我看到的结果

kubectl api-resources --api-group=metrics.k8s.io 输出了:

  • NodeMetrics
  • PodMetrics

说明资源指标 API 已注册。

metrics-server Deployment 里看到关键参数:

  • --metric-resolution=15s
  • --kubelet-use-node-status-port
  • --kubelet-insecure-tls

这说明:

  • 指标采样分辨率大约是 15 秒
  • metrics-server 会向 kubelet 拉指标
  • 当前环境为简化实验启用了不校验 kubelet TLS

我还直接读取了资源指标 API,拿到类似下面的原始数据:

{
  "items": [
    {
      "metadata": {
        "name": "cpu-demo-6845f656c6-g4sch"
      },
      "timestamp": "2026-04-10T05:48:24Z",
      "window": "16.405s",
      "containers": [
        {
          "name": "main",
          "usage": {
            "cpu": "8777n",
            "memory": "348Ki"
          }
        }
      ]
    },
    {
      "metadata": {
        "name": "no-request-demo-56c5f5fdf-cm27k"
      },
      "timestamp": "2026-04-10T05:48:21Z",
      "window": "15.987s",
      "containers": [
        {
          "name": "main",
          "usage": {
            "cpu": "999248264n",
            "memory": "388Ki"
          }
        }
      ]
    }
  ]
}

kube-controller-manager 静态 Pod 清单里没有看到显式的:

  • --horizontal-pod-autoscaler-downscale-stabilization
  • --horizontal-pod-autoscaler-sync-period

之类的覆盖参数。

原理解释

这一步要建立三条判断:

  1. kubectl top 不是凭空出现的,它背后依赖的是 metrics.k8s.io
  2. HPA 的资源指标,本质上也是从这条链路读取
  3. 如果 controller-manager 没有覆盖全局参数,那么很多 HPA 行为要按对象级配置或默认逻辑理解

另外,原始数据里的 999248264n 约等于:

  • 0.999 core
  • 也就是约 999m

所以你以后看到:

  • kubectl top pod 里的 1000m
  • 原始 API 里的 999248264n

要知道它们说的是同一件事,只是单位不同。


Step 2:应用本课实验清单,建立两个对照样本

实际命令

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl apply -f manifests/12-hpa/00-namespace-autoscaling-lab.yaml

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl apply -f manifests/12-hpa/10-cpu-demo-burn.yaml

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl apply -f manifests/12-hpa/20-cpu-demo-hpa.yaml

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl apply -f manifests/12-hpa/30-no-request-demo.yaml

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl apply -f manifests/12-hpa/31-no-request-hpa.yaml

这些清单各自是干什么的

00-namespace-autoscaling-lab.yaml

作用:

  • 创建独立命名空间 autoscaling-lab

目的:

  • 让实验对象和其他课题隔离
  • 避免事件流、Pod 列表、HPA 列表互相干扰

10-cpu-demo-burn.yaml

作用:

  • 创建 cpu-demo Deployment
  • 容器通过 MODE=burn 持续空转烧 CPU
  • 配置了 requests.cpu = 100m

目的:

  • 构造一个能让 HPA 正确计算 CPU 利用率并触发扩容的正样本

20-cpu-demo-hpa.yaml

作用:

  • cpu-demo 创建 HPA
  • 目标 CPU 利用率 50%
  • 最小 1 副本,最大 4 副本
  • 缩容稳定窗口显式设置为 30s

30-no-request-demo.yaml

作用:

  • 创建 no-request-demo
  • 同样持续烧 CPU
  • 但故意不配置 requests

目的:

  • 构造一个“指标有值但 HPA 算不出来”的反样本

31-no-request-hpa.yaml

作用:

  • no-request-demo 配同样的 CPU utilization 型 HPA

目的:

  • 让你看到同样是高 CPU,为什么有的工作负载能扩,有的不能扩

原理解释

这一组实验不是单纯为了“证明 HPA 可以用”。

它真正要建立的是对照思维:

  • 只看成功样本,你容易误以为“高 CPU 就一定扩容”
  • 只看失败样本,你又可能误以为“这套系统坏了”

把正反两个样本并排放,你才能看清真正决定 HPA 行为的变量是什么。


Step 3:观察 HPA 刚创建时的“预热期”

实际命令

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n autoscaling-lab get hpa,pod,deploy -o wide

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n autoscaling-lab describe hpa cpu-demo

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n autoscaling-lab describe hpa no-request-demo

为什么要专门看这一段

因为新手最容易把“暂时没有指标”误判为“系统故障”。

而真实系统里,HPA 刚建出来时先经历一个短暂的无数据阶段是非常正常的。

我看到的结果

在早期事件里,两个 HPA 都先报过:

  • FailedGetResourceMetric
  • unable to get metrics for resource cpu: no metrics returned from resource metrics API

原理解释

这表示:

  • Pod 刚启动
  • 指标采样窗口还没形成
  • metrics-server 还没把这批 Pod 的数据暴露给 API

因此你以后看到 HPA 是 <unknown> 时,排障顺序应该是:

  1. 先看 Pod 是否刚启动
  2. 再看 kubectl top
  3. 再看 metrics.k8s.io 原始 API
  4. 最后才去怀疑 HPA 自己

Step 4:观察 cpu-demo 的成功扩容

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl top pod -n autoscaling-lab

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n autoscaling-lab get hpa,pod,deploy -o wide

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n autoscaling-lab describe hpa cpu-demo

我看到的关键结果

预热完成后,kubectl top pod -n autoscaling-lab 看到:

  • cpu-demo-* 大约 999m
  • no-request-demo-* 也大约 1000m

当时 kubectl get hpa,pod,deploy -o wide 看到:

horizontalpodautoscaler.autoscaling/cpu-demo          Deployment/cpu-demo          cpu: 999%/50%          1         4         4
horizontalpodautoscaler.autoscaling/no-request-demo   Deployment/no-request-demo   cpu: <unknown>/50%     1         3         1

describe hpa cpu-demo 里可以看到:

  • ScalingActive=True
  • ValidMetricFound
  • ScalingLimited=True
  • TooManyReplicas

以及事件:

  • SuccessfulRescale3
  • SuccessfulRescale4

命令解释

kubectl top pod

用途:

  • 看实时近似资源使用量

注意:

  • 这不是 cgroup 文件的原始值
  • 它来自 metrics-server
  • 适合做“此刻大致用了多少”的快速判断

kubectl get hpa,pod,deploy -o wide

用途:

  • 一次性把 HPA、Pod、Deployment 放在同一个视图里看

适合回答:

  • HPA 算出来多少
  • Deployment 期望多少
  • 实际跑了多少 Pod

kubectl describe hpa

用途:

  • 看 HPA 计算状态、条件和事件

这是 HPA 排障最重要的命令之一。

原理解释

为什么 cpu-demo 会暴冲到 999%

因为它的 request 只有 100m,而实际 CPU 使用接近 1000m

1000m / 100m = 1000%

HPA 目标只有 50%,所以从算法角度它会强烈倾向扩容。

但为什么最后只停在 4

因为:

  • maxReplicas: 4

已经把上限封住了。

这就是为什么 describe hpa 里能看到:

  • ScalingLimited=True

这句话的真实含义不是“出问题了”,而是:

  • HPA 想再扩
  • 但被你配置的安全边界挡住了

Step 5:观察 no-request-demo 的失败场景

实际命令

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n autoscaling-lab describe hpa no-request-demo

我看到的关键结果

describe hpa no-request-demo 里明确出现:

  • ScalingActive=False
  • FailedGetResourceMetric
  • failed to get cpu utilization: missing request for cpu in container main

同时它的 Metrics 一栏显示:

  • resource cpu on pods (as a percentage of request): <unknown> / 50%

命令解释

describe hpa 的价值在这里体现得非常明显。

如果你只看 kubectl get hpa,你只是知道它是 <unknown>

describe hpa 会进一步告诉你:

  • 是拿不到指标
  • 还是拿到了指标但算不出 utilization
  • 还是算出来了但被上下限限制

原理解释

这个失败场景非常重要。

因为它证明了一件事:

CPU 数值本身存在,不等于 CPU utilization 一定能被计算。

no-request-demo 明明也烧到了一个核附近,但 HPA 仍然不扩。

根因不是没有 CPU 压力,而是:

  • 没有 requests.cpu
  • 所以没有利用率分母

这就是为什么平台治理里,资源规范不是“锦上添花”,而是自动化能力的基础前提。


Step 6:把 cpu-demo 从高负载切到空闲,观察缩容

实际命令

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n autoscaling-lab set env deployment/cpu-demo MODE=idle

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n autoscaling-lab rollout status deployment/cpu-demo --timeout=240s

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n autoscaling-lab get events --sort-by=.metadata.creationTimestamp

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n autoscaling-lab get rs -o wide

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n autoscaling-lab rollout history deployment/cpu-demo

KUBECONFIG=~/.kube/config-k8s-lab kubectl top pod -n autoscaling-lab

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n autoscaling-lab describe hpa cpu-demo

为什么我用 set env,而不是直接重新 apply 一个 Deployment

因为本课我想保留 HPA 对副本数的控制权,不想让 YAML 的固定副本值和 HPA 抢控制。

kubectl set env 的作用是:

  • 修改 Deployment Pod 模板里的环境变量
  • 触发滚动更新
  • 不直接把 replicas 改成某个固定值

它很适合做这种“只换业务行为,不碰副本控制权”的实验。

我看到的关键结果

  1. rollout status 显示新版本成功发布。
  2. kubectl top pod -n autoscaling-lab 最终看到:
    • cpu-demo-* 大约 1m
  3. describe hpa cpu-demo 最终看到:
    • resource cpu on pods (as a percentage of request): 1% (1m) / 50%
    • Deployment pods: 1 current / 1 desired
  4. describe hpa cpu-demo 事件里出现:
    • New size: 1; reason: All metrics below target

事件时间线里最关键的证据

kubectl get events --sort-by=.metadata.creationTimestamp 显示了完整过程:

  • 早期高负载时,cpu-demo 先扩到 3
  • 随后扩到 4
  • 执行 set env 后,Deployment 创建了新的 ReplicaSet:cpu-demo-6845f656c6
  • 旧 ReplicaSet 是:cpu-demo-7588dc5965
  • 新旧副本短时间并存,Deployment 在做滚动更新
  • 随后 HPA 给出:
    • SuccessfulRescale ... New size: 1; reason: All metrics below target

从事件年龄可以推断:

  • 新空闲版 ReplicaSet 开始接管,大约在 3m54s
  • HPA 给出最终缩回 1,大约在 3m21s

两者相差大约 30 多秒。

原理解释

这一步你要看懂三件事。

第一件:低负载出现,不等于立刻缩容

即使 CPU 很快从 999m 掉到 1m,HPA 也不会立即收缩。

因为对象上配置了:

  • scaleDown.stabilizationWindowSeconds: 30

这等于告诉 HPA:

先观察一下,确认这次下降不是短暂抖动,再决定是否真的缩。

第二件:Deployment 滚动更新和 HPA 是两个控制器

当你改环境变量时:

  • Deployment 负责创建新 ReplicaSet
  • HPA 负责根据新指标决定最终副本数

所以时间线上会出现:

  • 发布带来的副本波动
  • HPA 带来的目标副本变化

两套控制循环交错进行。

第三件:ReplicaSet 的 hash 变化,说明 Pod 模板确实变了

你可以看到旧的 ReplicaSet 是:

  • cpu-demo-7588dc5965

新的 ReplicaSet 是:

  • cpu-demo-6845f656c6

这说明 MODE 变化导致 Pod 模板摘要改变,Deployment 正常生成了新版本。

这在排障里很有价值,因为它能帮助你确认:

  • 到底是不是你期待的模板变更生效了

Step 7:整理本轮最重要的学习结论

结论 1:HPA 的前提不是“有 YAML”,而是“有指标链路”

至少要同时成立:

  • metrics.k8s.io 可用
  • metrics-server 正常
  • HPA controller 正常工作

结论 2:CPU utilization 型 HPA 离不开 requests.cpu

没有 request,并不是“精度差一点”,而是:

  • 根本算不出利用率

结论 3:<unknown> 不一定是故障,常常只是预热

尤其是新建 Pod、新 HPA、刚滚动发布完的场景。

结论 4:缩容通常比扩容保守

这是为了抑制抖动,而不是系统反应迟钝。

结论 5:HPA 扩容不是容量扩容

HPA 只增加 Pod 数量。

如果节点不够、调度失败、下游瓶颈没变,业务仍然可能没有改善。


本轮命令背后的方法论

这一轮你不应该只记住命令本身,更应该记住它们各自回答的问题。

kubectl config current-context

回答:

  • 我到底在操作哪套环境

kubectl api-resources --api-group=metrics.k8s.io

回答:

  • 集群里有没有注册资源指标 API

kubectl get --raw '/apis/metrics.k8s.io/...'

回答:

  • HPA 依赖的原始数据到底有没有、长什么样

kubectl top pod

回答:

  • 此刻近似资源使用是多少

kubectl describe hpa

回答:

  • HPA 为什么扩、为什么不扩、为什么被限制

kubectl get events --sort-by=.metadata.creationTimestamp

回答:

  • 这整件事按时间顺序到底发生了什么

kubectl get rs

回答:

  • 是否出现了新旧版本并存
  • 当前到底是哪一个 ReplicaSet 在生效

这一课给你的专家能力提升点

如果你把这轮实验真正吃透,你已经不再只是“会写一个 HPA YAML”。

你开始具备的是:

  • 验证指标链路的能力
  • 解释 HPA 算法输入与边界的能力
  • 区分预热、算法失败、上限限制、滚动发布干扰的能力
  • 把 HPA 放回整个控制面体系里排障的能力

这才是以后能做公司级平台架构和复杂问题排查的基础。