Kubernetes 基础体系 · 第 70/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。

Kubernetes 成本治理:资源归属、闲置、Spot、存储网络和优化边界

Kubernetes 成本治理不是简单地把节点数量乘以云厂商单价。集群账单通常由计算、持久化存储、网络流量、负载均衡、日志与监控、控制面,以及云厂商附加服务共同构成;Kubernetes 只直接管理其中一部分资源。

成本治理的核心问题可以形式化为:

总成本=计算成本+存储成本+网络成本+平台共享成本+外部服务成本\text{总成本} = \text{计算成本} +\text{存储成本} +\text{网络成本} +\text{平台共享成本} +\text{外部服务成本}

而组织真正需要回答的是:

  1. 这笔成本属于哪个团队、应用、环境或租户?
  2. 付费资源中有多少没有产生有效业务价值?
  3. 哪些工作负载可以使用更便宜但更不稳定的资源?
  4. 优化后是否损害了可用性、性能、数据持久性或故障恢复能力?
  5. 成本降低是否只是把费用从一个项目转移到了另一个项目?

因此,Kubernetes 成本治理应当同时处理资源归属、容量配置、闲置识别、弹性资源、存储、网络和优化边界


一、先建立成本模型:Kubernetes 对账单做了什么、没有做什么

1. 计算成本的基本单位是节点,而不是 Pod

云厂商通常按照虚拟机、裸金属节点或托管节点池计费。Kubernetes 调度 Pod 时关心的是节点的可分配资源,而不是云账单中的单个 Pod。

设一个节点的小时成本为 CnC_n,节点池中有 NN 个节点,则计算成本近似为:

Ccompute=n=1NCn×运行小时C_{\text{compute}} = \sum_{n=1}^{N} C_n \times \text{运行小时}

Pod 只是占用了节点上的 CPU、内存、临时存储等资源。除非使用了能够根据 Pod 或命名空间做成本分摊的外部系统,否则云厂商账单不会天然知道某个 Pod 应该承担多少钱。

因此必须区分两个概念:

  • 账单成本:云平台实际收费的节点、磁盘、流量和服务费用。
  • 分摊成本:根据资源使用、资源申请量或业务规则,把账单映射到团队和应用的内部成本。

例如,两个命名空间分别运行在同一个节点上:

节点实际成本:1.00 美元/小时
team-a 的 Pod request:2 vCPU,4 GiB
team-b 的 Pod request:1 vCPU,4 GiB
节点 allocatable:4 vCPU,8 GiB

如果按照 request 分摊,则:

team-a:约 66.7%
team-b:约 33.3%

但如果 team-a 的 CPU 实际利用率远低于 team-b,按照 request 分摊仍然可能不符合某些财务或容量分析目的。成本分摊规则必须先明确:是按预留容量实际使用量峰值占用,还是按固定的共享服务规则。

2. 成本归属至少需要三层信息

一套可审计的归属模型通常包含:

组织归属:团队、部门、成本中心
技术归属:集群、节点池、命名空间、工作负载
业务归属:服务、产品、环境、租户

Kubernetes 原生对象中的常见归属字段包括:

  • metadata.labels:用于查询、聚合和选择;
  • metadata.annotations:适合保存描述性信息,不适合承担高频筛选和严格枚举;
  • namespace:提供逻辑隔离,但不是天然的财务成本中心;
  • 节点标签:表示区域、架构、节点池、容量类型等;
  • 节点污点:控制哪些 Pod 可以被调度到节点上。

一个工作负载可以使用如下标签:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: checkout
  namespace: retail-prod
  labels:
    app.kubernetes.io/name: checkout
    app.kubernetes.io/instance: checkout-prod
    app.kubernetes.io/component: api
    app.kubernetes.io/part-of: retail
    cost-center: cc-042
    owner-team: payments
    environment: production
spec:
  replicas: 3
  selector:
    matchLabels:
      app.kubernetes.io/name: checkout
      app.kubernetes.io/instance: checkout-prod
  template:
    metadata:
      labels:
        app.kubernetes.io/name: checkout
        app.kubernetes.io/instance: checkout-prod
        cost-center: cc-042
        owner-team: payments
        environment: production
    spec:
      containers:
      - name: app
        image: example/checkout:1.2.3
        resources:
          requests:
            cpu: "500m"
            memory: "512Mi"
          limits:
            cpu: "1"
            memory: "1Gi"

这里有一个容易遗漏的细节:成本工具经常从 Pod 标签 读取归属,而不是只读取 Deployment 标签。Deployment 的标签和 Pod 模板标签必须同时维护,否则控制器对象看似有归属,实际运行的 Pod 可能无法被正确分摊。

3. 共享成本不能强行归给某个业务

以下资源往往属于共享成本:

  • 控制面或托管控制平面;
  • CoreDNS、网络插件、Ingress Controller;
  • 日志、监控、追踪采集器;
  • 节点操作系统和系统 DaemonSet;
  • NAT Gateway、跨可用区流量、公共负载均衡器;
  • 集群级别的安全、备份和镜像仓库服务。

可以采用两种方式:

  1. 单独保留共享成本池:报表中列为平台成本;
  2. 按规则分摊:例如按各团队 request 占比、Pod 数、节点使用量或收入比例分摊。

第二种方式必须记录公式。例如某共享成本池为 3000 元,团队 request 成本占比为 40%,则分摊金额为:

3000×40%=1200 元3000 \times 40\% = 1200 \text{ 元}

如果没有固定规则,不同月份可能使用不同口径,报表将失去可比性。


二、Request、利用率与闲置:便宜的资源不等于未使用的资源

1. Kubernetes 调度主要依据 Request

容器资源配置中:

  • requests 表示调度器为该容器预留的资源需求;
  • limits 表示容器可使用的上限;
  • 没有设置 request 时,可能继承 LimitRange 的默认值,也可能成为没有明确请求的容器;
  • ResourceQuota 可以限制命名空间总量,但不会自动让资源配置合理。

一个 Pod 的 CPU request 是其所有容器 CPU request 的总和;内存同理。一个节点能调度多少 Pod,取决于节点的 allocatable,而不是云主机标称的全部 CPU 和内存。

若节点有:

capacity:8 vCPU,32 GiB
系统和 kubelet 保留后 allocatable:7.5 vCPU,29 GiB
已调度 Pod requests:6 vCPU,20 GiB

那么调度视角下的剩余容量为:

CPU:1.5 vCPU
内存:9 GiB

即使操作系统监控显示当前只用了 3 vCPU,调度器也不会因此自动把更多需要 2 vCPU request 的 Pod 放入该节点。因为调度以 request 为主要约束,而不是实时利用率。

2. 闲置至少有三种含义

“闲置”不能只看 CPU 百分比,至少应区分:

资源申请闲置

Pod request 远高于实际使用量:

Irequest=1实际使用量requestI_{\text{request}} = 1 - \frac{\text{实际使用量}}{\text{request}}

例如:

内存 request:4 GiB
P95 实际使用:1 GiB

则按 P95 计算的内存申请闲置率为:

114=75%1 - \frac{1}{4} = 75\%

这意味着调度容量被大量预留,但不代表可以直接把 request 改成 1 GiB。还必须检查 OOM、缓存突增、JVM 堆外内存、内核页缓存和故障切换后的流量。

节点装箱闲置

节点已购买,但由于 Pod request 碎片化或约束无法装满。例如总剩余 CPU 足够,但每个节点都没有单独容纳新 Pod 所需 request 的连续容量,这属于碎片,不是简单的总量不足。

业务闲置

应用本身没有有效流量或没有产生业务价值,例如:

  • 已停止使用的预生产环境;
  • 过期的临时环境;
  • 已下线但 Deployment 仍保留;
  • 低流量服务长期运行固定副本数;
  • 批处理任务完成后留下的节点池或 PVC。

三者的处理方式不同。调低 request 解决的是第一类,重排节点或调整调度约束解决第二类,缩容或删除资源解决第三类。

3. 一个完整的 Request 优化算例

假设一个服务运行 10 个副本:

每个 Pod CPU request:1000m
每个 Pod 内存 request:2 GiB
实际 CPU P95:350m
实际内存 P99:1.1 GiB

原始申请量:

CPU:10 × 1000m = 10 vCPU
内存:10 × 2 GiB = 20 GiB

如果经过压测确认故障场景下单 Pod 的 CPU 不超过 600m、内存不超过 1.5 GiB,可以将配置调整为:

requests:
  cpu: "600m"
  memory: "1536Mi"
limits:
  cpu: "1"
  memory: "2Gi"

新的申请量:

CPU:10 × 600m = 6 vCPU
内存:10 × 1.5 GiB = 15 GiB

仅从调度容量看,释放了:

CPU:4 vCPU
内存:5 GiB

但这个结论成立需要满足几个条件:

  1. P95 或 P99 数据覆盖完整业务周期,而不是只覆盖低峰;
  2. 发生单实例故障后,剩余副本能够承接流量;
  3. CPU limit 触发节流不会破坏延迟目标;
  4. 内存峰值不会导致 OOMKilled;
  5. 滚动更新期间的额外副本仍可调度;
  6. HPA 的目标指标没有因为 request 改变而失真。

HPA 使用 CPU 利用率时,通常按“实际 CPU 使用量 / CPU request”计算相对利用率。request 从 1000m 调到 600m 后,同样使用 300m CPU,利用率会从 30% 变成 50%。因此调整 request 可能改变 HPA 扩缩容行为,而不是只改变成本。

4. limits 不是“越大越安全”

CPU limit 可能导致容器在节点有空闲时仍受到 CFS 限流;内存 limit 超出后可能触发 OOM。对于延迟敏感服务,CPU limit 的设置需要通过压测确认,而不是机械地设为 request 的固定倍数。

相反,完全不设置内存 limit 也有风险:单个失控容器可能消耗节点大量内存,最终触发节点级内存压力和驱逐。QoS Class、内存回收和驱逐行为会影响故障表现,因此成本优化不能脱离稳定性验证。

可以使用以下命令检查实际资源配置:

kubectl get pods -A \
  -o custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,CPU_REQ:.spec.containers[*].resources.requests.cpu,MEM_REQ:.spec.containers[*].resources.requests.memory'

这只能看到配置的 request,不能代表实际利用率。实际利用率通常来自 Metrics Server、Prometheus 或云厂商监控;kubectl top 依赖 Metrics API,且适合观察当前或近期数据,不适合作为长期成本核算的唯一数据源:

kubectl top pod -A
kubectl top node

生产环境应保存至少数周的 request、实际使用量、重启、OOM、驱逐、延迟和业务流量数据,再进行调整。


三、资源归属的实现:从标签到可审计的分摊

1. Namespace 是边界,不是完整的成本模型

Namespace 可以提供:

  • 资源命名隔离;
  • ResourceQuota 配额;
  • LimitRange 默认资源配置;
  • RBAC 授权范围;
  • NetworkPolicy 等策略作用域。

但 Namespace 不天然对应团队或账单。一个团队可以使用多个 Namespace,一个 Namespace 也可能包含多个服务。更重要的是,Namespace 中的共享组件和跨命名空间资源需要额外处理。

可以为生产命名空间设置资源总额:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: retail-prod-quota
  namespace: retail-prod
spec:
  hard:
    requests.cpu: "20"
    requests.memory: 40Gi
    limits.cpu: "40"
    limits.memory: 80Gi
    pods: "200"

这会拒绝超出配额的新对象,但不会自动缩小现有 Pod,也不会判断申请量是否合理。配额是治理边界,不是优化算法。

LimitRange 可以防止团队提交完全没有资源配置的容器:

apiVersion: v1
kind: LimitRange
metadata:
  name: default-container-resources
  namespace: retail-prod
spec:
  limits:
  - type: Container
    defaultRequest:
      cpu: "100m"
      memory: "128Mi"
    default:
      cpu: "500m"
      memory: "512Mi"
    max:
      cpu: "4"
      memory: "8Gi"

默认值只是兜底。若默认值远高于大多数应用实际需求,会把“缺少配置”转化为系统性的容量浪费,因此应定期统计默认值的命中率和后续实际使用量。

2. 标签治理需要防止伪归属

只允许任意用户填写 cost-center,会产生以下问题:

  • 团队名称拼写不一致;
  • 业务删除后标签仍保留;
  • 一个团队把成本标记到另一个成本中心;
  • Deployment 有标签,Pod 模板没有;
  • Job、CronJob、StatefulSet 使用了不同字段。

常见的控制手段是:

  • 使用组织规定的标签键和值;
  • 通过 Admission Webhook 或策略引擎拒绝缺少归属信息的对象;
  • 将归属从 Namespace、Git 仓库或服务目录自动注入;
  • 对标签值建立版本化字典;
  • 把无法归属的资源单独列为“未分配成本”。

Webhook、Gatekeeper 或 Kyverno 等工具不是 Kubernetes 核心 API,具体能力和兼容性取决于实现。治理系统本身也会产生运行成本和误拦截风险,必须提供审计模式、豁免机制和恢复路径。


四、节点自动扩缩容与成本:缩容的前提是可驱逐、可重调度

节点自动扩缩容通常包含以下数据流:

flowchart LR
    A[工作负载提交或负载变化] --> B[调度器]
    B -->|无可用节点| C[Pending Pod]
    C --> D[集群扩容组件]
    D --> E[节点组或云 API]
    E --> F[新节点加入集群]
    F --> B
    G[低利用率或空节点] --> D
    D --> H[驱逐可迁移 Pod]
    H --> I[节点排空]
    I --> J[删除节点或缩小节点组]

关键点是:扩容通常由 Pending Pod 的 requests 和调度约束 触发,而不是由节点 CPU 利用率直接触发。缩容则需要证明节点上的 Pod 能被驱逐并在其他节点重新调度。

一个节点看起来只有 10% CPU 使用率,但如果上面的 Pod 具有以下任意约束,仍可能无法缩容:

  • PodDisruptionBudget 不允许继续驱逐;
  • local 卷或节点本地数据不能迁移;
  • Pod 使用了不可重建的本地状态;
  • 反亲和性导致其他节点放不下;
  • 拓扑约束限制了目标区域;
  • DaemonSet 或系统 Pod 处理逻辑不支持;
  • Pod 的 request 过大,其他节点没有足够 allocatable;
  • 使用了本地临时存储,驱逐会造成数据丢失。

因此,“删除一个节点能省一台机器费用”只有在下面条件成立时才成立:

 一个可行调度方案,使所有必须保留的 Pod 在剩余节点上满足约束\exists \text{ 一个可行调度方案,使所有必须保留的 Pod 在剩余节点上满足约束}

这个条件不仅包含 CPU 和内存,还包含架构、区域、污点、卷拓扑、端口、GPU、拓扑分布和安全策略。

节点自动扩缩容组件并非 Kubernetes 核心统一实现。Cluster Autoscaler、云厂商托管扩缩容器、Karpenter 等工具在节点发现、实例选择、替换策略和缩容判定上存在差异。使用时应以对应项目和云厂商文档为准,不应假定某个组件的参数能迁移到另一个组件。


五、Spot:通过接受中断换取更低的计算价格

1. Spot 的经济模型

Spot、抢占式实例或可中断实例的共同特征是:云厂商可以在容量不足、价格策略变化或维护事件发生时回收实例。名称和回收通知机制因云厂商不同,但本质都是以较低价格换取较弱的供应稳定性。

如果普通实例每小时成本为 CoC_o,Spot 实例成本为 CsC_s,中断概率为 pp,一次中断造成的恢复和业务损失期望成本为 CiC_i,则粗略期望成本为:

E(Cspot)=Cs+p×CiE(C_{\text{spot}}) = C_s + p \times C_i

只有当:

Cs+p×Ci<CoC_s + p \times C_i < C_o

时,Spot 才具有经济意义。

这里的 CiC_i 不仅是重新创建节点的费用,还包括:

  • 请求失败和重试;
  • 缓存重新预热;
  • 批处理延迟;
  • 数据恢复;
  • 人工介入;
  • SLO 违约;
  • 多可用区流量或跨区域恢复成本。

因此,Spot 并不是“便宜节点池”,而是“把容量可靠性风险交给应用和调度系统处理”。

2. Spot 工作负载的必要条件

适合放到 Spot 上的工作负载通常具备:

  • 可水平扩展;
  • Pod 可以被重建;
  • 状态保存在外部可靠存储;
  • 能容忍短暂容量减少;
  • 具有合理的超时、重试和幂等设计;
  • 可以接受批处理延迟或吞吐波动。

不适合直接使用 Spot 的典型工作负载包括:

  • 单副本且不可中断的核心服务;
  • 将节点本地磁盘作为唯一数据副本的系统;
  • 需要稳定低延迟的关键路径;
  • 无法处理重复执行的任务;
  • 依赖固定节点身份或本地缓存内容的应用。

3. 用污点和容忍度隔离 Spot 节点

可以给 Spot 节点添加污点,例如:

capacity-type=spot:NoSchedule

只有声明容忍度的 Pod 才能调度到这些节点:

spec:
  tolerations:
  - key: capacity-type
    operator: Equal
    value: spot
    effect: NoSchedule

仅有 toleration 不代表 Pod 一定会去 Spot 节点,它只是允许调度。还需要节点标签和节点亲和性表达偏好或要求:

spec:
  affinity:
    nodeAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 80
        preference:
          matchExpressions:
          - key: capacity-type
            operator: In
            values:
            - spot
      - weight: 20
        preference:
          matchExpressions:
          - key: capacity-type
            operator: In
            values:
            - on-demand

这里使用 preferredDuringSchedulingIgnoredDuringExecution,表示优先选择 Spot,但没有 Spot 时仍可选择按需节点。若使用 requiredDuringSchedulingIgnoredDuringExecution,Spot 容量不足时 Pod 会保持 Pending,成本可能反而增加,因为业务无法运行。

更稳妥的部署通常是混合容量:

按需节点:承载最低可用副本和系统组件
Spot 节点:承载可中断的额外副本、批任务和弹性吞吐

还必须设置多样化的实例类型、区域或节点组选择条件,避免所有副本集中在一个可能同时失效的容量池中。具体字段和标签名称由云厂商或节点管理工具定义,不能假定 capacity-type=spot 在所有环境中自动存在。

4. 中断处理不是 PDB 的替代品

云厂商通常提供实例回收通知,应用可以通过节点生命周期组件、守护进程或云厂商集成执行:

  1. 接收中断通知;
  2. 对节点执行 cordon,禁止新 Pod 调度;
  3. 执行 drain,驱逐可迁移 Pod;
  4. 等待替代 Pod 在其他节点启动;
  5. 最终删除节点。

PodDisruptionBudget 只约束某些自愿中断场景下的并发驱逐数量,例如节点排空;它不能阻止云厂商强制回收 Spot 节点,也不能保证 Pod 在中断前完成迁移。

例如:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: checkout-pdb
  namespace: retail-prod
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app.kubernetes.io/name: checkout

对于 3 个副本,minAvailable: 2 通常允许一次自愿中断一个副本。若所有 3 个副本都在同一个 Spot 节点上,节点被强制回收时仍可能同时丢失全部副本。PDB 必须结合副本数、拓扑分布、节点池混合和应用启动时间评估。


六、存储成本:PVC 不是唯一费用,容量也不是唯一指标

1. PersistentVolumeClaim 的生命周期和费用生命周期可能不同

PVC 是用户对存储的请求,PV 是实际绑定的持久卷,StorageClass 描述动态供应策略。一个常见流程是:

提交 PVC
  ↓
StorageClass 动态供应
  ↓
创建 PV 和云磁盘
  ↓
Pod 挂载 PVC
  ↓
删除 Pod
  ↓
PVC/PV 是否删除,取决于回收策略和工作流

查看 PVC、PV 和 StorageClass:

kubectl get pvc -A
kubectl get pv
kubectl get storageclass
kubectl describe pvc -n retail-prod checkout-data

需要特别检查:

  • StorageClass 的 reclaimPolicyDelete 还是 Retain
  • 是否启用了 allowVolumeExpansion
  • 卷是否绑定到特定拓扑区域;
  • 卷类型的 IOPS、吞吐和快照费用;
  • 删除 Namespace 后 PVC 是否按预期清理;
  • 备份和快照是否有独立保留周期。

Delete 通常意味着删除 PV 时删除底层卷,但具体行为由 provisioner 实现;Retain 则通常保留底层卷,便于恢复,却容易形成孤儿资源。生产系统不能只根据 Kubernetes 对象是否存在判断云磁盘是否还在,应同时核对云平台资源。

2. 存储优化要同时看容量、性能和恢复要求

一个 1 TiB 卷即使只使用 100 GiB,也可能按 1 TiB 计费。可以考虑缩小卷,但缩容通常不如扩容简单,很多云盘和文件系统不支持原地缩容。安全流程一般是:

  1. 确认应用支持停机或在线迁移;
  2. 做可验证的备份;
  3. 创建目标容量的新卷;
  4. 复制数据并校验;
  5. 修改挂载关系;
  6. 观察读写错误和延迟;
  7. 在保留窗口后删除旧卷。

不要直接修改 PVC 期望通过把容量写小来实现缩容;Kubernetes 通常只支持扩容,具体能力还取决于 StorageClass、CSI 驱动和底层存储。

存储成本还包括:

  • 快照数量和保留周期;
  • 备份跨区域复制;
  • 高 IOPS 或高吞吐配置;
  • 多副本、纠删码或复制因子;
  • 日志和临时文件;
  • 对象存储请求与生命周期费用。

删除快照前应确认没有备份链依赖。某些云厂商的增量快照删除语义并不等同于简单删除一个独立文件,实际费用和恢复能力应以厂商实现为准。

3. StatefulSet 缩容不会自动删除所有数据

StatefulSet 的 Pod 和 PVC 生命周期需要分开理解。缩小副本数通常会减少 Pod,但历史 PVC 是否删除取决于 Kubernetes 版本、StatefulSet PVC 保留策略和具体配置;不能把缩容当成数据清理操作。

删除 StatefulSet、PVC 或 PV 前,应先回答:

数据是否仍需恢复?
是否有经过演练的备份?
是否还有其他 Pod 使用该卷?
底层云资源是否会被删除?
删除后是否能阻止自动重建?

成本治理不能以不可逆的数据删除作为默认手段。


七、网络成本:流量路径比流量总量更重要

Kubernetes 网络成本经常来自 Kubernetes 之外。常见收费路径包括:

Pod → 公网出口/NAT Gateway → 外部服务
Pod → 跨可用区节点 → Service 或数据库
集群 → 跨区域对象存储或备份
Ingress/LoadBalancer → 跨区域后端
日志采集 → 跨区域日志服务

同样是 1 TB 流量,经过不同路径可能产生完全不同的费用。尤其需要区分:

  • 入站流量和出站流量;
  • 同可用区和跨可用区流量;
  • 同区域不同服务之间的流量;
  • 公网出口、NAT、负载均衡和专线流量;
  • 重试流量和业务有效流量。

例如,一个服务部署在区域 A,数据库主节点在区域 B。即使应用和数据库都在同一云区域,只要跨可用区传输按流量收费,就可能产生双向费用。将副本均匀分布到多个可用区有助于可用性,但也可能增加跨区访问费用。优化目标不是“永远避免跨区”,而是比较:

额外跨区成本单区故障导致的业务损失\text{额外跨区成本} \quad \text{与} \quad \text{单区故障导致的业务损失}

应用层还应避免无意义的放大流量:

  • 大对象通过服务间同步而不是对象存储共享;
  • 无限重试导致流量倍增;
  • Sidecar 或代理重复传输日志;
  • 将本可本地聚合的数据频繁发送到中心服务;
  • 每个请求都跨区访问固定依赖。

网络优化必须结合延迟、故障域、数据合规和一致性要求。为了省跨区流量而把数据库和所有副本放进单一可用区,可能把可用性风险提高到不可接受的程度。


八、自动扩缩容如何影响成本和容量

1. HPA、VPA 和节点扩缩容解决不同问题

  • HPA 调整工作负载副本数;
  • VPA 调整 Pod 的资源建议或资源配置,VPA 不是 Kubernetes 核心内置控制器,通常作为独立项目安装;
  • Cluster Autoscaler 或云厂商节点扩缩容器 调整节点数量;
  • 节点选型工具 根据 Pod 约束选择实例类型和节点池。

HPA 的副本变化可能带来节点扩容,VPA 的 request 变化也可能让原本可装箱的 Pod 变成 Pending,继而触发节点扩容。几套控制器同时修改容量时,容易产生振荡:

利用率上升
  → HPA 增加副本
  → 节点扩容
  → 利用率下降
  → HPA 缩容
  → 节点缩容
  → 剩余 Pod 利用率再次升高

成本分析必须观察控制器动作的时间窗口,而不能只比较两个时刻的节点数。

2. Request 过大和过小都会增加成本

设节点总可分配资源为 AA,所有 Pod request 总和为 RR,粗略装箱利用率为:

Upacking=RAU_{\text{packing}} = \frac{R}{A}

RR 过大时,节点需要提前扩容,产生申请闲置;当 RR 过小时,调度器会把过多 Pod 放入节点,可能产生 CPU 争抢、内存压力和延迟恶化。

更可靠的优化目标不是最大化 R/AR/A,而是在满足故障余量和性能约束的前提下最小化节点数量:

minN\min N

约束包括:

每个 Pod 的 request 得到满足故障后剩余容量满足最低副本要求P95/P99 延迟不超过目标拓扑、污点、卷和安全约束成立\begin{aligned} &\text{每个 Pod 的 request 得到满足} \\ &\text{故障后剩余容量满足最低副本要求} \\ &\text{P95/P99 延迟不超过目标} \\ &\text{拓扑、污点、卷和安全约束成立} \end{aligned}

这也是为什么“把节点 CPU 使用率提升到 90%”不能作为通用目标。高利用率可能降低成本,但会减少故障余量、增加调度碎片和扩容响应压力。


九、优化边界:什么时候不应继续压低成本

成本优化存在明确边界,至少包括以下几类。

1. 可用性边界

如果服务要求跨可用区部署、保留故障余量或维持最低副本数,就不能把所有节点压缩到单个故障域。节省的节点费用必须与故障损失比较,而不是单看月账单。

2. 性能边界

CPU request 降低后,容器可能更容易被调度,但 CPU 争抢和 limit 节流可能使尾延迟上升。内存 request 降低后,节点可能因内存压力驱逐 Pod。优化前后应同时比较吞吐、错误率、P95/P99 延迟、OOM 和重启次数。

3. 数据边界

删除 PVC、快照、备份或降低副本数会影响恢复点目标(RPO)和恢复时间目标(RTO)。如果恢复演练没有成功,账面上省下的存储费用不能视为真实节省。

4. 运维边界

过度分散节点类型、Spot 池和调度规则会增加故障排查复杂度。一个每月节省少量费用、但需要大量人工处理 Pending Pod 和中断事件的方案,可能没有正向收益。

5. 组织边界

把共享成本全部摊给业务团队,会造成错误的优化激励;把共享成本全部留给平台团队,又可能让业务看不到资源浪费。成本模型应能区分:

可控成本:团队通过代码和配置可以改变
共享成本:多个团队共同产生
不可控成本:合同、控制面或最低购买承诺决定

十、一个可执行的治理闭环

成本治理应当形成“测量—归属—验证—变更—回滚”的闭环,而不是一次性清理。

第一步:建立资源和账单的对应关系

至少收集:

集群、节点池、节点类型和区域
Namespace、Pod、工作负载及归属标签
CPU/内存 request 与实际利用率
节点 allocatable 和系统开销
PVC、PV、StorageClass、快照和备份
网络出入方向、可用区和公网出口
HPA、PDB、拓扑约束和调度失败事件

云账单按节点池、磁盘、流量和服务分类,Kubernetes 指标按 Pod 和 Namespace 分类。两者需要通过节点标签、节点池元数据、时间窗口和分摊规则进行关联。

第二步:先找“无争议浪费”

优先检查:

kubectl get deploy,statefulset,daemonset,job,cronjob -A
kubectl get pods -A --field-selector=status.phase=Pending
kubectl get pvc -A
kubectl get nodes --show-labels
kubectl get events -A --sort-by=.lastTimestamp

适合优先处理的对象包括:

  • 已无业务流量的临时环境;
  • 完成但未清理的 Job;
  • 长期未使用的 PVC 和快照;
  • 明确过大的 request;
  • 空载但无法缩容的节点;
  • 失败重试造成的网络和计算放大。

“无争议”意味着删除或调整前能确认所有者、保留期限和恢复路径。

第三步:用变更前后的证据验证

每次调整都应记录:

变更对象和提交版本
变更前 request、节点数和账单基线
变更后的利用率、延迟、错误率和重启
HPA/扩缩容事件
Spot 中断和 Pod 重调度结果
PVC、快照和网络费用变化

例如将内存 request 从 2 GiB 调到 1.5 GiB 后,不能只看到节点数下降,还要观察至少一个完整业务高峰周期,并确认:

  • 没有新增 OOMKilled;
  • 没有出现内存压力驱逐;
  • P99 延迟未越过目标;
  • 滚动更新和节点故障演练仍能完成;
  • HPA 未出现异常振荡。

第四步:保留恢复路径

恢复路径包括:

  • 保留旧 Deployment 版本和资源配置;
  • 通过 Git 管理 YAML,避免直接修改后无法追溯;
  • 对 PVC 和云磁盘建立删除保护窗口;
  • 对 Spot 工作负载保留按需容量;
  • 对节点缩容设置可观察的排空过程;
  • 对策略拦截保留审计和豁免机制。

成本治理的成功标准不是某一天节点数最少,而是在满足业务约束的前提下,让每一单位付费资源都能被解释、归属、验证,并且在风险出现时可以恢复。


系列导航与关联阅读

官方资料

本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。