弹性控制环可观测、升级迁移与安全退出
一次促销流量上升后,业务队列已经积压,HPA 也把期望副本从 6 改成 18,值班群里却出现了三个互相矛盾的结论:应用团队说“扩容已经触发”,平台团队说“节点已经创建”,业务监控仍显示超时率上升。后来才发现 12 个新 Pod 因拓扑约束 Pending,新节点又卡在 CNI 初始化,真正可服务的副本在需求出现后很久才恢复。大家保存了 HPA 截图和云控制台截图,却没有一条能串联信号、控制器决策、调度、节点、Pod Ready 与业务结果的时间线。
下一次升级更隐蔽。旧控制器和新控制器在同一容量域短暂共存,Helm values 中一个参数改了默认值,CRD 的存储版本也发生变化。新实例看起来 Ready,但旧对象的 status.observedGeneration 没有追上 metadata.generation;回退镜像后,旧二进制又无法解释新字段。团队最终靠人工删对象止血,留下 ClusterRoleBinding、Webhook、CRD finalizer 和云侧身份继续生效。升级不是换一个镜像,退出也不是删一个 Deployment,而是控制权、对象、行为证据与外部资源的完整交接。
用一条证据脊柱解释“容量为什么还没回来”
弹性系统至少跨过四类控制环:HPA、VPA 或 KEDA 改变副本与资源需求;kube-scheduler 决定 Pod 放置;Cluster Autoscaler、Karpenter 或托管节点能力改变供给;应用启动、探针与流量入口决定容量是否真的可服务。每一层都可能成功,同时下一层仍失败。因而“控制器 reconcile 成功”只能作为中间状态,不能直接兑现业务 SLO。
统一时间线从 T0 需求信号超过目标开始,依次记录指标样本时间、对象 generation 变化、控制器 observedGeneration、期望副本或 requests 变化、首次 FailedScheduling、节点供给决策、Node 注册与 Ready、Pod Scheduled、Pod Ready、Endpoint 可用以及业务延迟和错误率恢复。关键耗时可以写成:
T_total = T_signal + T_reconcile + T_schedule + T_node + T_startup + T_traffic
T_signal: 信号产生到控制器读到新鲜样本
T_reconcile:控制器读到样本到目标对象发生期望变更
T_schedule: Pod 创建到 Bind,或进入 Unschedulable
T_node: 供给决策到 Node 可调度
T_startup: Pod Scheduled 到 Ready
T_traffic: Ready 到 Endpoint/网关接流并恢复业务 SLO不要把所有耗时压成一个平均值。扩容路径关心高分位恢复时间、超时和失败原因;缩容路径还要观察稳定窗口、PDB 阻塞、驱逐、连接耗尽和资源释放。量化门槛来自业务 SLO、历史基线和云供给配额,例如“连续若干轮中,需求到可服务容量的高分位不劣于既有基线,且失败样本都能归到确定阶段”,而不是照抄一个脱离负载的分钟数。
在可销毁命名空间启用对象、事件、指标与审计入口
观测控制环不等于先安装一套新平台。最低可用入口由 Kubernetes API、Event、控制器日志和现有指标采集组成;集群已有 Prometheus Operator 时再用 ServiceMonitor 接入。实验需要一个非生产集群、kubectl、可创建 namespace/Deployment/HPA 的身份,以及只读 Node、Event、HPA 和目标控制器对象的权限。涉及 CRD、Webhook、ClusterRole 或控制器升级时必须另用受审批的平台身份。
先确认服务端版本、API 发现和权限。Kubernetes 1.36 发行线可作为示例基线,实际集群应以 kubectl version 返回值和所装控制器 release 为准:
kubectl version
kubectl api-resources --api-group=autoscaling
kubectl auth can-i list horizontalpodautoscalers.autoscaling -A
kubectl auth can-i list events -A
kubectl auth can-i get --raw /apis/metrics.k8s.io/v1beta1
kubectl create namespace elasticity-lifecycle-lab预期前三项能够显示 autoscaling/v2 HPA 和明确的 yes/no 权限结果;资源指标入口若未启用会返回 NotFound 或 APIService 不可用,这时 HPA 的资源指标实验也不会成立。Resource Metrics API 只提供短时 CPU/内存事实,不承担长期监控、计费或审计。接口语义可对照 Metrics API 与 HPA 行为说明。
已有 Prometheus 抓取体系时,应让每个控制器暴露带版本、控制器名、namespace 和结果码的低基数指标,并采集 reconcile 次数/时长、workqueue 深度与年龄、API 请求错误、期望/当前状态差、驱逐与供给结果。不要把 Pod UID、完整对象名、队列消息键或租户自由文本直接做 label,它们会扩大基数并泄露敏感业务事实。API Server 审计需由集群管理员按 审计文档配置,实验身份不应为了“看全日志”获得读取 Secret 或集群管理员权限。
观测入口的选型取决于要回答的问题:API 对象与 Event 适合判断当前状态和离散失败;时序指标适合比较趋势、分位数和 SLO;结构化日志适合解释单次 reconcile 分支;审计记录用于确认谁在何时写了哪个资源;跨应用、网关和依赖的 Trace 才能解释 Pod Ready 之后为何业务仍未恢复。底层机制是不同组件各自保存局部状态,任何单一面板都无法自动拼成因果链。小集群可以先用 API、Event 与控制器指标跑通,生产环境再接入既有时序、日志和审计平台;不要为这一条控制环另建一套无人维护的数据孤岛。
把字段变化翻译成运行影响与所有权
升级前最重要的不是比较两个 values 文件,而是建立“字段 -> 写入者 -> 运行对象 -> 可见证据 -> 回退动作”的映射。spec.replicas 可能由 HPA 持有,GitOps 若持续回写同一字段会制造振荡;VPA 会改变容器 requests,进而同时影响调度可行性、HPA 利用率分母和节点扩容模拟;Karpenter NodePool 的 limits、requirements 与 disruption 字段会改变可供给实例和中断速度;Kueue 的配额与准入状态决定批作业何时进入调度,而不是 Pod 最终放在哪里。
原生对象先看三组通用字段。metadata.generation 表示期望配置发生过变化;控制器处理后应更新 status.observedGeneration;status.conditions 应给出类型、状态、reason 和 message。若 generation 已前进而 observedGeneration 长时间落后,说明控制器尚未处理、处理失败、没有权限,或状态写回冲突。resourceVersion 用于并发控制,不是业务版本号;不要用它比较新旧配置语义。
CRD 升级还要区分 served version 与 storage version。一个版本可以继续被 API Server 接收,却不再是 etcd 中的存储版本;转换 Webhook 故障可能让 list/get 失败,旧数据也不会仅因 CRD 声明改变而自动全部重写。Kubernetes 的 CRD versioning说明了 spec.versions[*].served/storage、转换和迁移约束。进入变更窗口前至少保存:
kubectl get crd <crd-name> -o yaml > crd-before.yaml
kubectl get crd <crd-name> \
-o jsonpath='{.status.storedVersions}{"\n"}'
kubectl get <resource> -A -o yaml > objects-before.yaml
kubectl get deploy,sa,role,rolebinding,clusterrole,clusterrolebinding -A \
-l app.kubernetes.io/part-of=<controller-name> -o yaml > access-before.yaml这些快照可能包含内部名称、镜像仓库、云资源 ID 和注解,必须存入受控变更记录,分享前脱敏,不得上传到公开工单。Secret 只记录引用、类型、轮换 owner 与校验结果,不导出 data 内容。
正向实验:让可解释的扩容链完整走一遍
创建一个有明确 CPU request 的 Deployment 和 autoscaling/v2 HPA。averageUtilization 的分母来自 request;如果漏掉 request,CPU 利用率目标无法正确计算。下面使用 Kubernetes 示例镜像,受限网络应替换为组织镜像仓库中固定 digest 的等价制品:
apiVersion: apps/v1
kind: Deployment
metadata:
name: evidence-web
namespace: elasticity-lifecycle-lab
spec:
replicas: 1
selector:
matchLabels: {app: evidence-web}
template:
metadata:
labels: {app: evidence-web}
spec:
containers:
- name: server
image: registry.k8s.io/hpa-example:latest
resources:
requests: {cpu: 100m, memory: 64Mi}
limits: {cpu: 500m, memory: 128Mi}
---
apiVersion: v1
kind: Service
metadata:
name: evidence-web
namespace: elasticity-lifecycle-lab
spec:
selector: {app: evidence-web}
ports:
- port: 80
targetPort: 80
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: evidence-web
namespace: elasticity-lifecycle-lab
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: evidence-web
minReplicas: 1
maxReplicas: 6
behavior:
scaleDown:
stabilizationWindowSeconds: 300
metrics:
- type: Resource
resource:
name: cpu
target: {type: Utilization, averageUtilization: 50}保存为 elasticity-evidence.yaml 后应用,并从临时 Pod 产生负载:
kubectl apply -f elasticity-evidence.yaml
kubectl -n elasticity-lifecycle-lab rollout status deploy/evidence-web
kubectl -n elasticity-lifecycle-lab run load --rm -it --restart=Never \
--image=busybox:1.36 -- /bin/sh -c \
'while true; do wget -q -O- http://evidence-web 2>/dev/null; done'另一个终端持续记录对象与事件:
kubectl -n elasticity-lifecycle-lab get hpa evidence-web -w
kubectl -n elasticity-lifecycle-lab get deploy,pod -o wide -w
kubectl -n elasticity-lifecycle-lab get events --sort-by=.lastTimestamp
kubectl -n elasticity-lifecycle-lab get hpa evidence-web -o yaml正向证据应连续出现:HPA 当前指标超过目标,desiredReplicas 上升,Deployment generation 前进,新 Pod 被创建并完成调度,Ready 副本增加,HPA condition 没有持续错误。若触发了节点扩容,还应追加 Unschedulable 事件、供给控制器决策、Node Ready 与新 Pod Ready 时间。停止负载后,缩容不会立即发生,因为 stabilizationWindowSeconds 会保留近期较高建议;这正是字段对抖动和成本的直接影响。
反向实验:用权限与字段冲突暴露“假健康”
第一类反例是让两个写入者争夺 spec.replicas。HPA 工作时执行一次人工 patch,再观察副本被改回:
kubectl -n elasticity-lifecycle-lab scale deploy/evidence-web --replicas=2
kubectl -n elasticity-lifecycle-lab get hpa,deploy -w
kubectl -n elasticity-lifecycle-lab get deploy evidence-web \
-o jsonpath='{.metadata.managedFields[*].manager}{"\n"}'预期 HPA 在后续同步中重新写 /scale,人工数字不能稳定保持。如果 GitOps 也声明固定 replicas,现象会变成持续漂移、频繁 patch 或副本振荡。修复不是扩大稳定窗口,而是明确字段所有权:启用 HPA 的工作负载从交付模板移除固定 replicas,紧急人工接管则先暂停对应自动写入者,并保留恢复步骤。
第二类反例是剥夺实验 HPA 读取目标或指标的能力,实际环境可通过一个只绑定实验 ServiceAccount 的副本控制器演练,不能修改共享控制器权限。可先用 impersonation 验证,而不真正破坏组件:
kubectl auth can-i get deployments/scale.apps \
-n elasticity-lifecycle-lab --as=system:serviceaccount:elasticity-lifecycle-lab:observer
kubectl auth can-i get pods.metrics.k8s.io \
-n elasticity-lifecycle-lab --as=system:serviceaccount:elasticity-lifecycle-lab:observer预期未授权身份返回 no。真实控制器缺权时,HPA condition 常出现 FailedGetScale、FailedGetResourceMetric 或相关 reason,日志出现 Forbidden,generation 与 observedGeneration/状态时间线停滞。诊断顺序是对象 condition、Event、APIService/指标可用性、控制器日志、审计记录和 kubectl auth can-i,而不是先重启控制器。修复 RBAC 后必须看到下一轮 reconcile 成功、condition 恢复以及业务容量证据闭合。
升级门禁同时检查 API、CRD、参数与行为
控制器升级包应固定镜像 digest、Helm chart/manifest 版本和 Kubernetes 兼容矩阵。安装顺序通常是 API/CRD 兼容检查、CRD 或转换组件、RBAC/ServiceAccount、controller Deployment、Webhook/APIService,最后才启用会写业务对象的策略。直接 helm upgrade 无法证明 CRD 已升级,因为 Helm 对 CRD 生命周期有特殊处理;应按该项目 release 指南显式验证。
升级前用服务端 dry-run 发现已删除 API 与 schema 错误,并查弃用入口:
kubectl apply --server-side --dry-run=server -f <rendered-manifests>
kubectl get --raw /metrics | grep apiserver_requested_deprecated_apis
kubectl api-resources
kubectl explain <resource>.<group> --recursiveAPI 弃用与移除应核对 Kubernetes deprecation guide;集群升级前还要检查审计或指标中是否仍有旧客户端请求被删除版本。kubectl convert 不是通用的自定义资源迁移器,CRD storage version 迁移应使用项目支持的 migrator 或经过验证的读写重存流程,并在备份与回退演练后执行。
参数漂移分三类:字段改名或删除会导致启动失败;默认值变化会让配置仍合法但行为改变;单位或语义变化会让同一个数字产生不同节奏。将旧、新 controller 的 --help、渲染后的 args/env、ConfigMap、Helm values schema 和运行时 /metrics 标签做结构化 diff。重点比较并发 worker、reconcile 周期、超时、重试、稳定窗口、批量驱逐、API QPS/burst、leader election 和 feature gate。门禁不是“Pod Ready”,而是 canary 容量域中的决策频率、错误率、队列年龄、API 压力、业务恢复时间与旧基线相容。
双轨迁移必须隔离容量域和写入字段
双轨的目的不是让两个控制器竞争同一对象,而是让新链路在独立容量域承接可控流量。新旧轨道使用不同的 namespace/selector、schedulerName、NodePool/节点组标签与污点、Queue、ServiceAccount、云标签和指标查询范围;每个可变字段只能有一个写入者。对 HPA/KEDA 迁移,隔离 ScaledObject/HPA 与 /scale 所有权;对节点供给迁移,隔离 NodePool/节点组及可落入其上的 Pod;对批队列迁移,隔离 LocalQueue/ClusterQueue 和 admission owner。
推荐顺序是:先部署新控制器但保持策略冻结或只观察;在 canary 域创建等价对象;注入可重复需求;比较两条证据脊柱;扩大 canary;冻结旧轨新增准入或扩容;排空旧容量;确认业务和成本不变量;再撤销旧身份。迁移记录至少保留目标对象 UID/generation、控制器版本与 digest、策略摘要、指标窗口、事件 reason、审计主体和外部资源 ID 的脱敏映射。
回退触发条件应在放量前写好:新轨持续无法追上 generation;恢复时间高分位显著劣化;API 错误或 workqueue age 单调增长;出现无法解释的驱逐/供给;成本或外部配额失控;转换 Webhook 影响对象读写。回退时先停止新流量和新写入,再恢复旧轨兼容对象与权限;不能在新 CRD 数据已经写入后直接降级二进制。若旧版本无法读取新 storage version,就必须先执行已演练的数据回迁或保留向前修复路径。
权限、凭证和敏感数据决定故障半径
弹性控制器通常需要读工作负载、写 /scale、创建或驱逐 Pod、管理 CRD,节点供给器还会调用云 API。把这些能力放进一个通配 ClusterRole 和长期 AK/SK,会让一个被攻陷的 Pod 同时拥有扩容、停机和消费云预算的能力。按控制环拆 ServiceAccount;namespace 控制器优先使用 Role;确需集群资源时把 ClusterRole 限到准确 group/resource/verb;云侧使用短期 workload identity,并用资源标签、区域、实例族和额度限制权限。
升级时 RBAC 常见两种失败:新版本增加 watch/status/finalizers 权限却未更新 Role,控制器 Ready 但 reconcile 持续 Forbidden;旧版本遗留的 ClusterRoleBinding 在卸载后仍把高权限授给可复用 ServiceAccount。用下列命令建立权限差异与主体清单:
kubectl auth can-i --list \
--as=system:serviceaccount:<namespace>:<service-account> -n <namespace>
kubectl get clusterrolebinding,rolebinding -A -o json \
| jq '.items[] | select(.subjects[]?.name == "<service-account>")'
kubectl get validatingwebhookconfiguration,mutatingwebhookconfiguration
kubectl get apiservice控制器日志、Event、审计和对象注解可能包含队列名、镜像地址、云实例 ID、指标查询、租户标签和内部拓扑。采集平台应限制保留期与查询权限,敏感 label 做 allowlist,导出样本用 example.com、<team-name> 和哈希化对象标识。凭证只验证“引用存在、挂载身份正确、轮换成功、旧凭证失效”,不把 Secret 值打印进流水线。
容量与成本要覆盖控制器自身和迁移重叠期
弹性系统自身也消耗容量。控制器副本、leader election、workqueue、缓存 informer、指标 adapter、Prometheus 抓取与日志存储都会占 CPU、内存、API QPS 和遥测预算。对象数与事件风暴增长时,瓶颈可能先出现在 API Server、指标后端或云 API,而不是业务节点。HA 副本提高可用性,但通常只有 leader 执行写操作,其他副本仍有 list/watch 和缓存成本。
迁移期的成本峰值由新旧控制器并存、双份缓冲节点、镜像冷启动、遥测双写、测试流量和暂不整合的 canary 组成。容量计划应预留这段重叠,而不是要求新轨立即靠缩容“自证节省”。需要同时观察:控制器 CPU/内存高分位、workqueue depth/age、reconcile 延迟、API 429/5xx、指标查询时延、Pending 年龄、Node 启动与空闲时间、可服务副本、碎片率、遥测基数和单位业务容量成本。
成本下降只有在 SLO、故障域和退出能力不退化时才成立。更激进的稳定窗口和整合参数可能减少空闲费,却增加冷启动和中断;减少日志保留会降低存储费,却可能让长周期容量事故无法归因。owner 应为指标保留、采样、告警窗口、容量缓冲和紧急冻结设置不同责任人,并定期用可重复负载校验阈值。
安全退出按冻结、排空、撤权和核销完成
安全退出先冻结新的控制动作,而不是先删 Deployment。停止新建策略和新准入,确认新 owner 已接管副本、requests、队列或节点容量;随后让旧控制器进入暂停/观察模式,记录最后一个成功 observedGeneration。对节点与批队列还要等待工作负载排空、PDB/终止宽限成立和外部资源释放。删除顺序通常是业务自定义对象、finalizer 已完成的实例对象、controller/Webhook/APIService、namespace RBAC、集群 RBAC、云身份,最后才评估 CRD。
CRD 不是普通空壳。删除 CRD 会级联删除其全部自定义资源;对象 finalizer 若依赖已删除控制器,又会长期卡住。退出前执行清单与存储版本检查:
kubectl get <resource> -A
kubectl get <resource> -A -o json \
| jq '.items[] | {name:.metadata.name, finalizers:.metadata.finalizers}'
kubectl get crd <crd-name> -o jsonpath='{.status.storedVersions}{"\n"}'
kubectl get validatingwebhookconfiguration,mutatingwebhookconfiguration \
-l app.kubernetes.io/part-of=<controller-name>
kubectl get clusterrole,clusterrolebinding \
-l app.kubernetes.io/part-of=<controller-name>只有业务对象归零、finalizer 完成、转换依赖解除、备份保留策略确认后,才删除 CRD。云侧继续核对实例、磁盘、网卡、IP、负载均衡目标、队列订阅和工作负载身份;指标侧核对抓取 target、告警、Dashboard、日志流和高基数 series 是否消失。最后用旧 ServiceAccount 和旧云身份做否定验证,预期 Kubernetes API 返回 Forbidden、云 API 拒绝调用,并确认没有控制器继续写旧对象。
清理与回滚必须遵守对象依赖:先停止负载与自动写入,恢复仍需保留的旧策略,再删除 namespace 内资源,最后核对集群范围和云侧残留。实验环境可按以下顺序回收;若负载 Pod 仍存在,先停止负载并等待 HPA 回到稳定状态:
kubectl delete namespace elasticity-lifecycle-lab --wait=true
kubectl get namespace elasticity-lifecycle-lab
kubectl get events -A --field-selector involvedObject.namespace=elasticity-lifecycle-lab预期 namespace 返回 NotFound,集群范围没有为实验创建的 ClusterRoleBinding、Webhook、APIService 或 CRD。若曾为实验创建云节点池、指标规则或临时身份,还必须在各自系统中核销并验证拒绝访问。应用团队拥有 readiness、requests 与业务 SLO;平台团队拥有控制器、调度和节点证据;安全团队拥有 RBAC、云身份和审计;可观测团队拥有信号新鲜度与保留;FinOps/容量 owner 解释缓冲和迁移重叠成本。一次升级或退出只有在这些责任共同给出证据后才结束。
