Rightsizing 与节省证据:从推荐值到真实兑现
一个团队收到成本平台给出的“可节省 40%”建议后,把所有 Deployment 的 CPU request 批量减半。账面 requests 立刻下降,节点数却没有变化;HPA 因利用率分母变小扩出更多副本,发布时又因碎片出现 Pending。另一个服务同时降低了 CPU limit,p99 延迟和节流快速上升。优化任务在工单里显示完成,云账单、SLO 和容量风险却都没有形成闭环。
Rightsizing 不是把推荐值写回 YAML。它是一项受控变更:先证明基线可信,再解释候选值怎样产生,经过容量、弹性和故障预算审查后灰度,最后用稳定业务量、真实资产和账单确认是否兑现。预测节省、可实现节省和已实现节省是三种状态,任何工具都不应跳过中间证据。
资源请求、限制、用量和价格不是同一个旋钮
Kubernetes 调度器主要依据 requests 放置 Pod;CPU limit 由内核通过节流执行,内存 limit 在压力下可能触发 OOM。实际 usage 描述执行消耗,却不会自动改写调度承诺。Kubernetes 资源管理明确区分了这些行为。
这会产生四种常见候选:
request 过高:调度器预留过多,节点整合和自动伸缩可能被阻碍。request 过低:Pod 更容易堆叠,运行时争用、驱逐和不可预测延迟增加。CPU limit 过低:即使节点有空闲 CPU,容器仍可能被 CFS 节流。
memory limit 过低:峰值或泄漏会触发 OOM,单看平均内存无法证明安全。
OpenCost 类工具会把 request、usage、资产价格和运行时间连接成分配模型;VPA 会依据历史使用和 OOM 等信号给出 lowerBound、target、upperBound。两者都能生成候选证据,却不拥有业务 SLO、发布波形、故障域冗余和变更审批。VPA 的 官方说明也把推荐、更新与准入拆成不同组件,推荐值不等于已经安全执行的生产规格。
用状态机管理每一项候选
把 rightsizing 候选保存为不可变记录,而不是一条会被覆盖的评论:
candidateId: checkout-api-cpu-v7
target:
cluster: <cluster-id>
namespace: <namespace>
workload: deployment/checkout-api
container: app
baseline:
specRevision: <git-revision>
window: <stable-window>
trafficCohort: checkout-paid
requestCpu: 800m
limitCpu: 1600m
proposal:
requestCpu: 500m
limitCpu: 1600m
evidence:
source: [prometheus, vpa-off, allocation, cloud-billing]
slo: [availability, p99_latency, error_rate]
capacity: [max_surge, failure_domain, pending_time]
guardrails:
maxCanaryShare: "<approved-share>"
rollbackOn: "slo_burn_or_capacity_regression"
owner:
workload: <team-id>
platform: <platform-owner>
approver: <change-approver>状态至少经过 Observed -> Candidate -> Approved -> Canary -> Realized。任一质量门禁失败则进入 Rejected;灰度退化进入 RolledBack;证据过期进入 Expired。Realized 只能由变更后稳定窗口、业务量归一化结果和账单/资产证据共同产生,不能由推荐系统自行标记。
specRevision 防止候选应用到已经变化的工作负载;trafficCohort 防止低流量窗口冒充优化;max_surge 和故障域证据防止稳态能跑、发布或节点故障时无法放置。每个字段只有一个 owner,GitOps、VPA 和优化平台不能同时写 requests。
建立可比较的基线
基线窗口要包含代表性流量、发布和周期峰值,并排除已知事故、压测污染和采集缺口。不要用“最近一天平均值”作为通用基线。先固定工作负载版本和规格:
export NS='<namespace>'
export WORKLOAD='checkout-api'
kubectl -n "${NS}" get deployment "${WORKLOAD}" -o yaml \
> /tmp/rightsizing-deployment-before.yaml
kubectl -n "${NS}" get deployment "${WORKLOAD}" \
-o jsonpath='{range .spec.template.spec.containers[*]}{.name}{" request="}{.resources.requests}{" limit="}{.resources.limits}{"\n"}{end}'
kubectl -n "${NS}" get hpa,pdb
kubectl -n "${NS}" get pods -l app="${WORKLOAD}" -o wide
kubectl -n "${NS}" top pods --containerskubectl top 只提供近期资源指标,不能支撑长期分位数。Prometheus 或等价监控应保留 CPU usage、working set、CPU throttling、OOM、restart、eviction、Pod 数、Pending 时间、HPA desired/current replicas、节点 allocatable 与 requests。业务侧同时保存 QPS、有效订单、错误率、p99、队列年龄和超时。
对 CPU 与内存分别取证:
sum by (namespace, pod, container) (
rate(container_cpu_usage_seconds_total{namespace="<namespace>", container!=""}[<rate-window>])
)sum by (namespace, pod, container) (
container_memory_working_set_bytes{namespace="<namespace>", container!=""}
)sum by (namespace, pod, container) (
rate(container_cpu_cfs_throttled_periods_total{namespace="<namespace>", container!=""}[<rate-window>])
)
/
sum by (namespace, pod, container) (
rate(container_cpu_cfs_periods_total{namespace="<namespace>", container!=""}[<rate-window>])
)查询必须记录 Prometheus 水位、查询语句、聚合维度和工作负载 revision。平均值不能替代 p95/p99、启动峰值和单 Pod 长尾;缺样本的 Pod 不能按零使用处理。
以观察模式部署推荐链
已有 VPA 时,先确认 CRD 和组件版本,再为目标工作负载创建只推荐、不写回的对象。VPA 1.7.0 使用 autoscaling.k8s.io/v1;Off 模式让 Recommender 生成状态,但不由 Updater 改写 Pod:
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: checkout-api-observe
namespace: <namespace>
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: checkout-api
updatePolicy:
updateMode: "Off"
resourcePolicy:
containerPolicies:
- containerName: app
controlledResources: [cpu, memory]
controlledValues: RequestsOnly
minAllowed:
cpu: 100m
memory: 128Mi
maxAllowed:
cpu: "4"
memory: 8Gikubectl apply -f checkout-api-vpa-observe.yaml
kubectl -n "${NS}" get vpa checkout-api-observe -o yaml预期 .status.recommendation.containerRecommendations[] 出现目标容器和 bounds。空 recommendation 不是“无需优化”,通常表示历史不足、selector 不匹配、Metrics API 或 Recommender 异常。minAllowed、maxAllowed 是组织护栏,不应从当前 request 原样复制;RequestsOnly 避免推荐器顺带改变 limit 策略。
若未部署 VPA,不要让一次优化任务在共享集群临时安装控制器。可从 Prometheus 分位数生成候选,并把 VPA 安装交给平台变更。VPA 安装与更新模式、Goldilocks 推荐治理与容量建模分别处理控制器、推荐与调度证据,成本平台只消费其可追溯输出。
正向实验:对一个灰度工作负载降低 request
选择可回滚的沙箱或独立 canary Deployment,确保它与稳定版本有相同镜像、配置和代表性流量。先导出当前对象,再只修改一个变量。下面把 CPU request 从 800m 调为 500m,保持 CPU limit 不变:
kubectl -n "${NS}" get deployment "${WORKLOAD}" -o yaml \
> /tmp/rightsizing-before.yaml
kubectl -n "${NS}" set resources deployment "${WORKLOAD}" \
--containers=app \
--requests=cpu=500m,memory=512Mi \
--limits=cpu=1600m,memory=1Gi
kubectl -n "${NS}" rollout status deployment "${WORKLOAD}" --timeout=180s
kubectl -n "${NS}" get pods -l app="${WORKLOAD}" -o wide
kubectl -n "${NS}" get events --sort-by=.metadata.creationTimestamp预期新 ReplicaSet 收敛、可用副本不下降、没有新的 Pending/OOM/eviction,HPA 行为可解释,业务 SLO 与基线保持在批准预算内。CPU request 下降本身不会直接制造 CPU 节流;若 limit 未变却节流上升,应检查节点争用、Pod 放置和负载变化。若 HPA 使用 CPU utilization,request 下降会提高利用率分母比值,可能扩大副本数,必须把副本和节点结果纳入证据。
变更后保存新 revision、Pod UID、节点分布、指标窗口和 Allocation 查询。至少跨过一个代表性业务周期和一次正常发布,再判断候选是否稳定。只观察几分钟内的 Running 没有证明内存峰值、发布 surge 或故障域冗余安全。
反向实验:拒绝缺失 SLO 的“高收益”候选
先在本机创建一个候选 JSON,它有预测金额,却没有稳定基线和 SLO 证据:
cat > /tmp/rightsizing-candidate.json <<'JSON'
{
"candidateId": "checkout-api-cpu-v7",
"baselineFresh": false,
"projectedSavings": 1200,
"sloEvidence": [],
"rollbackArtifact": "/tmp/rightsizing-before.yaml",
"fieldOwner": "gitops"
}
JSON
jq -e '
.baselineFresh == true and
(.sloEvidence | length) > 0 and
(.rollbackArtifact | length) > 0 and
.fieldOwner == "gitops"
' /tmp/rightsizing-candidate.json预期 jq 返回非零状态,候选保持 Rejected,即使 projectedSavings 很高。把 baselineFresh 改为 true 并补入 availability、p99_latency、error_rate 后,结构门禁才可通过;后续仍需容量模拟和灰度,不会直接进入 Realized。
运行时反例应在沙箱完成。降低 CPU limit 会制造节流,降低 memory limit 可能制造 OOM,二者都不应在生产服务上作为探针。若灰度自然出现 SLO burn、OOM、节流、Pending 或副本异常,立即停止扩量并执行回滚,不必为了“收集足够样本”继续伤害流量。
回滚必须恢复规格和控制器关系
最直接的回滚是恢复已保存对象或 Git 中上一版资源字段:
kubectl apply -f /tmp/rightsizing-before.yaml
kubectl -n "${NS}" rollout status deployment "${WORKLOAD}" --timeout=180s
kubectl -n "${NS}" get deployment "${WORKLOAD}" -o jsonpath='{.metadata.generation}{"\n"}'
kubectl -n "${NS}" get pods -l app="${WORKLOAD}" -o wide若 GitOps 管理该对象,应提交恢复 revision,由同一控制器执行,而不是现场 kubectl apply 与 Git 互相覆盖。回滚后确认 requests、limits、HPA target、VPA mode、rollout strategy 和 Pod 分布都恢复;SLO 回稳且 Pending、OOM、节流不再增长,才算回滚完成。
使用 VPA 自动更新时,先把 update mode 切回 Off,再恢复 Git 字段。直接删除 VPA 不能恢复已经改过的 Pod template;直接恢复 YAML 也不能阻止仍在运行的控制器再次写回。
把预测节省和真实节省分开
候选系统常用当前 request 与建议 request 的差额估算节省:
projected_savings = current_allocated_cost - modeled_candidate_cost这只是潜力。request 降低后,节点可能因为碎片、PDB、DaemonSet、拓扑约束或最小节点数无法缩减;承诺折扣和预留容量也可能让短期发票不变。可实现节省要经过放置模拟与供应边界:
realizable_savings
= removable_asset_cost
- migration_cost
- added_reliability_headroom
- new_operational_cost已实现节省则比较变更前后稳定窗口的真实资产与有效成本,并按业务量、价格、汇率和产品组合归一化:
realized_savings
= normalized_baseline_cost
- normalized_post_change_cost
- one_time_change_cost证据包至少包含:候选 ID、旧新 revision、基线与变更后窗口、业务量、SLO、Pod/节点变化、Allocation 差异、云账单水位、承诺利用变化和一次性迁移成本。若节点数未变但空闲容量增加,可以记录“capacity released”,不能记作发票节省;若账单下降来自到期折扣或流量下降,也不能归因于 rightsizing。
容量、弹性与可靠性一起审查
单 Pod 推荐无法回答发布和故障场景。审批前至少检查:
maxSurge 新副本能否在当前 requests 下放置。丢失一个承诺故障域后,剩余容量能否满足恢复目标。HPA 指标是否以 request 为分母,候选会不会改变副本曲线。
节点自动伸缩和整合是否能真正释放实例,而非只制造碎片。DaemonSet、PDB、拓扑约束、GPU/设备和本地盘是否阻止迁移。Java 堆、页缓存、sidecar、启动峰值与批处理峰值是否包含在内存证据中。
对 stateful、延迟敏感、GPU 或低副本服务使用更小灰度和更长观察。批量候选按共同故障域限流,不能同一窗口缩减所有副本和节点余量。FinOps 负责证据和收益,工作负载 owner 对 SLO 负责,平台团队对调度与容量负责,财务确认账单兑现;任何一方都不应独自批准全链路。
权限、敏感信息与自动化边界
推荐链只需要读指标、工作负载规格和成本数据。执行链需要修改 Deployment、StatefulSet 或云资源,权限强度完全不同。将两类身份分开:
collector: get/list/watch workloads + read metrics + read cost views
recommender: write candidate store only
executor: patch approved targets in approved namespaces
approver: change-system decision, no cluster credential
auditor: read immutable evidence and action log执行身份不能拥有全集群通配写权限;每次动作绑定 candidate ID、目标 UID、旧 revision、新 revision、审批人和过期时间。账单折扣、租户成本、内部 SLO、资源名称和云账号都可能敏感,报表按职责授权,诊断包脱敏并记录下载审计。不要把 kubeconfig、云 token、Prometheus bearer token 或原始成本导出放进候选 YAML。
自动执行只适合证据成熟、回滚可靠、故障半径受限的对象。默认从 read-only recommendation 开始;经历多轮人工灰度后,再允许特定 namespace、特定资源字段和有限变更幅度自动化。任何工具只要无法提供 dry-run、审批、审计和 kill switch,就不应获得生产写权限。
按故障证据逐层排查
没有候选时,检查指标水位、Pod 与 controller 映射、容器名称、历史长度和 VPA recommendation;不要把空结果解释为“规格完美”。
候选异常激进时,检查是否遗漏启动峰值、OOM、低流量窗口、sidecar、季节性和故障流量;再检查推荐器版本、min/max 护栏与 percentile。候选长期不变时,检查 spec revision 是否漂移、字段 owner 是否冲突、GitOps 是否反复覆盖。
灰度 Pending 时,按 quota、节点 allocatable、拓扑、taint/toleration、PDB 和供应失败分型;灰度 SLO 退化时,区分 CPU 节流、内存压力、垃圾回收、网络、下游和流量偏差。账单不下降时,检查节点是否真正释放、承诺覆盖、最小容量、磁盘与负载均衡器残留、账单延迟和价格口径。
诊断不能只保留截图。保存候选 JSON、旧新 manifest、事件、指标查询、Prometheus 水位、Allocation 参数、云账单批次和回滚结果。这样一次失败能改进策略,而不是在下一轮重新猜测。
规模化治理、升级与退出
大规模推荐平台的成本来自指标查询、时间序列基数、候选存储、模拟计算、审批集成和账单回测。分批扫描 namespace,限制并发和回看窗口,缓存稳定基线;监控推荐延迟、候选过期率、API 限流、查询扫描量、执行失败和平台自身成本。候选数量不是绩效,已验证且兑现的净收益才是。
升级推荐器、VPA、成本模型或价格源时,固定旧新版本并行生成候选,比较推荐方向、幅度、缺失率和目标映射。新版本不得沿用旧版“已批准”状态;模型变化需要重新审批。若 API schema、字段所有权或权限扩大,停止自动执行并回到观察模式。
退出工具时先冻结新动作,导出候选、审批、执行、回滚和 realized savings 证据;把 requests/limits 的唯一所有权交还 Git 或指定控制器;删除 webhook/CRD 前确认没有对象依赖;撤销集群与账单身份,停止指标写入并按保留制度处理历史。替代系统必须能解释同一候选为何通过或拒绝,不能只迁移一个“推荐值”字段。
成熟的 rightsizing 流程不会承诺每次都省钱。它能明确区分释放容量、降低预测成本和减少真实账单,能在 SLO 或容量证据变坏时立即回滚,也能说明一笔节省由哪个变更兑现。这样的证据链,才允许优化从零散调参升级为可持续的架构治理。
