1. 扩容前先想清楚:你缺的到底是Pod还是节点
先说一个我踩过好几次的坑。某个大促前夜,业务方跑过来跟我说"集群扛不住了,赶紧扩容",语气急得像机房已经冒烟了。结果我打开监控一看,CPU总使用率不到30%,集群里几百个节点一大半都在闲着,真正的问题是有个服务的Pod全都调度到同一批机器上去了,而那批机器的本地盘快写满了。
这种场景下你扩再多的Pod都没用,因为Pod还是会被调度到那几台“热”节点上。真正该做的是调整调度策略、重新打散Pod,而不是闭着眼睛加机器。
所以在聊K8s扩容之前,我强烈建议你先花十分钟回答三个问题:
- 现在卡住业务的瓶颈是什么?是Pod副本数不够,还是单Pod的资源配额太小,还是节点总数不够?
- 你的扩容是“临时救火”还是“长期规划”?这决定了你是手动扩容快速生效,还是需要配置HPA、Cluster Autoscaler做自动化。
- 你扩出来的资源,业务真的能用上吗?有些时候应用本身的单实例吞吐已经到上限了,加副本不加并发配置,扩了等于白扩。
我个人的经验是,把K8s扩容拆成两个层面来看会清楚很多。第一层是工作负载层,也就是Deployment、StatefulSet里的副本数调整,以及HPA、VPA这类自动扩缩容机制;第二层是基础设施层,也就是集群节点、NodePool的扩缩容,以及控制面组件(etcd、kube-apiserver)的容量规划。这两层经常被混在一起讨论,但实际上它们是两个完全不同的决策路径,扩容方式、生效时间、风险等级都天差地别。
这篇文章我就按这两条线展开,把我在生产环境里做K8s扩容的完整思路、命令、参数和踩坑记录都写出来。
2. 工作负载层扩容:HPA的正确打开方式
2.1 HPA不是配好了就完事,算法你得先懂
HPA(Horizontal Pod Autoscaler)是K8s里最常用的工作负载层扩容手段,但很多人对它的理解停留在“设置一个CPU阈值,超过就扩容”这个层面。如果你真的按这个朴素理解去配置,大概率会在流量高峰看到一些让你困惑的现象。
HPA的核心算法是这样的:它会周期性(默认15秒,可以通过--horizontal-pod-autoscaler-sync-period调整)获取Pod的监控指标,然后计算一个期望副本数,计算公式是:
期望副本数 = ceil(当前副本数 × (当前指标值 / 期望指标值))举个例子,你的Deployment当前有10个副本,HPA配置的CPU目标利用率是50%,现在实际CPU使用率是75%,那么期望副本数就是ceil(10 × (75% / 50%)) = ceil(15) = 15。如果实际使用率是30%,那期望副本数就是ceil(10 × (30% / 50%)) = 6。
这个公式看起来简单,但有几个隐藏细节:
- 指标采集延迟:Metrics Server默认15秒采集一次节点和Pod的指标,HPA再15秒同步一次,所以从流量上涨到Pod真正扩出来,中间至少有30秒以上的延迟。如果你们的业务是陡峭的尖峰流量,比如秒杀、抢购,光靠HPA大概率来不及,需要配合“提前扩容”或者KEDA这类事件驱动的方案。
- 扩容瞬间的指标回落:新Pod启动后,CPU使用率会被平摊到更多Pod上,指标值会快速回落。如果回落幅度很大,HPA可能很快就触发缩容,副本数又降回去。这就形成了“扩容-缩容”的抖动。解决方法是配置
behavior字段,给缩容设置更长的稳定窗口(stabilizationWindowSeconds),甚至直接禁止缩容到某个水位以下。
2.2 实操:一个带缓冲的HPA配置长什么样
我现在的标准做法是,给核心业务配置HPA时不要只配CPU和内存,还要把behavior字段加上,同时给一个minReplicas保底值。下面这个配置是我在模拟项目X(一个电商网关服务)里实际用过的,你可以直接抄走改改:
apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: gateway-hpa namespace: prod spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: gateway minReplicas: 5 maxReplicas: 50 behavior: scaleDown: stabilizationWindowSeconds: 300 policies: - type: Percent value: 20 periodSeconds: 60 scaleUp: stabilizationWindowSeconds: 0 policies: - type: Percent value: 100 periodSeconds: 60 # 允许每60秒最多翻倍副本数 - type: Pods value: 4 periodSeconds: 60 # 同时也允许每60秒最多增加4个Pod,取两个策略中的较大值 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 60 - type: Resource resource: name: memory target: type: Utilization averageUtilization: 70这里有几个参数值得展开说说。
scaleDown的stabilizationWindowSeconds: 300:意思是缩容决策要等5分钟稳定窗口,也就是说即使指标已经降下来了,HPA也会再观察5分钟,确认指标没有反弹才执行缩容。这对于处理有抖动的业务特别管用。policies里我配了每次最多缩20%的Pod数,防止一次性缩掉一大半副本导致服务能力断崖式下跌。
scaleUp侧的policies我配了两个,一个是百分比策略,允许副本数每60秒最多翻倍;一个是固定数量策略,允许每60秒最多增加4个Pod。HPA会取这两个策略计算结果中更大的那个值。为什么要配翻倍策略?因为如果当前只有5个副本,流量突然涨了10倍,按固定4个Pod的扩法要十几个周期才能跟上,而按翻倍策略5→10→20→40,两个周期就追上来了。这是处理突发流量非常实用的小技巧。
2.3 别忘了给单Pod预留突发余量
配置HPA的时候,很多人只盯着Deployment的requests,却忽略了limits和Pod内的具体线程配置。这会导致一个很尴尬的场面:HPA疯狂扩容,副本数从5扩到了50,但每个Pod的能力并没有提升——因为它们虽然拿到了更多的CPU配额,但应用内部的线程池、连接池、协程数还是按照1个Pod的标准配置的。
举一个真实例子。我们有个Java服务,requests配了1核,limits配了2核,HPA按CPU 60%扩容。按说CPU打到60%就会扩容,但实际观察到的现象是:副本数确实涨了,但单Pod的RT(响应时间)没有下降。后来查下去才发现,这个服务用了一个默认线程池,核心线程数只有10,所有请求都在这个线程池里排队。加Pod只能增加排队通道,不能增加处理能力——因为每个Pod进来之后,线程池还是只有10个线程在干活。
所以我的建议是:在做K8s扩容方案时,一定要把“单Pod吞吐能力”作为一个独立变量来评估。你需要知道一个Pod在目标负载下每秒能处理多少请求、数据库连接池占用多少、下游连接的KeepAlive上限是多少。这些参数决定了“扩多少副本才够”。
3. 从HPA到VPA:为什么我不建议新手直接上VPA
3.1 VPA能解决什么问题,解决不了什么问题
VPA(Vertical Pod Autoscaler)是另一种扩容思路——它不增加副本数,而是动态调整单个Pod的CPU和内存requests。听起来很美好,但我在生产环境里很少直接启用VPA的自动模式(Auto模式),原因后面会说。
VPA解决的核心问题是“requests配得不合理”。很多团队配Pod资源的时候是拍脑袋定的,比如统一给1核2G,结果有的服务高峰期内存冲到1.8G,有的服务平时只用0.1核。VPA会持续监控Pod的实际用量,然后给出一个建议值,或者直接帮你把requests改掉。
但VPA有几个非常实际的问题:
- Pod需要重启才能生效。VPA调整的是
requests,而requests只能在Pod创建时生效,所以VPA改完之后会驱逐并重建Pod。这可不是无感的。 - 和HPA同时用容易打架。如果你对一个Deployment同时开HPA和VPA,VPA会去调整
requests,HPA会基于新的requests重新计算副本数,两者经常互相干扰。官方推荐的做法是区分场景:要么用HPA处理负载波动,要么用VPA处理资源规格优化,不要同时对同一个工作负载开两个。 - VPA的recommender需要时间学习。刚部署完VPA,它需要积累一段时间的监控数据才能给出合理的推荐值,通常是24小时以上。指望它“装完就精准”是不现实的。
3.2 我实际用的VPA方式:先当建议工具用
我现在的做法是,给VPA配mode: Off或者mode: Initial,把它当成一个“资源建议工具”来用,而不是自动改配置的工具。流程是这样的:
- 部署VPA并设置
updateMode: "Off",它不会改任何Pod,只是持续输出推荐值。 - 跑一周,观察推荐值和实际使用率的分布。
- 每周挑几个服务,手动把
requests调成VPA推荐值的1.2倍左右,留出20%的余量。 - 同时记录调整前后HPA的行为变化,确认没有出现奇怪抖动。
为什么留20%余量?因为VPA的推荐值本身就是基于历史分位数算出来的,比如percentile: 95的推荐值意味着95%的时间用量不会超过它。但线上流量是有随机性的,留一点余量能避免因为某个异常波动就频繁触发扩容。
3.3 什么时候VPA的Auto模式可能是合适的
也不能一棍子打死VPA Auto模式。有一种场景我确实会启用:StatefulSet配合本地盘或有状态服务。有状态服务扩缩容很麻烦,因为涉及数据分片、数据迁移,不是加个副本就行。这种情况下用VPA去微调每个Pod的资源配额,反而比加副本更安全。
不过即使在这种场景,我也有一个硬性要求:必须配置maxAllowed,并且这个上限要比实际情况低10%左右。VPA如果没有上限约束,一旦某个容器出现内存泄漏,它会无限调大requests,直到把节点资源耗光。这算是VPA使用中的安全底线。
4. 扩容到最后必然要面对的问题:节点池与Cluster Autoscaler
4.1 Pod扩容到顶之后的瓶颈
工作负载层扩容做到一定程度,你一定会遇到另一个瓶颈:Pod想扩,但节点资源不够了。具体表现是Pod一直Pending,看事件会看到一堆0/5 nodes are available: insufficient cpu之类的提示。
到了这一步,扩容的对象就从Pod变成了节点。手动加节点不难,云服务商的控制台点几下按钮就行,但问题是生产环境流量是动态变化的,手动加节点不仅慢,而且容易加多——加多了成本扛不住,加少了又不够。所以Cluster Autoscaler(以下简称CA)就成了节点层扩容的核心工具。
CA的工作原理并不复杂:它持续关注那些因为资源不足而Pending的Pod,一旦发现Pod长时间调度不上去,就会触发节点池扩容,申请新的节点;反过来,当节点的资源利用率持续偏低,且上面的Pod可以被调度到其他节点时,它会排空这台节点并缩容。
4.2 配CA前先把NodePool规划好
CA虽然好用,但它只会帮你加机器,不会帮你选机型。所以在配置CA之前,你必须先把节点池规划好。我见过太多团队直接把所有业务混在一个节点池里开CA,结果流量一起来,CA把一批性能型机器加进来了,但那些业务根本用不上高性能CPU,纯属浪费钱。
我的规划方式是分成三类节点池:
| 节点池类型 | 机型建议 | 主要用途 |
|---|---|---|
| Web/无状态池 | 通用型,CPU和内存均衡 | 网关、Web服务、前端应用 |
| 计算型池 | CPU核数多、主频高 | 数据处理、AI推理、批处理任务 |
| 内存型池 | 内存比例高 | 缓存类、内存数据库类负载 |
通过给不同工作负载打nodeSelector或nodeAffinity,强制它们落到对应的节点池。这样CA扩容时就能按节点池维度申请,不会出现“加了一堆计算型机器给Web服务当CPU闲鱼用”的情况。
4.3 Cluster Autoscaler的关键参数与避坑
CA的部署本身不难,大部分云服务商都有托管版的CA(比如ACK的节点自动伸缩、EKS的Karpenter/CA、TKE的节点池伸缩),自建集群也可以用Helm部署开源版。但参数配置上,有几个坑值得单独说说。
第一个坑:scale-down相关的参数默认值太激进。CA默认的缩容判断条件包括:节点利用率低于50%(scale-down-utilization-threshold默认0.5),并且Pod可以成功调度到其他节点。但在实际业务中,有些节点的利用率是动态波动的,可能只是某一瞬间低了一下就被判为缩容候选,结果节点还没删干净,流量又上来了,Pod又Pending了。我的建议是:
- 提高缩容阈值判断周期,把
scale-down-delay-after-add和scale-down-unneeded-time调大,比如缩容前至少观察10分钟。 - 降低缩容利用率阈值到0.4甚至0.3。宁可多留一点闲置机器,也不要频繁扩缩容。节点创建和销毁是有成本的,尤其是每次新节点加入后,镜像拉取、Pod调度、就绪检查都要时间。
第二个坑:CA和HPA的响应速度叠加可能导致“扩容雪崩”。高峰期时,HPA先扩Pod,Pod Pending,CA再扩节点,节点从申请到Ready通常要几分钟。如果你们的业务流量是持续上涨的,这个链路最后可能会导致:HPA把副本数拉满、CA把扩容额度用尽、但Pod因为镜像还没拉完而长时间ContainerCreating。解决思路是在流量高峰期前手动预扩一批Pod和节点(比如提前2小时把副本数从10调到30),让CA先处于一种“有富余节点”的状态。这个操作我每次大促前都会做,算是验证过多次的成熟做法。
第三个坑:多节点池分配不均。CA默认会选择一个节点池来扩容,但它并不总是选到你想让它选的那个。如果你的多个节点池优先级没有配置,CA可能因为某个池里便宜的机型而把新流量都塞进去,然后那个池子被塞爆,其他池子闲置。解决办法是给节点池配置expander策略,比如priority模式,按你定义的优先级顺序选择扩容的池。
4.4 Karpenter/弹性节点和CA的取舍
如果你用EKS或者自建K8s但集群在云上,还有一个选择是Karpenter。它在某些场景下比CA更聪明——CA本质上是“发现资源不足然后申请整个节点”,Karpenter可以更细粒度地根据Pod的调度需求(比如GPU型号、可用区、实例类型)直接创建最合适的实例。Karpenter的调度效率更高,尤其适合异构负载比较多的集群。
不过Karpenter的学习成本比CA高不少,而且如果你的工作负载相对标准化(都是占CPU内存比较均衡的Web服务),CA配合精心规划的节点池已经完全够用。我的原则是:业务简单就上CA,业务复杂再研究Karpenter。
5. 控制面扩容:最容易被忽视的容量天花板
5.1 控制面也会成为瓶颈,尤其是etcd
很多人做K8s扩容只盯着业务节点和工作负载,忽略了控制面组件本身的容量。事实上,当你的集群规模跨过某个量级(比如节点数超过500、Pod数超过1万),控制面的问题就会逐渐浮出水面。
排在最前面的通常是etcd。etcd是K8s所有状态数据的存储层,它的写入能力是有硬上限的——受到磁盘IOPS、网络延迟、Raft共识协议等多方面限制。一旦etcd写入延迟变高,整个集群的调度、Pod状态上报都会变慢,表现就是:Pod创建速度骤降、kubectl get nodes响应超时、节点上报状态异常。
这里面典型的诱因是事件(Event)过多。很多团队没有配置事件保留策略,一堆Pod反复崩溃重建,会产生海量Event写入etcd。Event对象在etcd里占据的存储和写入带宽比你想象的大得多。我处理过的一个案例:某个命名空间里一个异常Deployment每小时产生几十万个Event,直接把etcd写入IO打到了80%以上,整个集群的控制面都跟着受影响。
5.2 控制面扩容的实操建议
控制面扩容不像工作负载层那样“加个副本就行”,它更依赖前置规划和架构调整。我自己总结出来的几条经验:
etcd层:
- 控制Event数量。要么给核心命名空间设置
Event的TTL(通过kube-apiserver的--event-ttl参数),要么接入更完善的事件采集系统(比如采集到外部存储,而不是全量留在集群里)。 - 定期做etcd defragmentation(碎片整理)。etcd的存储是B+树实现的,删除大量key之后空间不会自动回收,需要执行
etcdctl defrag。建议每个季度做一次,操作前必须在低峰期且做好备份。 - 如果确实到了etcd容量瓶颈,最根本的办法是数据分拆——把一部分工作负载迁移到另一个K8s集群里。这是最不想做但最有效的扩容方式。
kube-apiserver层:
- 给kube-apiserver预留足够的CPU和内存,同时关注它的
LIST请求数量。有些客户端(比如各种Controller、Operator)会频繁LIST全量资源,导致apiserver CPU打满。可以通过--max-requests-inflight限制并发请求数,但要小心不要压到正常调度请求。 - apiserver是无状态的,支持水平扩容。自建集群时多部署几个apiserver副本,然后通过负载均衡分发请求,这个方案对提高读能力很有效,一般不需要像etcd那样做数据分拆。
Node和kubelet层:
- 当节点数超过几百个时,kubelet节点心跳和Pod状态上报会给apiserver和etcd带来可观的压力。可以通过调大
kubelet的--node-status-update-frequency(默认10秒)到15秒甚至30秒来降低上报频率,减轻控制面压力。
5.3 探测控制面容量是否到顶的三个手段
生产环境里,我习惯用这三个手段快速判断控制面是否已经到容量瓶颈:
- 看apiserver的延迟指标:重点关注
apiserver_request_duration_seconds的P99值,如果长时间超过1秒,说明控制面已经有点吃紧。 - 看etcd的写入延迟:
etcd_disk_wal_fsync_duration_seconds如果P99超过几十毫秒,说明磁盘IO可能是瓶颈。 - 看调度延迟:
kube_scheduler_pending_pods如果持续有积压,同时节点资源明明充足,那就不是业务资源不够,而是调度链路处理不过来了。
这三个手段配合前面说的工作负载扩容方案,基本能覆盖我从业务Pod到控制面的完整扩容判断链路。
6. 大促前的扩容演练清单
理论说了这么多,最后分享一个我每逢大促前都会完整执行的扩容检查清单。这个清单是我在多次大促保障中迭代出来的,你可以把它当成你的“最后一公里”保险。
- 检查HPA配置的点位:确认所有核心服务的HPA存在、
minReplicas合理、behavior字段有缩容稳定窗口。不要有大促前才第一次上HPA的服务,至少要提前两周跑过完整链路。 - 检查Cluster Autoscaler的扩容测试:用一个临时Deployment设置超量副本,确认CA能够在15分钟内完成节点扩容。如果超过15分钟,说明节点池配额、镜像拉取链路里有问题,大促前必须解决。
- 检查镜像拉取速度:新节点启动后,从节点注册到Pod Ready的时间是多少?如果在5分钟以上,建议检查镜像仓库是否有节点本地缓存、镜像大小是否异常。
- 检查节点池配额:云服务商的节点配额上限是不是够用?如果你规划扩容到100台机器,但云账户的配额只有50台,大促当天怎么扩都不够。
- 检查etcd和apiserver延迟:大促前一周的监控数据拉出来看一眼P99延迟,如果有明显上升趋势,提前处理。
- 和业务团队对齐“扩容峰值预期”:你们预估的最大QPS是多少?对应副本数是多少?节点数是多少?这些数字必须提前算出来写进文档,而不是大促当天现场拍脑袋。
这套演练我建议大促前至少跑两次,一次提前两周做“故障演练+容量摸底”,一次提前一天做“全链路最终确认”。跑过几轮之后,你会发现大促当天的扩容操作基本就是“按脚本执行,看监控确认”,不会再有那种心跳加速的慌乱感。
我个人这几年做扩容最大的体会是:K8s扩容不是一道“加机器”的计算题,而是一套从工作负载到基础设施、从自动策略到人工预案的完整工程体系。希望这篇基于实战经验的总结,能帮你少走一些我走过的弯路。