问一个SRE看似简单的问题:checkout-api 该多大,现在该跑几个副本?

打开网易新闻 查看精彩图片

Helm values 里写着 replicas: 3、cpu: 1000m、memory: 2Gi。但活着的集群讲的是另一个故事:HPA 按 CPU 负载把应用扩到 11 个副本;VPA 坚持认为它超配了,建议改成 cpu: 350m、memory: 3.1Gi;KEDA 因为上游 Kafka 积压飙升,要求 24 个副本;Karpenter 已经开了两台新节点,而且严格按声明的 requests 而不是实际用量来算物理容量。

五个来源,五个互相矛盾的答案。每个控制器单独看都合乎逻辑,但没有一个看得到全貌。这种孤立观测与实际负载需求之间的落差,是现代 Kubernetes 运维里的一处核心架构失效。

Unix 哲学在弹性系统里失灵了

Kubernetes 及其组件建立在 Unix 哲学之上:造小而模块化的工具,每个只把一件事做好。放到容器编排里,这套原则听起来很直观:

  • HPA 基于运行时资源利用率处理水平弹性;
  • VPA 基于历史消耗处理容器垂直规格;
  • KEDA 基于外部队列积压,把事件源桥接到副本数;
  • Karpenter 和 Cluster Autoscaler 在 Pod 处于 Pending 时管理节点容量;
  • GitOps(Argo CD / Flux)从版本控制强制推行集群基线状态。

在标准 Unix 环境里,grep 和 awk 能干净地组合,是因为它们顺序处理不可变的文本流:一个工具干完,把输出交给下一个。

自动扩缩控制器不是顺序执行的。它们并发运行,作用于同一个目标工作负载,改动同一批 API 字段:.spec.replicas、.resources.requests,以及底层的节点池。每个都在独立的 reconcile 循环里跑,默认自己对工作负载规格有独占控制权,却看不见旁边的工具。

确定工作负载容量是一道单一、耦合的数学题。把它拆给彼此解耦的控制器,得到的不是模块化效率,而是抖动。

四种撞车方式

当独立的控制循环去操作相互依赖的变量,反馈回路就会失稳,演变成四类常见故障。

第一种,分母塌缩:HPA 对 VPA。HPA 用 Pod CPU 利用率占声明 requests 的百分比来算目标副本数,声明的 requests 就是分母。VPA 同时运行时会把 CPU requests 调低以消除浪费,分母随之缩小。一点轻微流量上涨就能把百分比利用率推过 HPA 的阈值,触发激进的水平扩容。副本越多,流量被摊得越薄,单 Pod 用量下降,VPA 又把 requests 缩得更小。工作负载进入死亡螺旋:VPA 不停缩小 Pod 体积,HPA 造出一支管不过来的微型容器舰队。

第二种,多写者竞态:原生 HPA 对 KEDA。团队把 KEDA 的队列扩缩和标准 CPU 驱动的 HPA 混用时,两个控制器瞄准同一个 Deployment 的 /scale 子资源。事件队列飙升,KEDA 扩容以消化积压;但这些 worker Pod 是 I/O 密集型、CPU 用量低,独立的 HPA 一看利用率这么低,判定应用超配,又把它缩回去。API server 记录下一次次交替的扩缩事件,实际处理吞吐却卡住了。

第三种,GitOps 漂移乒乓:声明式 IaC 对动态运行时。GitOps 引擎把 Git 仓库当作唯一事实来源。流量高峰时 HPA 扩容,Argo CD 就把应用标记为 OutOfSync。如果开了自动同步,GitOps 会把线上 Deployment 重置回基线,在需求峰值时抽走容量。工程师常把 GitOps 配成忽略副本差异,但这破坏了声明式基础设施:Git 不再反映生产现实,后续发布还有悄悄恢复过时基线的风险。

第四种,节点层的乘数效应。Karpenter 这类节点扩缩器严格按声明的容器 requests 而非实际消耗做放置决策。一个容器请求 1000m CPU 却只消耗 150m,调度器照样把整核预留出去。Karpenter 为幽灵需求开物理实例,把上游的规格低效成倍放大成云账单。在生产集群里,尽管节点开了不少,平均 CPU 利用率徘徊在 8% 左右。

真正的扩缩器该回答什么

每个传统扩缩器回答的都是一个窄问题:“我这一项设置该是多少?”但真实的运维需求更宽:什么样的副本数、资源 requests、limits 和扩缩阈值组合,能在最低成本下满足延迟 SLO,以及这套配置该在什么时候变?

水平与垂直规格是一道优化问题里相互关联的变量。真正的扩缩器不能依赖反应式的单指标启发法,它必须作为一个统一控制平面运转,具备五项核心能力:

  • 统一信号摄入:同时评估 CPU/内存指标、队列深度、事务速率、延迟 SLO 达标情况、节点价格和 IaC 清单;
  • 联合决策:成对计算出一个动作向量,同时定义水平上限和垂直资源 requests,而不是把副本交给一个工具、把 Pod 规格交给另一个;
  • 持续的 GitOps 同步:向应用仓库提交自动生成、附带理由的 pull request,而不是逼 GitOps 引擎忽略线上集群状态,让版本控制与现实需求保持一致;
  • 预测式、主动的容量规划:反应式扩缩天生有缺陷,因为初始化新计算资源需要时间。传统反应式控制器等负载突破某个任意阈值才动,需求骤增时就会延迟飙升。真正的扩缩器要引入预测算法,分析历史消耗、识别日或周的周期性、预测工作负载需求,在流量到来之前预热应用容量、调整资源规格,把扩缩从惊慌的反射变成平滑的排程操作;
  • 可解释的审计:为每一次调整给出清晰的运维理由,让 SRE 对成本节省、预测需求和延迟边界之间的取舍有完整透明度。

从反射到自主

云原生生态习惯把“自动化”和“自主”混为一谈。今天的扩缩器是自动化的:它们像生物反射,测一个孤立的遥测信号,触发一个预定义响应。事件发生,HPA 触发、KEDA 扩缩、Karpenter 开机器。但反射没有中枢神经系统。五条独立反射同时作用在同一条肢体上,系统就抖了。

自主需要的是一个闭环:观测整个运行环境,理解性能与成本之间的取舍,并对结果承担端到端的责任。

Unix 哲学仍是执行层的正确蓝图:HPA、VPA、KEDA、Karpenter 应该继续执行它们各自被设计来处理的那几个 API 变更。Kubernetes 真正缺的,是决定这些执行器该做什么的中央智能层。

下次平台团队再争论该上 HPA 还是 VPA,不妨退一步问个更大的问题:谁来决定这个工作负载的规格,他看得到全貌吗?

在现代集群里,没人看得到。要把 Kubernetes 的规格调对,得先造出一个终于看得到全貌的系统。