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

Kubernetes 节点维护:Cordon、Drain、Eviction、DaemonSet 和恢复验证

节点维护不是简单地执行一条 kubectl drain 命令。一个节点从“继续接收工作负载”变成“安全退出”,通常会同时涉及:

  • Cordon:阻止调度器把新的普通 Pod 放到节点上;
  • Drain:迁移或删除节点上的可迁移 Pod,并等待它们结束;
  • Eviction:通过 Kubernetes API 请求驱逐 Pod,并受 PodDisruptionBudget 约束;
  • DaemonSet:代表节点级代理、网络、日志、存储等组件,其 Pod 不应按普通工作负载迁移;
  • 恢复验证:节点重新加入调度前,必须确认 kubelet、Node Condition、Lease、污点、网络、存储和业务副本都已恢复。

这些动作分别由调度器、控制器管理器、API Server、kubelet、DaemonSet 控制器和应用控制器参与完成。理解它们的边界,比记住几个命令更重要。


一、先区分节点状态、调度状态和工作负载状态

1. Node 的 Ready 不是“允许调度”的同义词

Node 至少存在三类容易混淆的状态信息:

  1. Node Condition

    • Ready=True:kubelet 能够向控制面报告节点基本健康;
    • Ready=False:节点明确不健康;
    • Ready=Unknown:控制面在一段时间内没有收到有效状态;
    • MemoryPressureDiskPressurePIDPressure 等:资源或系统压力状态。
  2. Node Lease

    • kubelet 会周期性更新位于 kube-node-lease 命名空间中的 Lease;
    • Lease 主要用于快速判断 kubelet 是否仍然活跃;
    • Lease 正常不等于业务一定正常,Lease 失联也不等于 Pod 已经立即消失。
  3. spec.unschedulable

    • true 表示节点被 cordon,不接受普通调度;
    • 它不是 Node Condition;
    • 它不等于 Ready=False,一个节点可以同时是 Ready=Trueunschedulable=true

可以先查看这些信息:

kubectl get node worker-1
kubectl describe node worker-1
kubectl get lease -n kube-node-lease worker-1 -o yaml

典型的健康节点可能显示:

NAME       STATUS   ROLES    AGE   VERSION
worker-1   Ready    <none>   20d   v1.xx.y

STATUS 通常会把 ReadySchedulingDisabled 等信息组合展示。例如被 cordon 的节点可能显示:

worker-1   Ready,SchedulingDisabled   <none>   20d   v1.xx.y

这里的 SchedulingDisabled 只说明新 Pod 不会被普通调度到该节点,并不说明节点已经停止运行现有 Pod。


1.1 手工 cordon 与故障自动隔离不是同一条路径

管理员执行:

kubectl cordon worker-1

本质上会把 Node 的 spec.unschedulable 设置为 true。调度器之后不会把新的、普通的 Pod 绑定到该节点。

节点异常时,控制面还可能通过污点传播故障。例如常见的:

node.kubernetes.io/not-ready:NoSchedule
node.kubernetes.io/not-ready:NoExecute
node.kubernetes.io/unreachable:NoSchedule
node.kubernetes.io/unreachable:NoExecute

它们与 cordon 有不同作用:

  • NoSchedule:阻止不容忍该污点的新 Pod 调度;
  • NoExecute:还可能驱逐已经运行在节点上的、不容忍该污点的 Pod;
  • unschedulable=true:是调度层面的人工或工具设置,主要阻止新调度;
  • Node Controller 的故障处理:受心跳、Lease、Node 状态和相关超时影响,并非执行 cordon 的同义操作。

因此,Ready=TrueReady=FalseSchedulingDisabledNoExecute 污点必须分别观察,不能仅凭 kubectl get node 的一列输出判断完整状态。


二、Cordon:只改变“以后能不能调度”,不迁移现有 Pod

2.1 Cordon 的语义

执行:

kubectl cordon worker-1

可以理解为:

允许绑定到节点=Pod 的调度约束满足¬Node.spec.unschedulable\text{允许绑定到节点} = \text{Pod 的调度约束满足} \land \neg \text{Node.spec.unschedulable}

worker-1.spec.unschedulable=true 时,调度器不会为普通 Pod 选择它,即使:

  • 节点还有充足 CPU 和内存;
  • Pod 的 nodeSelector、亲和性和拓扑约束都匹配;
  • 节点仍然是 Ready=True

Cordon 不会自动执行以下动作

  • 不会删除 Pod;
  • 不会把 Pod 复制到其他节点;
  • 不会等待 Pod 变为 Terminated
  • 不会保证 StatefulSet、Deployment 或 Job 已经在其他节点运行;
  • 不会停止 DaemonSet Pod;
  • 不会修复节点上的 kubelet、容器运行时、CNI 或 CSI。

检查结果:

kubectl get node worker-1 -o jsonpath='{.spec.unschedulable}{"\n"}'

预期为:

true

也可以查看节点上的 Pod:

kubectl get pod -A -o wide --field-selector spec.nodeName=worker-1

Cordon 后,这些现有 Pod 通常仍然处于 Running,所以只执行 cordon 不能称为“节点已排空”。


2.2 为什么先 cordon 再 drain

drain 的目标是在节点维护期间排出可迁移工作负载。先 cordon 的因果关系是:

  1. 先禁止新的普通 Pod 进入节点;
  2. 再处理已有 Pod;
  3. 迁移过程中,其他控制器创建的新副本不会把节点重新填满;
  4. 维护期间节点不会继续承接新的业务调度。

当前 kubectl drain 在正常使用时也会尝试将节点标记为不可调度,但显式执行 cordon 有两个好处:

  • 维护脚本的状态转换更清晰;
  • 如果后续 drain 因 PDB、孤立 Pod 或超时失败,节点仍不会继续接收新工作负载。

不过,DaemonSet 控制器是一个重要例外:DaemonSet 的目标是“每个匹配节点运行一个 Pod”,它可以继续在被 cordon 的节点上维持自己的 Pod。cordon 不会阻止 DaemonSet 的节点级代理运行。


2.3 Uncordon:恢复调度资格,而不是恢复业务

恢复时可以执行:

kubectl uncordon worker-1

它将 spec.unschedulable 清除,使节点重新成为普通调度候选节点。

uncordon 不会:

  • 重新创建被删除的业务 Pod;
  • 清除所有故障污点;
  • 修复节点网络或磁盘;
  • 保证节点上的旧 Pod 仍然存在;
  • 自动验证应用副本是否满足可用性。

因此,uncordon 应该是验证流程的最后一步,而不是修复流程的第一步。


三、Drain:排空节点上的可迁移工作负载

3.1 Drain 实际要处理哪些 Pod

执行:

kubectl drain worker-1 \
  --ignore-daemonsets \
  --timeout=30m

通常会经历以下逻辑:

  1. 将节点标记为不可调度;
  2. 列出绑定到该节点的 Pod;
  3. 跳过或拒绝处理不适合 drain 的 Pod;
  4. 对符合条件的 Pod 发起驱逐或删除;
  5. 等待这些 Pod 从节点上消失;
  6. 由 Deployment、ReplicaSet、StatefulSet、Job 等控制器在其他可用节点创建替代 Pod;
  7. 所有目标 Pod 退出后,命令成功返回。

Drain 不是一个 Kubernetes API 对象,而是 kubectl 客户端执行的一套运维流程。它可能调用 Eviction API,也可能在某些情况下使用普通删除请求;具体行为受资源类型、API 能力和命令参数影响。


3.2 一个可控的排空流程

先检查节点上的 Pod:

kubectl get pod -A \
  --field-selector spec.nodeName=worker-1 \
  -o custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,OWNER:.metadata.ownerReferences[0].kind,PHASE:.status.phase'

然后执行:

kubectl cordon worker-1

kubectl drain worker-1 \
  --ignore-daemonsets \
  --timeout=30m

参数含义:

  • --ignore-daemonsets:忽略 DaemonSet 管理的 Pod;
  • --timeout=30m:整个 drain 操作最多等待 30 分钟;
  • 默认情况下,存在未被控制器管理的 Pod、镜像 Pod,或其他不可安全处理的 Pod 时,命令可能拒绝继续。

排空期间,可以在另一个终端观察:

kubectl get pod -A -o wide --watch

同时观察 PDB:

kubectl get pdb -A
kubectl describe pdb -n application api-pdb

观察节点上的剩余 Pod:

watch 'kubectl get pod -A -o wide --field-selector spec.nodeName=worker-1'

在没有特殊约束的 Deployment 场景中,预期现象是:

  • 原节点上的业务 Pod 进入 Terminating
  • 新节点上出现由控制器创建的新 Pod;
  • 新 Pod 就绪后,原 Pod 完成终止;
  • DaemonSet Pod 仍然存在;
  • drain 最终退出码为 0。

3.3 Drain 为什么可能卡住或失败

情况一:Pod 没有控制器

例如手工创建:

kubectl run debug-shell --image=busybox:1.36 -- sleep 3600

这个 Pod 没有 Deployment、Job 等控制器。删除它之后没有控制器负责重建,因此 kubectl drain 默认会拒绝处理,以避免管理员意外删除临时或重要工作负载。

只有在确认它确实可以丢弃时,才使用:

kubectl drain worker-1 \
  --ignore-daemonsets \
  --force \
  --timeout=30m

--force 的风险不是“强制迁移”,而是允许 drain 处理没有控制器管理的 Pod。它不提供数据安全保证。

情况二:使用了 emptyDir

如果 Pod 使用了本地临时目录:

volumes:
  - name: cache
    emptyDir: {}

迁移或删除 Pod 时,该 emptyDir 数据会丢失。若明确接受此损失,才使用:

kubectl drain worker-1 \
  --ignore-daemonsets \
  --delete-emptydir-data \
  --timeout=30m

这个参数只改变 kubectl 对风险的确认,不会把 emptyDir 转换成持久存储,也不会备份数据。

情况三:PodDisruptionBudget 阻止驱逐

如果 PDB 计算出当前不允许再发生自愿中断,Eviction API 会返回类似 HTTP 429 Too Many Requestskubectl drain 通常会持续重试或最终超时。

这不是命令失效,而是 Kubernetes 正在执行 PDB 的保护规则。直接改用普通删除可能绕过该保护,生产环境不能把它作为无条件的“解决方案”。

情况四:DaemonSet Pod

默认情况下,节点上有 DaemonSet Pod 时,drain 可能提示必须使用 --ignore-daemonsets。使用该参数后,kubectl 不会删除这些 Pod,因为 DaemonSet 控制器会继续认为节点需要它们。

这不是说明节点已经完全没有 Pod,而是说明“业务工作负载已排空,节点级 DaemonSet 被保留”。

情况五:静态 Pod和镜像 Pod

由 kubelet 静态清单管理的 Pod,其真实来源是节点文件系统中的 manifest,而不是普通 API 对象控制器。通过 API 删除它通常不能实现持久删除,kubelet 可能重新创建。

由 kubelet 镜像 Pod 机制产生的 Pod 也不是普通控制器管理。对这类 Pod 使用 --force 或手工删除前,应先确认其来源。否则可能得到一个“命令返回了,但节点上的组件仍在运行”的误判。


四、Eviction:受可用性策略约束的 Pod 驱逐请求

4.1 Eviction 的定义

Eviction 是 Kubernetes 提供的一个子资源,用于请求删除某个 Pod,同时让 API Server 根据 PodDisruptionBudget 等策略判断这次删除是否允许。

它与普通删除的关键区别是:

Eviction allowed满足 PDB 等准入约束\text{Eviction allowed} \Rightarrow \text{满足 PDB 等准入约束}

而普通删除通常表示:

DELETE Pod直接请求删除对象,不以 PDB 为准入条件\text{DELETE Pod} \Rightarrow \text{直接请求删除对象,不以 PDB 为准入条件}

Eviction 主要用于自愿中断,例如:

  • 节点维护;
  • 集群缩容;
  • 手工安全迁移;
  • 其他管理员或控制器主动减少 Pod。

它不是节点宕机时的强制故障恢复机制。节点突然断电、内核崩溃或网络隔离时,控制面无法先向 kubelet 发起一个正常的优雅驱逐流程。


4.2 使用 policy/v1 Eviction API

可以直接构造 Eviction 对象:

apiVersion: policy/v1
kind: Eviction
metadata:
  name: api-7d8f6c9d7b-abcde
  namespace: application

通过 API 提交:

kubectl create -f eviction.yaml

也可以使用 kubectl proxy 后发送 HTTP 请求,但命令行直接调用时必须正确编码命名空间和 Pod 名称。生产脚本通常优先使用客户端库或 kubectl 自带的 drain 逻辑,避免自行实现重试、超时和状态判断。

如果 PDB 不允许驱逐,API Server 可能返回:

Error from server (TooManyRequests): Cannot evict pod as it would violate the pod's disruption budget.

这时应检查:

kubectl get pdb -n application api-pdb -o yaml
kubectl get pod -n application -l app=api --show-labels
kubectl get pod -n application -l app=api \
  -o custom-columns='NAME:.metadata.name,READY:.status.containerStatuses[*].ready,PHASE:.status.phase,NODE:.spec.nodeName'

4.3 Eviction 的数据流

典型路径可以表示为:

sequenceDiagram
    participant Admin as kubectl/operator
    participant API as API Server
    participant PDB as PDB admission
    participant ETCD as etcd
    participant Kubelet as 节点 kubelet
    participant Ctrl as Deployment/ReplicaSet controller
    participant Sched as Scheduler

    Admin->>API: POST /api/v1/namespaces/ns/pods/pod/eviction
    API->>PDB: 检查可用 Pod 与 disruptionAllowed
    alt PDB 不允许
        PDB-->>API: 拒绝,HTTP 429
        API-->>Admin: Eviction failed
    else PDB 允许
        PDB-->>API: 允许
        API->>ETCD: 设置 Pod deletionTimestamp
        API-->>Admin: Eviction accepted
        API-->>Kubelet: Pod 进入删除流程
        Kubelet->>Kubelet: 执行 preStop、发送终止信号、等待 grace period
        Kubelet->>API: 更新 Pod 状态/最终删除
        Ctrl->>API: 发现副本不足,创建新 Pod
        Sched->>API: 为新 Pod 选择其他节点
    end

这里有三个容易忽略的并发关系:

  1. Eviction 被接受,不等于 Pod 已经立即从节点消失;
  2. 控制器可以在旧 Pod 仍处于 Terminating 时创建替代 Pod;
  3. 新 Pod 是否能在其他节点运行,还取决于资源、亲和性、拓扑约束、污点容忍和存储绑定。

因此,驱逐成功后仍要等待:

kubectl wait \
  --for=delete pod/api-7d8f6c9d7b-abcde \
  -n application \
  --timeout=5m

kubectl wait --for=delete 适用于指定对象仍存在的情况;如果对象已经不存在,命令行为应结合脚本逻辑处理,不能简单把“对象不存在”当成所有清理步骤都成功。


4.4 优雅终止不等于立即停止

Pod 被驱逐后,kubelet 通常会进行优雅终止:

  1. Pod 获得 deletionTimestamp
  2. kubelet 执行容器的终止流程;
  3. 如果配置了 preStop,按生命周期规则执行;
  4. 向主进程发送终止信号;
  5. 等待 terminationGracePeriodSeconds
  6. 仍未退出时才可能强制结束;
  7. Pod 对象最终从 API 中消失。

例如:

spec:
  terminationGracePeriodSeconds: 60

这只表示应用最多获得一个优雅退出窗口,不保证应用一定在 60 秒内完成事务提交。应用必须正确处理终止信号,连接池、消息消费、选主、临时文件和状态持久化都要与这个窗口相匹配。

如果在 drain 中使用:

kubectl drain worker-1 --grace-period=10

它会影响此次删除所使用的宽限期,可能覆盖 Pod 原有的终止时间设置。缩短该时间可能导致请求中断、事务回滚或消息重复处理,不能作为解决 drain 慢的默认手段。


五、PodDisruptionBudget 如何影响 Drain

5.1 PDB 保护的是“自愿中断”

PDB 通过标签选择器匹配一组 Pod,并声明在自愿中断期间至少保留多少可用副本,或者最多允许多少副本不可用。

例如:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api-pdb
  namespace: application
spec:
  maxUnavailable: 1
  selector:
    matchLabels:
      app: api

如果该选择器匹配 3 个 Pod,且 3 个 Pod 都 Ready,则最多允许 1 个 Pod 因 Eviction 进入中断。

一个简化模型是:

D=max(0,AR)D = \max(0, A - R)

其中:

  • AA:当前可用 Pod 数量;
  • RR:PDB 要求保留的可用数量;
  • DD:当前允许的自愿中断数量。

当使用 minAvailable 时:

R=minAvailableR = \text{minAvailable}

当使用 maxUnavailable 时:

D=maxUnavailableD = \text{maxUnavailable}

但实际计算还涉及选择器匹配、控制器期望副本数、不可用 Pod 统计和百分比取整,不能只看 Deployment 的副本数。


5.2 完整算例:为什么 drain 会被阻塞

假设 Deployment 有 3 个副本:

api-1   Ready   worker-1
api-2   Ready   worker-2
api-3   NotReady worker-3

PDB:

spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: api

当前可用数量:

A=2A = 2

要求保留:

R=2R = 2

因此:

D=max(0,22)=0D = \max(0, 2 - 2) = 0

如果此时维护 worker-1,驱逐 api-1 会使可用 Pod 从 2 降到 1,违反 minAvailable: 2,所以 Eviction API 拒绝请求。

正确的处理顺序应是:

  1. 先恢复 api-3,使 A=3A=3
  2. 此时 D=32=1D=3-2=1
  3. 驱逐 api-1
  4. 等待新的 Pod 在其他节点 Ready;
  5. 再进行下一个可能影响该 PDB 的操作。

如果把 PDB 临时改成 minAvailable: 1,确实可能允许排空,但这同时降低了业务可用性保证。它应当是经过容量和故障预算评估的变更,而不是为了让命令尽快返回而盲目修改。


5.3 PDB 的边界

PDB 不是所有故障的保护机制:

  • 节点断电时,系统无法先进行正常 Eviction;
  • kubelet 无法通信时,Pod 可能长时间处于未知或终止状态;
  • 强制删除可以绕过正常的自愿中断保护;
  • PDB 不会自动增加集群容量;
  • PDB 不会解决拓扑约束导致的新 Pod无法调度的问题。

此外,PDB 选择器必须与工作负载标签正确匹配。选择器不匹配时,PDB 看似存在,但实际没有保护目标。应检查:

kubectl get pdb -n application api-pdb -o yaml
kubectl get pod -n application --show-labels
kubectl get deployment -n application api -o yaml

如果使用百分比,必须注意取整。例如某些 PDB 百分比在副本数较小时,取整方向可能使实际允许中断数比直觉更严格或更宽松。生产维护前应查看 PDB 的状态字段,而不是只从 YAML 文本推算:

kubectl get pdb -n application api-pdb \
  -o custom-columns='NAME:.metadata.name,EXPECTED:.status.expectedPods,HEALTHY:.status.currentHealthy,DESIRED:.status.desiredHealthy,ALLOWED:.status.disruptionsAllowed'

字段具体展示依赖 Kubernetes 版本和客户端列定义,但核心应关注 currentHealthydesiredHealthydisruptionsAllowed


六、DaemonSet:节点级工作负载为什么不随普通 Pod Drain

6.1 DaemonSet 的控制目标

Deployment 的目标通常是某个数量的副本:

副本数N\text{副本数} \approx N

DaemonSet 的目标则更接近:

n匹配节点集合,运行一个或指定数量的 DaemonSet Pod\forall n \in \text{匹配节点集合},\quad \text{运行一个或指定数量的 DaemonSet Pod}

典型用途包括:

  • CNI 网络代理;
  • 节点日志采集;
  • 节点监控;
  • CSI 节点插件;
  • 安全或运行时代理;
  • 存储、设备和硬件发现组件。

当节点被 cordon 时,它仍然是一个匹配 DaemonSet 选择器的节点,因此 DaemonSet 控制器通常不会因为 unschedulable=true 就删除其 Pod。


6.2 为什么 drain 使用 --ignore-daemonsets

不使用参数时:

kubectl drain worker-1

如果节点上有 DaemonSet Pod,kubectl 通常会拒绝继续,并提示使用 --ignore-daemonsets

使用:

kubectl drain worker-1 --ignore-daemonsets

语义是:

  • 不把 DaemonSet Pod 视为需要排出的普通业务 Pod;
  • 不删除这些 DaemonSet Pod;
  • 允许 drain 继续处理其他 Pod。

它不是“忽略所有系统 Pod”,也不是“强制删除 DaemonSet”。例如静态 Pod、镜像 Pod和未管理 Pod仍可能触发其他检查。


6.3 Drain 后节点仍有 Pod是否正常

假设节点上只剩下:

kube-system   cni-agent-xxxxx       DaemonSet
kube-system   node-exporter-yyyyy   DaemonSet
kube-system   csi-node-zzzzz        DaemonSet

这通常表示普通工作负载已经排空,而节点级代理仍在运行。此时不能用:

kubectl get pod -A --field-selector spec.nodeName=worker-1

输出非空作为“drain 失败”的唯一判断依据。

更准确的判断是区分 owner:

kubectl get pod -A \
  --field-selector spec.nodeName=worker-1 \
  -o json

然后检查 metadata.ownerReferences 是否指向 DaemonSet,以及是否存在:

  • Deployment/ReplicaSet 管理的业务 Pod;
  • StatefulSet 管理的有状态 Pod;
  • Job 管理的任务 Pod;
  • 没有 owner 的孤立 Pod;
  • 静态 Pod或镜像 Pod。

6.4 DaemonSet 的污点容忍与维护风险

许多 DaemonSet 会配置对节点污点的容忍,以便在故障或初始化场景中继续运行。例如网络插件、存储插件经常需要在节点状态特殊时仍存在。

这意味着:

  • cordon 不一定阻止 DaemonSet;
  • NoSchedule 污点不一定阻止有对应 toleration 的 DaemonSet;
  • NoExecute 污点是否驱逐某个 DaemonSet Pod,取决于该 Pod 的 toleration;
  • 删除 DaemonSet Pod 后,控制器可能立即重建它。

因此,升级内核、容器运行时或 CNI 时,不应只依赖 --ignore-daemonsets。应明确知道每个 DaemonSet 的终止和重建行为,否则可能在节点维护期间失去日志、网络、存储或监控能力。


七、从维护前检查到节点恢复的完整流程

下面是一套适合单节点维护的基础流程。节点名、命名空间和工作负载名称都应替换为实际值。

7.1 维护前:确认节点和集群容量

kubectl get node worker-1 -o wide
kubectl describe node worker-1
kubectl get events --all-namespaces \
  --field-selector involvedObject.kind=Node,involvedObject.name=worker-1 \
  --sort-by=.lastTimestamp

检查:

  • Ready 是否为 True
  • 是否已有 MemoryPressureDiskPressurePIDPressure
  • 是否存在 NoScheduleNoExecute 污点;
  • kubelet Lease 是否持续更新;
  • 其他节点是否有足够的可分配 CPU、内存和临时存储;
  • Pod 的 nodeSelector、亲和性、反亲和性和拓扑分布是否允许迁移;
  • 相关 PVC 是否可以在目标节点挂载;
  • 业务 PDB 是否允许至少一次驱逐。

查看节点资源:

kubectl get nodes
kubectl top node 2>/dev/null || true
kubectl describe node worker-2

kubectl top 依赖 Metrics Server,命令失败不代表节点一定不健康。资源调度还使用 Pod requests 和 Node allocatable,不完全等于实时使用量。


7.2 维护前:识别所有 Pod 来源

kubectl get pod -A \
  --field-selector spec.nodeName=worker-1 \
  -o custom-columns='NS:.metadata.namespace,NAME:.metadata.name,OWNER_KIND:.metadata.ownerReferences[0].kind,OWNER_NAME:.metadata.ownerReferences[0].name,PHASE:.status.phase'

重点分类:

Pod 类型 默认处理思路
Deployment/ReplicaSet 通常可通过 Eviction 迁移
StatefulSet 需确认副本、存储和顺序语义
Job/CronJob 需确认中断是否会影响任务结果
DaemonSet 通常使用 --ignore-daemonsets,不作为普通 Pod 排空
无 owner Pod 默认拒绝,确认可丢弃后才考虑 --force
使用 emptyDir 删除或迁移会丢失本地临时数据
静态 Pod/镜像 Pod 先定位 kubelet manifest 或生成来源
关键系统 Pod 确认维护期间控制面、网络、DNS、存储仍可用

有状态 Pod 即使被安全 Eviction,也不意味着数据已经迁移。PVC 的绑定、访问模式、卷分区、CSI attach/detach 过程都可能延长恢复时间。


7.3 执行排空

kubectl cordon worker-1

kubectl drain worker-1 \
  --ignore-daemonsets \
  --timeout=30m

如果存在明确确认可以删除的 emptyDir 数据:

kubectl drain worker-1 \
  --ignore-daemonsets \
  --delete-emptydir-data \
  --timeout=30m

如果存在确认可以丢弃的无 owner Pod:

kubectl drain worker-1 \
  --ignore-daemonsets \
  --force \
  --timeout=30m

不建议把所有危险参数一次性组合:

# 不应作为默认生产命令
kubectl drain worker-1 \
  --ignore-daemonsets \
  --force \
  --delete-emptydir-data

这会同时放宽 DaemonSet、孤立 Pod 和本地数据三个不同层面的安全检查,失败原因变得难以区分,也容易把本不应删除的数据带走。


7.4 排空期间如何判断是“慢”还是“错误”

Pod 处于 Terminating

查看:

kubectl get pod -A -o wide --field-selector spec.nodeName=worker-1
kubectl describe pod -n application api-xxxxx

重点看:

  • deletionTimestamp
  • preStop 是否阻塞;
  • 容器是否忽略终止信号;
  • Volume 卸载是否卡住;
  • finalizer 是否未完成;
  • kubelet 是否仍然能更新状态;
  • Pod 是否被 terminationGracePeriodSeconds 限制。

新 Pod 一直 Pending

查看:

kubectl get pod -n application -l app=api
kubectl describe pod -n application api-new-xxxxx

常见事件包括:

0/3 nodes are available: insufficient cpu
0/3 nodes are available: pod has unbound immediate PersistentVolumeClaims
node(s) didn't match Pod's node affinity/selector
node(s) had untolerated taint

这说明 drain 已经发起迁移,但集群没有满足调度约束的目标节点。增加 --timeout 只会延长等待,不会创造资源或改变亲和性规则。

PDB 一直不允许

查看:

kubectl get pdb -A
kubectl describe pdb -n application api-pdb
kubectl get pod -n application -l app=api \
  -o custom-columns='NAME:.metadata.name,READY:.status.containerStatuses[*].ready,NODE:.spec.nodeName'

应先恢复不健康副本、增加可用容量或调整维护批次,而不是立即绕过 PDB。


八、Drain、普通 Delete 和节点故障驱逐的区别

操作 发起者 是否通常检查 PDB 典型场景 主要风险
kubectl drain 管理员客户端 通常使用 Eviction 节点计划维护 受容量、PDB、Pod 类型约束
Eviction API 客户端或控制器 请求一次受保护驱逐 可能返回 429
kubectl delete pod 管理员客户端 通常不以 PDB 为准 明确删除对象 可能绕过可用性保护
节点故障处理 Node Controller 等 不等同于正常 Eviction 节点失联、NotReady 可能无法优雅终止,状态延迟

“Pod 被删除”与“Pod 被驱逐”在结果上都可能导致 Pod 结束,但准入语义不同。生产脚本不应通过批量 kubectl delete pod -l ... 模拟安全 drain,因为这会绕过节点维护所需的可用性约束。

某些旧教程会使用 policy/v1beta1 Eviction。当前稳定 API 应使用:

apiVersion: policy/v1
kind: Eviction

具体 Kubernetes 发行版、托管平台或旧客户端可能存在版本偏差;提交前可检查:

kubectl api-resources | grep -i eviction
kubectl explain eviction

云厂商的节点替换、自动扩缩容和托管升级还可能在后台执行额外的 drain、实例删除或容量调度。不要假设云平台一定沿用与你手工命令完全相同的超时和强制删除策略,应查阅对应平台的节点升级行为。


九、恢复节点:先恢复系统,再恢复调度

节点维护结束后,不能只执行:

kubectl uncordon worker-1

更可靠的顺序是先在节点主机上确认:

  • kubelet 正常运行;
  • 容器运行时正常;
  • CNI 接口、路由和 DNS 正常;
  • 节点磁盘、inode、PID 和内存没有压力;
  • CSI 节点插件正常;
  • 内核、挂载点和设备状态符合预期;
  • 节点时间同步正常;
  • 节点没有遗留的维护污点或错误配置。

随后检查控制面观察到的状态:

kubectl get node worker-1
kubectl describe node worker-1
kubectl get lease -n kube-node-lease worker-1 -o yaml

至少确认:

Ready=True
MemoryPressure=False
DiskPressure=False
PIDPressure=False

这些 Condition 的具体集合会随版本和节点实现变化,但 Ready 与资源压力是恢复验证的核心。


9.1 检查污点和调度标志

kubectl get node worker-1 \
  -o jsonpath='unschedulable={.spec.unschedulable}{"\n"}taints={.spec.taints}{"\n"}'

确认:

  • unschedulable 不再为 true
  • 不存在本次维护遗留的 NoScheduleNoExecute 污点;
  • 如果污点由云平台、Node Controller 或自动化系统添加,先确认其来源,再决定是否清除。

只有明确知道污点用途时,才执行:

kubectl taint node worker-1 key=value:NoSchedule-

盲目删除污点可能重新允许工作负载进入本来仍不安全的节点。


9.2 恢复 DaemonSet 和基础设施组件

kubectl get pod -A -o wide --field-selector spec.nodeName=worker-1
kubectl get daemonset -A
kubectl rollout status daemonset/cni-agent -n kube-system --timeout=10m

cni-agent 替换为实际名称。应确认:

  • CNI DaemonSet Pod 为 Ready
  • 节点网络插件已在本机建立;
  • kube-proxy 或等价组件正常;
  • CSI node plugin 正常;
  • 日志、监控和安全代理恢复;
  • DNS、Service 网络和跨节点通信可用。

如果节点在升级过程中曾经删除或重建 DaemonSet Pod,kubectl get daemonsetDESIREDCURRENTREADYAVAILABLE 应与集群状态一致。


9.3 取消 cordon

当节点和基础设施检查通过后:

kubectl uncordon worker-1

验证:

kubectl get node worker-1

预期不再出现:

SchedulingDisabled

但此时业务 Pod 不一定立即回到该节点。调度器只会为新创建且满足调度约束的 Pod 选择节点;已经在其他节点运行的 Pod 不会因为 uncordon 自动迁回。节点恢复后的“重新均衡”不是 uncordon 的默认行为。


十、恢复后的端到端验证

10.1 验证节点能够承接新 Pod

可以创建一个临时测试 Pod:

kubectl run node-recovery-check \
  --image=busybox:1.36 \
  --restart=Never \
  --command -- sh -c 'echo node-ok; sleep 20'

查看其调度位置:

kubectl get pod node-recovery-check -o wide

如果需要确保测试 Pod 能落到目标节点,可使用临时 nodeSelector,但这会测试“节点标签和调度约束”,不一定等价于普通业务调度:

kubectl get node worker-1 --show-labels
kubectl delete pod node-recovery-check --ignore-not-found

测试完成后清理:

kubectl delete pod node-recovery-check --ignore-not-found

不要把“测试 Pod 被调度成功”当作完整健康证明。它可能只验证了 kubelet 能启动一个简单容器,未验证 CNI、Service、PVC、探针、业务依赖和节点资源压力。


10.2 验证工作负载控制器和业务可用性

kubectl get deployment,statefulset,daemonset -A
kubectl get pod -A
kubectl get events -A --sort-by=.lastTimestamp

对关键 Deployment:

kubectl rollout status deployment/api \
  -n application \
  --timeout=10m

对关键 Service:

kubectl get endpointslice -n application \
  -l kubernetes.io/service-name=api

检查:

  • Deployment 的 availableReplicas 是否满足预期;
  • StatefulSet 的序号副本是否都 Ready;
  • PDB 的 disruptionsAllowed 是否恢复到合理值;
  • Service 的 EndpointSlice 是否包含预期后端;
  • readiness probe 是否通过;
  • 应用日志是否有连接重建、数据恢复或重复消费问题;
  • PVC 是否完成重新挂载;
  • 监控和日志是否能看到恢复节点的数据。

10.3 验证“没有残留 Pod”时要排除正常例外

可以先列出所有 Pod:

kubectl get pod -A \
  --field-selector spec.nodeName=worker-1 \
  -o wide

然后逐个确认其来源。一个被成功 drain 的节点仍可能保留:

  • DaemonSet Pod;
  • 静态 Pod;
  • 镜像 Pod;
  • 某些节点级管理组件。

所以正确的问题不是“节点上是否还有任何 Pod”,而是:

节点上是否还存在本次维护目标中的业务 Pod,或者存在未被明确批准保留的 Pod?


十一、常见误区与反例

误区一:Cordon 会迁移 Pod

反例:

kubectl cordon worker-1
kubectl get pod -A -o wide --field-selector spec.nodeName=worker-1

如果原有 Pod 仍为 Running,这是正确行为。Cordon 只影响未来调度,不负责迁移当前工作负载。


误区二:Drain 成功意味着节点完全没有 Pod

反例:节点只剩 DaemonSet、静态 Pod 或镜像 Pod。drain 成功表示符合 drain 目标的 Pod 已被处理,不表示 kubelet 上没有任何容器。


误区三:Eviction 成功意味着新副本已经 Ready

Eviction API 接受请求后,旧 Pod 可能仍在 Terminating,新 Pod 可能处于 PendingContainerCreating 或探针失败状态。必须分别等待:

kubectl get pod -n application -l app=api -w

并检查新副本的调度、启动和 Ready 状态。


误区四:PDB 能防止所有节点故障造成的中断

PDB 只约束受支持的自愿驱逐路径。节点断电、硬件故障、网络分区和 kubelet 崩溃不一定有机会进行正常 Eviction。高可用还需要:

  • 多节点部署;
  • 合理的拓扑分布;
  • 足够的备用容量;
  • 应用级重试和故障转移;
  • 正确的存储和数据复制策略。

误区五:为了排空成功而直接强删

例如:

kubectl delete pod -n application api-xxxxx --force --grace-period=0

这可能只会快速从 API Server 中移除对象,并不等于节点上的进程已经结束。网络分区或 kubelet 失联时,旧进程可能仍在运行,造成:

  • 双实例同时访问外部系统;
  • Stateful 工作负载重复挂载或脑裂;
  • 消息重复消费;
  • 控制器过早创建替代副本;
  • API 状态与实际进程不一致。

强制删除只适用于已经评估过节点确实不可恢复、旧 Pod 不会继续运行,或业务明确允许该风险的场景。


误区六:恢复时只执行 uncordon

如果 Node 仍为 NotReady、存在 DiskPressure,或者 CNI/CSI 没有恢复,uncordon 只会让调度器重新尝试使用一个仍不健康的节点。恢复验证必须覆盖节点状态、污点、DaemonSet、调度、存储和业务。


十二、生产取舍:维护速度与可用性之间的约束

节点排空的安全性可以粗略看成多个条件的交集:

安全排空=节点可控目标副本可迁移PDB允许中断目标节点有容量存储可重挂载应用能优雅终止\text{安全排空} = \text{节点可控} \land \text{目标副本可迁移} \land \text{PDB允许中断} \land \text{目标节点有容量} \land \text{存储可重挂载} \land \text{应用能优雅终止}

其中任意一项为假,drain 都可能阻塞或产生业务风险。

例如:

  • 节点资源充足,但 PDB 不允许中断:Eviction 被拒绝;
  • PDB 允许中断,但其他节点没有 CPU:新 Pod Pending;
  • 资源和 PDB 都满足,但 Pod 使用本地 emptyDir:数据丢失;
  • 新 Pod 已启动,但 Service readiness 未通过:业务可用副本仍不足;
  • 节点已经恢复 Ready,但遗留 NoSchedule 污点:uncordon 后仍无法调度;
  • 业务副本可迁移,但 PVC 受单节点访问模式约束:Pod 可能卡在卷挂载阶段。

因此,生产维护通常按小批次执行:先 cordon 一个节点,观察 drain、PDB、调度和业务指标,确认恢复后再处理下一个节点。批次大小不应只由节点数量决定,还要由业务可用副本、拓扑约束、存储能力和故障预算共同决定。


十三、把节点维护看成一个可验证的状态机

一个完整的状态转换可以表示为:

stateDiagram-v2
    [*] --> ReadySchedulable
    ReadySchedulable --> Cordoned: cordon
    Cordoned --> Draining: drain / eviction
    Draining --> Drained: 业务 Pod 已处理\nDaemonSet 等例外已确认
    Draining --> Blocked: PDB/容量/孤立 Pod/超时
    Blocked --> Draining: 修复约束或调整维护批次
    Drained --> Maintained: 主机与节点组件维护
    Maintained --> Recovering: kubelet/CNI/CSI 恢复
    Recovering --> ReadyUnschedulable: Ready=True 且压力正常
    ReadyUnschedulable --> ReadySchedulable: uncordon
    ReadyUnschedulable --> Recovering: Ready/Lease/污点检查失败

每个状态都应有可观察条件:

  • ReadySchedulableReady=True,无不应存在的维护污点,unschedulable 未设置;
  • Cordonedunschedulable=true,但现有 Pod 仍可运行;
  • Draining:Pod 出现删除时间戳,控制器在其他节点创建替代 Pod;
  • Drained:业务 Pod 已离开,DaemonSet 等保留对象有明确解释;
  • Maintained:主机操作完成,但不代表 Kubernetes 已恢复;
  • ReadyUnschedulable:节点健康,但仍禁止调度,等待最终验证;
  • ReadySchedulable:节点健康且恢复调度资格。

这个状态机的价值在于:任何命令失败,都能定位是在“阻止新调度”“驱逐准入”“新 Pod 调度”“Pod 终止”“节点恢复”还是“业务验证”阶段,而不是笼统地认为“drain 有问题”。


系列导航与关联阅读

官方资料

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