节点启动、排空、整合与缩容安全
凌晨扩容后,云控制台显示新虚机已经运行,节点对象也出现在 kubectl get nodes 中,但一批订单 Pod 仍然 Pending。事件里交替出现 node(s) had untolerated taint 和 Insufficient cpu;新节点上的 CNI、CSI 与日志 DaemonSet 还没完成初始化,业务 Pod 却因为一个过宽的 toleration 提前进入。它们先经历网络探针失败,随后在卷挂载超时中重启。自动扩容增加了账单,却没有增加可用业务容量。
几个小时后,缩容控制器又选中了另一台低利用率节点。值班人员执行 kubectl drain,命令被一个严格的 PodDisruptionBudget 长时间阻塞;有人改用 --disable-eviction 强制删除,恰好把唯一健康副本赶走。节点最终消失,但请求错误率上升,终止中的 Pod 来不及完成连接摘除,云盘还滞留在旧可用区。问题不在某条命令,而在团队把“实例存在、节点 Ready、Pod 可调度、Pod Ready、业务容量可用”误当成同一个状态。
先把节点生命周期拆成六个可观察状态
云实例 Running 只说明计算资源已启动。kubelet 注册 Node 后,节点可能仍为 NotReady;CNI 没有写好网络、CSI 没有注册驱动、镜像缓存为空、NodeLocal DNS 或安全代理尚未工作时,它还不能承载真实流量。Ready=True 也只代表 kubelet 心跳与节点健康条件满足,不代表每个 DaemonSet、挂载路径和业务依赖都可用。
一条可治理的节点时间线至少包含六个状态:供给系统创建实例;bootstrap 配置启动 kubelet;节点注册并带初始化污点;基础 DaemonSet 完成;移除初始化污点后进入可调度状态;退出时先 cordon、再按 Eviction API drain、等待终止宽限与卷分离,最后才删除实例。Karpenter 的整合或 Cluster Autoscaler 的缩容只是这条退出链的触发者,不能绕过调度器、PDB、kubelet 和云资源控制器。
排障时把时间戳串在一起,而不是只截一张 kubectl get nodes:实例创建时间、Node creationTimestamp、Ready 条件转换时间、关键 DaemonSet Ready 时间、初始化污点移除时间、首个业务 Pod Ready 时间,以及终止时的 cordon、eviction、Pod 删除、卷 detach 和实例删除时间。业务恢复时间应从需求出现算到可服务副本出现,而不是算到虚机开机。
在隔离节点池建立可销毁实验入口
这些实验需要一个至少三节点的非生产集群、可创建 Deployment/PDB 的 namespace,以及对实验节点执行 label、taint、cordon 与 drain 的权限。kubectl drain 属于客户端命令,不需要安装控制器;云节点自动供给则应使用平台已经批准的 Cluster Autoscaler、Karpenter 或托管能力。先读取真实版本和对象,避免把示例字段直接套到不兼容发行线:
kubectl version
kubectl auth can-i patch nodes
kubectl auth can-i create pods/eviction -n node-life-lab
kubectl get nodes -o wide
kubectl get ds -A -o wide
kubectl get storageclass选一台没有生产负载的实验节点并加上专用标签。下面的 <lab-node> 必须替换成已审批节点名,不能靠模糊匹配批量操作:
kubectl create namespace node-life-lab
kubectl label node <lab-node> lifecycle-lab=true
kubectl taint node <lab-node> startup.example.com/initializing=true:NoSchedule
kubectl get node <lab-node> \
-o custom-columns=NAME:.metadata.name,READY:.status.conditions[-1].status,TAINTS:.spec.taints预期能看到标签和 NoSchedule 污点。没有对应 toleration 的新 Pod 不会落到该节点;既有 Pod 不会因为 NoSchedule 自动被驱逐。实验身份应只操作带 lifecycle-lab=true 的节点,生产环境可再用准入策略限制 Node 修改来源。
启动污点把初始化窗口变成显式协议
初始化污点的价值是把“节点尚未准备好”编码成调度约束。只有 CNI、CSI、监控、安全代理、DNS 和必要镜像预热完成后,受控 bootstrap 组件才移除它。Karpenter 的 startupTaints 会把预期由其他组件移除的污点写入 NodePool;如果实际 DaemonSet 使用了不同 key,Karpenter 可能持续认为新节点仍有待满足需求并继续供给。
业务 Deployment 不应普遍容忍初始化污点。需要在初始化阶段运行的 DaemonSet,才配置精确的 key、effect 和必要时的 operator:
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: startup-sentinel
namespace: node-life-lab
spec:
selector:
matchLabels:
app: startup-sentinel
template:
metadata:
labels:
app: startup-sentinel
spec:
tolerations:
- key: startup.example.com/initializing
operator: Equal
value: "true"
effect: NoSchedule
nodeSelector:
lifecycle-lab: "true"
containers:
- name: sentinel
image: registry.k8s.io/pause:3.10应用后检查 kubectl -n node-life-lab get pod -o wide。预期 sentinel 能进入实验节点,而普通 Pod 仍被挡住。真实移除器还应检查 CNI 路由、CSI node registration、关键 DaemonSet numberReady 和节点条件,不能只睡眠固定秒数。通过后执行精确删除:
kubectl taint node <lab-node> startup.example.com/initializing=true:NoSchedule-
kubectl get events -A --field-selector involvedObject.name=<lab-node> \
--sort-by=.lastTimestamp若污点长期不消失,先看负责移除它的 controller/DaemonSet 日志及 RBAC,再看节点条件。直接人工删除会掩盖 bootstrap 缺陷,并让下一批节点重复失败。
正向实验:让排空在业务副本仍可用时完成
创建三个副本并用 hostname 拓扑分散,再用 PDB 保证自愿中断期间至少两个副本可用。镜像来自 Kubernetes 官方示例仓库;受限网络可替换为组织已镜像并固定 digest 的等价镜像。
apiVersion: apps/v1
kind: Deployment
metadata:
name: drain-safe
namespace: node-life-lab
spec:
replicas: 3
selector:
matchLabels:
app: drain-safe
template:
metadata:
labels:
app: drain-safe
spec:
terminationGracePeriodSeconds: 30
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: drain-safe
containers:
- name: web
image: registry.k8s.io/e2e-test-images/agnhost:2.53
args: ["netexec", "--http-port=8080"]
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /healthz
port: 8080
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: drain-safe
namespace: node-life-lab
spec:
minAvailable: 2
selector:
matchLabels:
app: drain-safe将清单保存为 drain-safe.yaml 后执行:
kubectl apply -f drain-safe.yaml
kubectl -n node-life-lab rollout status deploy/drain-safe
kubectl -n node-life-lab get pod -l app=drain-safe -o wide
kubectl -n node-life-lab get pdb drain-safe
kubectl cordon <node-with-one-lab-pod>
kubectl drain <node-with-one-lab-pod> \
--ignore-daemonsets \
--delete-emptydir-data \
--grace-period=30 \
--timeout=5m预期 drain 通过 Eviction API 逐个请求驱逐,Deployment 在其他节点补足副本,PDB 的 disruptionsAllowed 随可用副本变化。证据不是命令退出码 alone,而是目标节点变为 SchedulingDisabled、旧 Pod 完成终止、新 Pod 在其他节点 Ready、可用副本始终不低于预算,并且业务探针连续成功。Kubernetes 安全排空文档说明了 --ignore-daemonsets 等行为;PDB 文档强调预算保护的是自愿中断,不是节点突然失联。
实验完成后恢复节点:
kubectl uncordon <node-with-one-lab-pod>
kubectl get node <node-with-one-lab-pod> \
-o custom-columns=NAME:.metadata.name,UNSCHEDULABLE:.spec.unschedulable反向实验:用不可满足的 PDB 暴露缩容阻塞
把预算改成三个副本全部可用,再排空承载其中一个副本的节点:
kubectl -n node-life-lab patch pdb drain-safe --type merge \
-p '{"spec":{"minAvailable":3}}'
kubectl -n node-life-lab get pdb drain-safe
kubectl cordon <node-with-one-lab-pod>
kubectl drain <node-with-one-lab-pod> \
--ignore-daemonsets \
--delete-emptydir-data \
--timeout=90s预期 drain 超时,并出现类似 Cannot evict pod as it would violate the pod's disruption budget 的信息;Pod 不应被静默删除。再用以下证据区分“预算阻塞”和“没有替代容量”:
kubectl -n node-life-lab get pdb drain-safe -o yaml
kubectl -n node-life-lab get events --sort-by=.lastTimestamp
kubectl -n node-life-lab get pod -l app=drain-safe -o wide
kubectl describe node <node-with-one-lab-pod>disruptionsAllowed: 0 且健康副本等于 desiredHealthy 指向预算本身;若 eviction 成功但替代 Pod Pending,则继续看调度事件中的资源、亲和性、污点和拓扑约束。不要把 --disable-eviction 当修复,它会绕过 PDB 并改用 DELETE。恢复预算并解除封锁:
kubectl -n node-life-lab patch pdb drain-safe --type merge \
-p '{"spec":{"minAvailable":2}}'
kubectl uncordon <node-with-one-lab-pod>终止宽限、preStop 与节点关机不是同一层
收到删除请求后,kubelet 运行 preStop,发送 TERM,并在 terminationGracePeriodSeconds 内等待容器退出;endpoint 传播、Ingress/网关摘流、连接耗尽和应用提交都要装进这个窗口。宽限期过短会中断请求,过长会拖慢 drain 和 Spot 接管。应用应在收到 TERM 后停止接收新请求、完成有界清理,并让 readiness 尽快失败,而不是只靠固定 sleep。
计划关机还可使用 kubelet 的 Graceful Node Shutdown 配置,为普通 Pod 和 critical Pod 分配关机宽限。它依赖操作系统通知与 kubelet 配置,云实例被强制断电、内核崩溃或网络分区时无法提供同等保证。Spot/抢占式实例的中断通知也有严格时限,节点终止处理器需要提前 cordon/drain;应用仍必须容忍通知丢失和突然故障。
含本地持久数据、emptyDir、hostPath、长事务或单副本状态的 Pod 不能仅靠 drain 获得数据安全。--delete-emptydir-data 明确允许删除本地临时数据,执行前应知道里面是否真的可重建;静态 Pod、裸 Pod、DaemonSet 和带 finalizer 的对象也各有不同处理方式。先从 kubectl drain --dry-run=server 和对象清单确认候选,再进入变更窗口。
整合与缩容必须先证明替代容量存在
Cluster Autoscaler 通常从节点组模型判断一个节点能否移除,并模拟其 Pod 是否可在其他节点调度;Karpenter 则围绕 NodePool、NodeClaim 和 disruption budget 计算 empty、drifted 或 underutilized 节点的替代。两者都依赖 Pod requests、调度约束、PDB、系统 Pod 和云供给事实。错误 requests 会让模拟与真实容量同时失真。
Karpenter NodePool 可用 spec.disruption.consolidationPolicy 和 consolidateAfter 控制整合节奏,并用 budgets 限制并发中断;字段应以所用发行线的 Disruption 文档为准。高波动服务不适合极短整合窗口:新节点冷启动、镜像拉取、卷拓扑和应用预热可能让节省的空闲费转化为延迟与错误率。
判断某节点可回收时,至少同时成立:所有非 DaemonSet Pod 都有可行落点;PDB 允许中断;替代节点覆盖架构、可用区、存储和设备;剩余节点有 requests 余量;关键服务的可用副本与拓扑不变量成立;终止后卷、负载均衡目标和云实例能按时释放。只看 CPU 平均利用率会漏掉内存、临时盘、GPU、端口、本地 PV 和硬亲和性碎片。
权限、云身份与敏感证据要分开保管
能够 patch Node、创建 pods/eviction、修改 PDB 和删除实例的身份组合起来,几乎可以让整个集群停机。日常观察身份只读 Node、Pod、Event 和 PDB;排空操作员只在批准窗口操作指定节点池;自动供给控制器使用专用 ServiceAccount 和云工作负载身份;云侧权限限制在受管实例、模板、安全组、子网与标签,不授予账户级通配权限。
bootstrap token、kubelet client certificate、云实例角色、镜像拉取凭证和 CNI/CSI 凭证都属于不同信任域。不要把云 AK/SK 放进 Node user-data、日志或普通 ConfigMap。首选短期工作负载身份,并让审计日志能把 NodeClaim/实例创建、Node patch、Eviction 和实例终止关联到同一变更。节点日志可能包含内部地址、镜像路径、卷 ID 和工作负载名,导出故障证据前需要脱敏和限权。
Kubernetes 的 Node authorization 与 NodeRestriction 能约束 kubelet 修改对象的范围,但不能替代平台控制器的最小 RBAC。业务团队不应拥有任意节点标签修改权,否则可伪造专用池、合规域或高信任工作负载的落点。
用容量时间线计算成本,而不是只数节点
节点成本由实例运行时间、未释放卷和地址、跨区流量、镜像拉取、DaemonSet 固定开销、冷启动缓冲和中断重试共同组成。大节点减少 DaemonSet 比例,却扩大单次排空半径;小节点更灵活,但系统预留与碎片比例更高。Spot 降低单价,却增加中断处理、冗余和跨故障域供给要求。
容量报表应同时记录 需求出现 -> NodeClaim/扩容决策 -> 实例 Running -> Node 注册 -> Ready -> 初始化污点移除 -> Pod Scheduled -> Pod Ready -> 业务 SLO 恢复。缩容则记录候选发现、cordon、首次 eviction、最后 Pod 退出、卷 detach、Node 删除和实例终止。这样才能区分云启动慢、bootstrap 慢、调度约束过窄、镜像慢和应用预热慢。
治理指标包括节点启动分位数、初始化污点年龄、DaemonSet Ready 延迟、Pending Pod 年龄、drain 时长、PDB 阻塞次数、替代 Pod Ready 延迟、整合失败原因、终止后孤儿卷/地址数量,以及单位可服务 request 的节点成本。阈值来自业务 SLO、启动基线和供给配额,不能复制演示数字。
升级、回滚与退出要保留节点所有权
升级 kubelet、CNI、CSI、自动供给器或节点镜像前,先创建小规模 canary 节点池。验证注册、初始化污点、所有关键 DaemonSet、DNS、网络策略、卷挂载、镜像、业务探针、drain 和实例清理,再逐批替换旧池。回滚不是把控制器镜像改回去:新节点镜像、NodePool/节点组字段、CRD、云启动模板和已创建实例都要有兼容路径。
从 Cluster Autoscaler 迁移到 Karpenter,或从自管供给迁移到托管能力时,不能让两个控制器同时管理同一容量域。新旧节点池使用不同标签、污点、云标签和权限;先让新池承接 canary,证明扩容与缩容证据链,再冻结旧池扩容、排空旧节点、核对卷与云资源,最后撤销旧 ServiceAccount、ClusterRoleBinding 和云身份。
实验环境按对象依赖倒序清理,先确认没有 Pod 仍需要实验节点:
kubectl delete namespace node-life-lab --wait=true
kubectl taint node <lab-node> startup.example.com/initializing=true:NoSchedule- \
--ignore-not-found
kubectl label node <lab-node> lifecycle-lab- \
--ignore-not-found
kubectl uncordon <lab-node>
kubectl get node <lab-node> -o wide若实验创建过临时节点池或云实例,还要从对应控制器删除声明并核对实例、磁盘、网卡、地址和负载均衡目标是否归零。平台团队拥有 bootstrap 与供给控制器,应用团队拥有 readiness、终止行为和 PDB,存储/网络团队拥有 detach 与摘流证据,安全团队拥有节点身份与审计,容量 owner 决定缓冲、整合节奏和 Spot 比例。任何一方都不能用“节点已经删掉”替代业务可用与外部资源清理证据。
