Kubernetes 弹性与容量证据模型及工具选型
一次容量事故复盘中,应用团队说 HPA 在 30 秒内完成扩容,平台团队说新节点在 4 分钟内 Ready,基础设施团队说云 API 没有报错。三条陈述都是真的,业务却持续超时了 17 分钟:指标晚到 2 分钟,Pod 因拓扑约束等待 5 分钟,新节点注册后又花 6 分钟拉取镜像,应用 readiness 还在等待缓存预热。每个局部组件都拿自己的绿色状态证明“不是我的问题”,整个系统却没有一条从需求到业务恢复的共同时间线。
另一场事故发生在缩容。Descheduler 驱逐了偏斜 Pod,Karpenter 随后整合节点,PDB 也没有被违反;但被驱逐的 GPU 任务找不到同型号设备,只能重新排队。控制器执行符合配置,不代表架构目标得到满足。证据模型要同时回答三件事:谁看见了什么事实、谁改变了哪个对象、业务用什么结果证明容量真的可用。
建立六层证据,不把状态对象当业务结果
把一次容量变化表示为 E = D + M + C + P + N + B:需求 D、指标 M、控制决策 C、放置 P、节点供给 N、业务结果 B。每层都要有稳定身份、时间、状态和 owner。
| 层 | 权威对象 | 最小证据 | 常见误判 |
|---|---|---|---|
D 需求 | 网关、队列、作业提交、SLO | QPS、并发、backlog、提交时间、错误预算 | 用 CPU 猜业务需求 |
M 指标 | Metrics API、Custom/External Metrics API | 查询值、时间戳、窗口、标签、采集错误 | kubectl top 正常就认为 HPA 信号正常 |
C 决策 | HPA、VPA、ScaledObject、Workload | generation、condition、recommendation、desired replicas、admission | 对象存在就认为控制器工作正常 |
P 放置 | Pod、Event、scheduler 日志、PodGroup | Pending 原因、调度 profile、候选节点与拒绝插件 | 集群总资源够就认为一定可调度 |
N 供给 | 节点组、NodePool、NodeClaim、云实例、Node | 请求、配额、实例、注册、taint、Ready | 云实例 Running 就认为节点可用 |
B 业务 | readiness、吞吐、延迟、错误率、队列水位 | 首个成功请求、稳态时间、积压清空、业务不变量 | Pod Ready 就认为容量恢复 |
一条证据必须能关联前后层。HPA 的 desired replicas 要关联使用的指标和值;Pending Pod 要关联它的 request、约束和调度 Event;NodeClaim 要关联触发它的不可调度需求、实例 ID 和 Node Ready 时刻;业务恢复要关联新副本真正接收流量后的吞吐和延迟。
时间线用单调事件,而不是截图拼图
所有参与系统必须使用可信时钟。集群节点、控制面、指标后端和压测机的时间偏差会让时间线出现“节点先创建,扩容后发生”的假象。复盘记录同时保存 RFC 3339 时间、对象 UID、resourceVersion 或 generation,并把查询时刻与事件发生时刻分开。
incident: capacity-example-001
workload:
namespace: checkout
kind: Deployment
name: api
uid: '<workload-uid>'
timeline:
demandObservedAt: '<incident-date>T02:00:00Z'
metricVisibleAt: '<incident-date>T02:01:25Z'
desiredReplicasChangedAt: '<incident-date>T02:01:40Z'
firstPodCreatedAt: '<incident-date>T02:01:42Z'
firstUnschedulableAt: '<incident-date>T02:01:44Z'
nodeRequestedAt: '<incident-date>T02:02:10Z'
nodeReadyAt: '<incident-date>T02:06:20Z'
firstNewPodReadyAt: '<incident-date>T02:10:55Z'
businessRecoveredAt: '<incident-date>T02:12:30Z'从这些时间计算 signalLag、controllerLag、schedulingLag、supplyLag、startupLag 和 recoveryLag。SLO 应约束整个 T0 -> T6,内部预算再分配给各层。只给 Cluster Autoscaler 一个“节点 5 分钟 Ready”目标,会漏掉指标和应用启动的大头。
先证明资源事实,再选择伸缩器
requests 是调度器、基于利用率的 HPA、VPA 推荐和节点伸缩模拟共享的输入。request 过小,Pod 容易过度装箱并在运行时争用;request 过大,集群看起来空闲却持续 Pending。CPU limit 通过节流执行,内存 limit 通常在压力下触发 OOM;两者的证据和业务影响不同。
export NS=checkout
export APP=api
kubectl get deploy "$APP" -n "$NS" -o jsonpath='{range .spec.template.spec.containers[*]}{.name}{"\t"}{.resources}{"\n"}{end}'
kubectl top pod -n "$NS" --containers
kubectl get resourcequota,limitrange -n "$NS" -o yaml
kubectl get pod -n "$NS" -l app="$APP" -o json | jq '.items[] | {name:.metadata.name,qos:.status.qosClass,restarts:[.status.containerStatuses[]? | .restartCount]}'若目标是应对流量并行度,优先考虑 HPA;若目标是修正单 Pod 资源画像,使用 VPA 推荐并经过审核;若信号来自队列或外部事件,KEDA 提供指标与激活路径。不要先选产品再寻找指标。业务信号无法稳定表达时,增加控制器只会自动化错误决策。
Kubernetes 资源管理给出了 request、limit 和执行行为;HPA 算法解释了利用率、缺失样本和稳定窗口;VPA 应按目标发行版的 VPA 1.7.0 文档核对更新模式与兼容性。
调度失败要还原约束交集
Pod 并不是从全体节点中选择“最空闲”的一个。Filter 阶段会依次排除资源不足、污点不容忍、NodeAffinity 不匹配、卷拓扑冲突、端口占用、拓扑分布不满足等节点;Score 只在剩余候选中排序。任何硬约束把候选集变成空集,节点总量都没有意义。
export NS=checkout
export POD='<pending-pod>'
kubectl get pod "$POD" -n "$NS" -o yaml
kubectl describe pod "$POD" -n "$NS"
kubectl get event -n "$NS" --field-selector involvedObject.name="$POD" --sort-by=.lastTimestamp
kubectl get node --show-labels
kubectl get pv,pvc -n "$NS" -o wide
kubectl get priorityclass反向实验可以给 Pod 增加一个不存在的 zone 标签硬亲和,预期 FailedScheduling 明确显示节点亲和不匹配。恢复时删除错误约束并观察同一个 Pod UID 是否重新进入调度队列。不要直接增加节点:若新节点模板也没有该标签,节点伸缩器只会得出“没有可行节点组”。
Topology Spread、反亲和与专用节点用于降低故障相关性,也会减少候选集并提高冗余成本。架构评审需要把可用性目标转换为所需 zone 数、每域最小容量、允许的 maxSkew 和故障时降级策略,而不是照抄一组 YAML。
节点供给工具由节点模型决定
Cluster Autoscaler 面向节点组:它模拟把不可调度 Pod 放入节点组模板,再调整该组规模。Karpenter 面向约束和实例候选:它创建 NodeClaim 并由 provider 选择具体云容量。托管集群还可能提供平台自己的节点自动供给,控制权、升级责任和可观测对象不同。
| 判断 | Cluster Autoscaler 更匹配 | Karpenter 更匹配 | 托管能力更匹配 |
|---|---|---|---|
| 容量池 | 固定、审批明确的节点组 | 规格、zone、容量类型候选较多 | 接受云平台抽象 |
| 可移植性 | provider 差异仍存在,但模型较统一 | provider 成熟度差异明显 | 与云平台绑定 |
| 实例选择 | 节点组预先决定 | 按 Pod 约束动态决定 | 由服务承诺决定 |
| 整合 | 组内 scale-down | consolidation、drift、expiration | 平台策略可能不可见 |
| 权限 | 调整节点组规模 | 创建实例、网络、角色等更广权限 | 云服务托管部分权限 |
| 退出 | 恢复固定节点组 | 迁回节点组并核销 NodePool/云权限 | 受平台迁移能力限制 |
选择前用同一组工作负载比较:无约束通用 Pod、单 zone PVC、Spot 可容忍任务、GPU Pod、超大 request Pod、受 PDB 保护的服务。记录扩容时间、可行性判断、实例选择、失败 Event、缩容行为和实际费用。单次“扩得更快”不能覆盖权限范围、供应波动和退出成本。
批任务先区分准入控制和实际调度
Kueue 创建 Workload 并根据 ClusterQueue、LocalQueue、ResourceFlavor、Cohort 和 AdmissionCheck 决定作业何时获得配额;准入后,默认 kube-scheduler 仍负责 Pod 放置。Volcano 提供 PodGroup、Queue、Gang Scheduling 和调度插件,可以通过 schedulerName: volcano 等路径参与实际放置。
提交 Job
-> Kueue:是否有配额、是否借用、是否满足准入检查
-> Job Controller:创建 Pod
-> kube-scheduler:Pod 放到哪个 Node
提交 VolcanoJob
-> Volcano Controller:生成 PodGroup / Pod
-> Volcano Scheduler:是否成组可调度、选择 Node、抢占或回填如果问题是租户配额、排队、公平共享和作业何时启动,优先评估 Kueue;如果问题是 gang、DRF、批量抢占、拓扑和指定负载的调度行为,评估 Volcano。两者可以组合但会增加状态机、webhook、升级和排障面,必须先画清谁暂停工作负载、谁创建 Pod、谁设置 schedulerName、谁拥有 Queue 配额。
用三组反向实验验证证据链
第一组破坏信号:暂时让 Custom Metrics Adapter 返回空序列,预期 HPA 的指标 condition 和 Event 失败,但业务需求仍持续。第二组破坏放置:给 Pod 增加不存在的硬标签,预期产生 Unschedulable,节点伸缩器应解释为什么没有可行容量。第三组破坏供给:在隔离环境把节点池最大规模设到当前值,预期供给层明确报告上限,不能静默等待。
# 每次实验前记录对象身份和起点
kubectl get hpa,pod -n "$NS" -o yaml > before.yaml
kubectl get event -n "$NS" --sort-by=.lastTimestamp > events-before.txt
# 实验后收集差异和业务证据
kubectl get hpa,pod -n "$NS" -o yaml > after.yaml
kubectl get event -n "$NS" --sort-by=.lastTimestamp > events-after.txt
diff -u before.yaml after.yaml || true实验必须有停止条件:错误率超过预算、生产队列受影响、跨租户资源被驱逐、云费用超过上限或回滚在规定时间内没有生效,立即终止。凭证使用短期 workload identity 或专用实验身份;示例中的 UID、账号、队列和端点全部使用占位符,证据归档先脱敏再共享。
成本模型要包含空闲与扰动
节点成本只是容量成本的一部分。完整模型至少包含空闲节点小时、过大 requests 造成的碎片、指标与控制器资源、镜像拉取和网络、Spot 中断、驱逐重启、队列等待导致的业务时延、值班与升级维护。为了降低节点数而频繁整合,可能把节省的计算费转成错误率和工程时间。
capacityCost = nodeHours
+ controlPlaneOverhead
+ idleAndFragmentation
+ scaleEventNetworkAndStorage
+ interruptionAndRetry
+ operationalLabor容量家族输出利用率、requests、节点空闲、碎片、扩缩事件和中断事实;账单分摊、showback、chargeback 与预算由 FinOps 系统处理。两者通过稳定资产 ID 和时间窗口连接,但不把成本平台反向变成调度器。
成本还要绑定停止条件。节点供给已经达到预算上限、下游服务开始限流、队列消息处理速度不再随副本增长、错误率跨过保护阈值时,继续扩容只会放大重试、连接和费用。控制器需要受 maxReplicas、NodePool/节点组 limits、云配额和业务熔断共同约束;这些边界分别由应用、平台、基础设施和业务 owner 管理,任何一方都不能用自己的局部上限代替全链路保护。月度复盘至少核对触顶次数、触顶时的业务结果、人工绕过记录和边界是否仍符合容量模型。
选择结论必须带回滚和长期 owner
任何工具选型都要填写最小合同:目标故障、输入事实、写入对象、成功证据、失败状态、权限、容量上限、升级责任、冻结开关、回滚路径和退出后核销项。没有这些信息的“采用 Karpenter”或“接入 KEDA”只是产品名,不是架构决策。
上线顺序从观察开始:先建立时间线与告警,再在隔离工作负载启用控制器,随后用阶梯、突发、故障和浸泡负载验证。迁移阶段保留旧路径但划清唯一写入者,不能让两套控制器同时管理同一节点池或副本字段。退出时删除 CRD 之前先导出对象和状态,恢复静态容量,核销 webhook、ClusterRole、云 IAM、Secret、实例、磁盘与持续费用。
控制器冲突的判定不能只靠“最后一次写入”。要同时查看 managedFields、generation、控制器日志与变更仓库,确认谁持续把值拉回。应急时先冻结自动写入者,再设置经过容量计算的静态值;业务稳定后按指标源、工作负载伸缩、调度、节点供给的顺序逐层恢复。若同时解冻,短时间内的旧指标、积压和节点回收可能再次触发振荡。每次演练都记录冻结耗时、静态容量可维持多久、恢复顺序和残留权限。
最终评审只接受可以复现的判断:需求何时出现、信号何时可见、谁作出什么决策、为什么能或不能放置、节点为何创建或失败、业务何时恢复、成本和扰动是多少。证据链完整后,工具选择才从偏好变成可审计的工程结论。
