1. HPA到底是什么先搞懂它存在的意义在Kubernetes集群里跑服务早晚会遇到一个逃不掉的问题流量来了怎么扛流量走了怎么省。传统的做法是提前把副本数定死比如固定跑5个Pod。高峰期CPU打满、接口超时低峰期一堆空转的Pod白吃内存。手动的玩法就是人盯着监控看到水位上去了kubectl scale半夜还得爬起来。这显然是没法接受的于是就有了HPAHorizo​​ntal Pod Autoscaler水平Pod自动扩缩容。HPA做的就一件事持续盯着Pod的运行时指标CPU、内存或者自定义的业务指标计算当前实际负载跟设定目标值的偏差自动调整Deployment或者StatefulSet的副本数让运行的Pod数量始终贴合流量需求。负载上去就扩容负载下来就缩容整个过程不需要人介入Kubernetes的控制循环自动完成。适合谁来用基本上所有在Kubernetes里跑Web服务、API服务、定时任务之外的有状态工作负载的团队都应该了解它。尤其是在流量波动明显的生产环境HPA不是你选不选的问题是你迟早要面对的东西。这篇文章我会把HPA从工作原理、核心参数、组装过程到排错经验完整过一遍内容以实战为导向官方文档的章节我也对照着拆开揉碎讲尽量让你看完就能照着落地而不是停留在“知道有这个东西”的层面。2. 控制循环与核心参数把官方文档的细节逐个拆开2.1 HPA控制循环的工作机制在Kubernetes的kube-controller-manager里有一个专门负责HPA的控制器horizontal-pod-autoscaler controller它按照一个固定周期循环工作默认每15秒执行一次这个周期由kube-controller-manager的启动参数--horizontal-pod-autoscaler-sync-period控制。每次调度循环里控制器会先通过API Server拿到所有HPA对象再根据HPA里配置的目标资源scaleTargetRef找到对应的Deployment或StatefulSet然后从metrics API里拉取运行Pod的实时指标。拉到的指标不是简简单单拿来用。对于CPU、内存这类资源指标HPA控制器会先计算出所有Pod的平均利用率再跟HPA里设置的target值做除法得到一个比例关系。这个比例就是最终调整副本数的依据desiredReplicas ceil[currentReplicas * (currentMetricValue / desiredMetricValue)]举个例子当前跑了10个PodCPU平均利用率是80%HPA目标值是50%那么比例就是80/501.6期望副本数就是10*1.616个。控制器会把Deployment的副本数改成16个下一轮循环再用新的Pod数量重新计算直到比例收敛到1附近也就是实际负载和目标基本持平。这个工作有两点需要关注一是HPA控制器只负责决定副本数并更新scale子资源真正创建/销毁Pod的是Deployment控制器。这意味着你改HPA不会引起Pod的中断重启它会走滚动更新的逻辑平滑调整。二是HPA的扩容和缩容不是对称的。Kubernetes的策略是扩容要快、缩容要慢。扩容最快可以在数秒内响应默认允许每3秒扩一次--horizontal-pod-autoscaler-downscale-stabilization-window默认5分钟专门控制缩容冷却因为要快速抗住流量缩容则要观察一段时间确认负载真的降下来才动手避免Pod刚杀完流量又回来导致抖动。理解了控制循环再看HPA的YAML配置时你会对每个字段的含义有非常清晰的感知。2.2 核心字段逐一拆解官方文档里HPA的配置看起来很短但每个细节都有实际意义。我拿一个最常见的配置来说明apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: api-server-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: api-server minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 60 behavior: scaleDown: stabilizationWindowSeconds: 300 policies: - type: Percent value: 50 periodSeconds: 60逐个说scaleTargetRef定义HPA作用于哪个工作负载。注意kind不一定是Deployment只要是支持scale子资源的都行比如StatefulSet或者自定义控制器管理的可扩展资源。实际操作中绝大多数是Deployment。minReplicas和maxReplicas副本数的上下边界。minReplicas设置得太高会浪费资源但太低又可能在负载突增时扩容来不及。生产环境一般至少保持2个保证单节点故障时服务还在。maxReplicas就是HPA允许扩到的上限这也是给你的预算设一个天花板。metricsHPA支持指标类型的重点。官方文档里明确列了几类ResourceCPU和内存是最常用的资源指标。PodsPod级别的自定义指标指标值是所有Pod的平均值。Object自定义指标直接作用于某个具体对象比如根据Ingress的QPS、根据网关的请求量。External外部系统的指标比如云厂商的负载均衡器指标或者消息队列的积压数量。每个metric都可以配置type、name、targettarget又有三种形式——Utilization利用率、AverageValue平均值、Value具体数值。CPU指标用得最多的是Utilization即所有Pod的平均使用率自定义业务指标则几乎都用AverageValue例如averageValue: 30表示每个Pod平均每秒处理30个请求。behaviorKubernetes 1.18之后引入的高级功能专门用来微调扩缩容策略。刚才提到的缩容稳定窗口就是这个字段控制的stabilizationWindowSeconds缩容稳定时间。默认是300秒意味着即使指标持续偏低HPA也要等这段时间后才开始缩容。如果是CPU密集型的服务负载波动频繁可以把这值调高到600秒避免频繁缩容抖动。scaleDown.policies每次缩容允许减少的最大Pod数量。Percent: 50表示一次最多缩一半periodSeconds: 60表示这个限制按60秒周期计算。scaleUp也有自己的policies和selectPolicy选择策略默认扩容的策略是优先选择能扩更多的策略执行确保快速扩容。从业者容易忽略的细节是behavior里字段名的大小写和含义。stabilizationWindowSeconds只作用于scaleDown和scaleUp各自窗口互不干扰。而scaleUp里如果设置了selectPolicy: Disabled直接禁止扩容——这在某些场景下比如大促前的维护窗口想人工控制有奇效。2.3 目标值的选择逻辑在配置HPA时最容易翻车的不是YAML语法而是目标值选得不对。这里有个系统性的思考方法。先看CPU目标值。生产环境常见的配置是50%~70%但不是每个服务都适合同一个值。如果你的服务对响应时间敏感例如用户下单和支付链路建议把CPU目标值调低比如40%让HPA在CPU用到40%时就开始扩容这样Pod能保有更多余量接口延迟会更稳定。如果服务本身是批处理类型的对延迟不敏感则可以设高一些比如70%~80%让Pod跑得更饱和再扩容减少空闲成本。再看自定义指标。如果根据QPS扩容目标值怎么定有两个角度一是单Pod的容量。你需要用压测工具测出单个Pod在延迟达标前提下的最大QPS这个值就是你的目标值。二是冗余策略。目标值设置得比实测容量低30%~50%作为安全余量。我见过不少团队把QPS目标值拍脑袋设为100结果线上Pod在QPS到80时CPU已经爆了HPA还没来得及扩容接口先超时。正确的做法是先压测再定值且目标值要留冗余。这个细节值得反复强调HPA的目标值设得越低服务越稳但成本也越高你需要找到那个平衡点。3. 指标来源与metrics-server部署细节3.1 指标链路:资源指标从哪里来HPA要工作得先有指标数据可读。在Kubernetes里资源指标CPU、内存是由metrics-server提供的它通过kubelet的Summary API采集每个节点上Pod的CPU和内存使用量然后聚合出Pod维度的指标暴露给API Server的metrics API。整个链路是HPA控制器调用metrics.k8s.ioAPI。metrics-server接收请求从自身缓存里取指标。metrics-server定时从各节点的kubelet拉取指标数据默认每15秒一次--metric-resolution参数从1.20版本改名之前叫--metric-resolution对应参数为--metric-resolution还是--kubelet-use-node-status-port不同阶段有区别当前以官方发布为准。有个容易混淆的概念HPA本身不用Prometheus它只读metrics.k8s.io接口。如果你要用自定义业务指标需要额外的 adapter比如prometheus-adapter它负责把Prometheus里的数据翻译成metrics.k8s.io或者custom.metrics.k8s.io接口格式HPA才能读。3.2 metrics-server部署与验证很多Kubernetes集群默认没装metrics-serverkubectl top nodes一执行就是报错。装这个组件本身很简单但在国内网络环境下镜像拉取是第一个坑。我的做法是先把官方YAML拉下来改好镜像地址再applykubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml如果Pod一直CrashLoopBackOff看日志通常会有一个典型错误x509证书校验失败。原因是metrics-server默认走TLS访问kubelet但有些自建集群的kubelet证书没有包括API Server的地址。解决办法是给metrics-server加启动参数spec: containers: - args: - --kubelet-insecure-tls加完之后再验证kubectl top nodes kubectl top pods -n prod如果这两条命令能正常输出指标说明链路通了。3.3 内存指标的特殊性官方文档的HPA示例大量用CPU做演示但很多业务场景其实更关心内存。比如一个Java应用堆内存涨上去就降不下来必须靠重启或者扩容来缓解。配置内存HPA本身不复杂metrics: - type: Resource resource: name: memory target: type: Utilization averageUtilization: 70但内存指标有一个天然缺陷内存使用率不会像CPU那样快速下降。GC之后内存可能依然处于高位HPA计算出的期望副本数可能会长时间保持在高位缩容迟迟不发生。这本身不是HPA的bug而是内存这种资源指标的特性决定了它不适合作为唯一的扩容依据。我的经验内存HPA更合理的做法是跟CPU混合使用或者用AverageValue直接指定每个Pod的内存上限而不是以百分比表示利用率。例如你的Pod内存Limit是2Gi不希望任何Pod超过1.5Gi就可以设置averageValue: 1500Mi让控制器在平均内存超过1.5Gi时扩容。这样语义更清晰不易受到Base Pod内存规格变化的影响。4. 实操全过程从创建HPA到验证扩容缩容4.1 准备一个可压测的Deployment先造一个消耗CPU的测试服务用什么镜像没关系关键是它能跑起来而且能被压测工具打到。我常用一个简单的Web服务镜像配合压测工具来模拟负载。创建DeploymentapiVersion: apps/v1 kind: Deployment metadata: name: cpu-demo spec: replicas: 1 selector: matchLabels: app: cpu-demo template: metadata: labels: app: cpu-demo spec: containers: - name: cpu-demo image: registry.example.com/library/cpu-demo:latest resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 256Mi注意resources.requests这个字段HPA计算CPU利用率时用的是requests的CPU而不是节点的物理CPU。如果你的Pod写了requests: 100m实际使用50m那利用率就是50%。假如你没写requestsHPA会取不到值——这个是需要牢牢记住的坑。4.2 创建HPA并观察行为Deployment创建完成并确认Pod运行后创建HPAapiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: cpu-demo-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: cpu-demo minReplicas: 1 maxReplicas: 5 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 50用下面命令查看HPA状态kubectl get hpa cpu-demo-hpa -w刚创建时可能会看到unknown的TARGETS这是因为指标还没收集到。等几秒后就会显示NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE cpu-demo-hpa Deployment/cpu-demo 42%/50% 1 5 1 2m接下来用压测工具给服务加压。给这个服务加CPU负载让利用率超过50%。过几十秒再观察REPLICAS会从1变成2、3甚至更多直到稳定在满足目标值的副本数。压测结束后负载掉下来计数器开始缩容。但注意即便是最简单的配置缩容也需要等缩容稳定窗口默认5分钟所以你不会看到立即缩回1个Pod。这正好印证了前面说的扩容快缩容慢的设计。4.3 resource metrics与custom metrics混合配置实际生产里单一指标往往不够用。比如一个Web服务既要防CPU打满又要确保每个Pod的QPS不能太高就需要同时配置两类指标。spec: metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 50 - type: Pods pods: metric: name: http_requests_per_second target: type: AverageValue averageValue: 100HPA在处理多指标时不是取平均值而是分别计算每个指标需要的副本数然后取其中的最大值。这样设计很符合实际需要任一指标告急都说明容量不够取最大值能够保证服务始终跑在最安全的水位。这里要提醒一点这种多指标合并的配置里如果某个指标长时间采集不到HPA会把该指标视为失效并跳过它但不会删掉整个HPA。避免这种情况的正确做法是确保自定义指标的采集链路是稳定的。4.4 通过behavior策略控制缩容速度有这样一个真实场景某核心服务CPU负载波动很大频繁出现“扩容到10个Pod30秒后又缩回4个再过1分钟又扩到12个”的现象。原因就是HPA对负载变化的响应太灵敏每次同步周期都重新计算而Deployment还在滚动创建Pod时CPU指标又因为新Pod加入迅速下降触发了缩容判断。针对这个问题我给行为策略加了限制behavior: scaleDown: stabilizationWindowSeconds: 600 policies: - type: Percent value: 20 periodSeconds: 300600秒的稳定窗口意味着即使现在指标偏低HPA也必须等待10分钟确认低负载是持续的才会真正执行缩容计算。同时每次缩容最多缩当前副本数的20%相当于限流。对于吃流量波动大的服务这个配置实测下来稳定很多。扩容侧一般不建议加太多限制默认的策略已经够谨慎。如果你的服务在冷启动时非常慢比如需要加载大模型或者初始化连接池很耗时可以额外调整scaleUp的policies把value调大让单次扩容的Pod更多减少冷启动期间的流量压力。5. 常见问题与排查技巧实录5.1 HPA没有生效现象kubectl get hpa能看到TARGETS显示unknown或者HPA状态一直是Unable to fetch metricsPod数量无变化。排查路径从底向上检查metrics-server是否正常kubectl top nodes如果这条命令本身报错说明问题在指标链路最底层。检查HPA引用的Deployment是否存在且命名正确HPA不会报“Deployment不存在”这种明显的错它只是安静地什么都不做。检查Deployment的PodTemplate里是否设置了resources.requests.cpu。前面提过CPU利用率计算的分母就是这个值没写等于没法算。如果用的是自定义指标检查adapter的log看有没有从Prometheus拉到数据。常见错误是PromQL写错或者指标名对不上。5.2 扩容后指标持续打满有时扩容完指标还是红的比如期望副本数已经扩到maxReplicas上限但CPU依然90%。这种情况有几种可能的根因目标值本身设置偏高Pod在目标水位下已经无法满足延迟要求。单Pod容量被错误评估压测时忽略了流量倾斜、内存分配等副作用。应用扩容后并没有把负载分摊开比如使用了一些有状态组件新Pod启动后并没有真正承接流量。排查建议先看kubectl describe hpa里的扩容事件它会记录每次扩容时的指标快照和计算过程能清晰看到哪个指标把副本数推到了上限。5.3 缩容抖动缩容抖动是最常见的生产事故源。表象是副本数像心电图一样上下起伏底层Pod频繁被删又频繁重建既影响稳定性又浪费资源。我自己的排错顺序是先看behavior.stabilizationWindowSeconds是否设了合理的值。默认300秒只是通用值如果你的业务负载天然波动大应该调大。再看指标本身是否有毛刺。比如某个Pod的CPU瞬时冲到90%但平均下来只有30%HPA捕捉到了峰值。这种情况下除了稳定窗口还要考虑指标的平滑比如调整metrics-server的采集频率或者在adapter层面对PromQL做区间聚合。最后确认应用是否在启动后有预热期。一个Pod刚起来时CPU通常很高如果HPA在这个阶段采样会误判负载偏高。5.4 自定义指标adapter的坑用prometheus-adapter接入自定义指标时坑主要集中在两个地方。一是命名空间。prometheus-adapter配置的seriesQuery里如果没正确关联namespace标签HPA会找不到Pod在哪个namespace下的指标值。二是单位。adapter默认会把Prometheus里的值原样返回而HPA的averageValue目标值单位要与指标一致。如果指标本身是毫秒而目标值写的秒算出来的副本数会偏离好几倍。我的习惯是先把指标在adapter里调试通直接用kubectl get --raw检查kubectl get --raw/apis/custom.metrics.k8s.io/v1beta1/namespaces/prod/pods/*/http_requests_per_second | jq .这条命令会直接返回HPA读取到的指标值格式跟HPA控制器的请求一样。如果这里拿到的数据是对的HPA那边就基本不会有问题。5.5 极限场景突发流量的扩容延迟所有HPA都不能解决“瞬间流量暴涨”的问题因为从指标采集到扩容生效至少需要几十秒。具体拆解下指标采集周期metrics-server默认15秒 HPA同步周期默认15秒 Deployment滚动创建新Pod的时间取决于镜像大小和启动速度。如果业务对突发流量的容忍度极低比如秒杀场景HPA单独扛不住。业界常用的组合拳是在HPA之外配合Cluster AutoscalerCA做节点层扩容这样上层的Pod扩容能有足够的资源空间。对于已知的突发场景提前在HPA基础上做定时扩容或者提前扩容把副本数预先抬高。用KEDA这类事件驱动扩缩容组件支持根据消息队列积压量、事件延迟等提前扩容比HPA多一层响应手段。但是这些都属于架构层面的补充。纯粹从HPA使用的角度讲把同步周期调短并不是最优解调短意味着指标噪声更容易触发误判稳定性反而下降。合理的做法是接受HPA的响应延迟用预测性扩容来弥补。5.6 HPA YAML语法与apiVersion版本适配使用HPA时如果照着网上的旧教程抄了一个apiVersionapply的时候可能会被API Server拒绝。原因很简单autoscaling/v1不支持CPU Utilization类型的指标只支持autoscaling/v2beta2或autoscaling/v2的多指标语法。判断依据是Kubernetes版本。生产环境如果低于1.23建议用autoscaling/v2beta2高于1.23可以直接用autoscaling/v2。当前主流的云厂商托管集群都已经支持autoscaling/v2但如果拿着一个autoscaling/v2beta1的老YAML去applyKubernetes 1.26以上版本会直接报错。我一般统一写成autoscaling/v2并且确保所有字段都用新版字段名比如averageUtilization而非旧版的targetCPUUtilizationPercentage。旧写法的兼容性在新版本中越来越差不值得为了一时的省事给自己埋雷。6. 一个完整的生产级HPA示例给一套可以直接拿来用的DeploymentHPA组合这套配置包含了前面提到的核心要点CPU与自定义业务指标混用、缩容稳定窗口、显式的requests配置。apiVersion: apps/v1 kind: Deployment metadata: name: user-service namespace: prod spec: replicas: 3 selector: matchLabels: app: user-service template: metadata: labels: app: user-service spec: containers: - name: user-service image: registry.example.com/prod/user-service:v2.1.0 ports: - containerPort: 8080 resources: requests: cpu: 200m memory: 512Mi limits: cpu: 2 memory: 2Gi readinessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 10 periodSeconds: 5apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: user-service-hpa namespace: prod spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: user-service minReplicas: 3 maxReplicas: 20 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 50 - type: Pods pods: metric: name: http_requests_per_second target: type: AverageValue averageValue: 200 behavior: scaleDown: stabilizationWindowSeconds: 600 policies: - type: Percent value: 30 periodSeconds: 300 scaleUp: stabilizationWindowSeconds: 0 policies: - type: Percent value: 100 periodSeconds: 60配置的几个细节解释一下readinessProbe必须加。如果新Pod还没就绪就接收流量HPA扩容等于白做指标会被拉低反而触发缩容判断。limits的内存设成requests的4倍是给业务内存波动留出安全兜底。CPU的limits如果不必要可以不写让Pod可以突峰使用多核CPU但在高密度部署时建议还是加上防止单个Pod把节点CPU吃穿。stabilizationWindowSeconds: 600会让缩容变得非常保守。对于长时间低峰期的业务这个值可以降到300具体要看你对Pod重建的容忍度。scaleUp.stabilizationWindowSeconds: 0表示扩容不做稳定窗口只要指标达标立即扩容这是保证快速响应的关键。7. 经验向的总结把前面所有内容收拢我直接说几个带有明显倾向性的结论都是实操里验证过的东西。HPA不是万能的但它一定是Kubernetes生产环境里的必备组件。难点不在于怎么把YAML写出来而在于把目标值调准、把behavior策略调稳、把指标链路打理顺畅。这三件事做好了HPA能给你省下大量运维精力做不好HPA会变成半夜电话的制造机。从指标选择的角度CPU优先没问题但别只盯CPU。根据业务类型选主指标再搭配一两个辅助指标做兜底比单指标准确得多。凡是追求极致稳定性的团队我一定会建议他们尽快上自定义指标哪怕初期只是把网关的QPS接入进来也比纯看CPU好用得多。关于调参我个人的一条准则是扩容策略大胆缩容策略保守。扩快一点不会造成严重事故最多是成本短暂上升缩太快则会造成Pod反复重建对长连接和有状态服务的影响非常大。所以稳定窗口一定优先保障。最后想给所有参考这篇文章的人一个提醒HPA的官方文档只是工具书生产环境的很多问题它都不会告诉你。多压测、多看指标快照、多记录扩容事件把HPA的每个决策都变成可解释、可回溯的“日志”你才能真正驾驭它。