Kubernetes节点运维:封锁、驱逐、污点与节点状态管理
节点(Node)运维 是集群日常维护中最高频的一类操作。本页讲的是 kubectl 视角下的节点管理:查看节点状态、把节点从调度中摘除、安全地驱逐其上的 Pod,以及节点 NotReady 时按什么顺序排查。
节点在 Kubernetes 中是一个 API 对象(v1/Node),由所在机器上的 kubelet 注册并持续上报心跳。控制平面看到的「节点」始终是 kubelet 的一面镜子 —— 机器活着但 kubelet 挂了,节点照样是 NotReady。
三个高频动作的关系:
| 动作 | 作用 | 对已运行 Pod 的影响 |
|---|---|---|
cordon |
标记节点不可调度 | 无,已有 Pod 继续运行 |
drain |
先 cordon,再逐出节点上的 Pod | 有,Pod 被驱逐到别处重建 |
uncordon |
解除不可调度标记 | 无,新 Pod 可重新调度上来 |
cordon 本质上就是给节点打上 node.kubernetes.io/unschedulable:NoSchedule 污点,所以「封锁」和「污点」是同一套机制的两种用法。
kubectl cordon <node>
kubectl uncordon <node>
kubectl drain <node> --ignore-daemonsets --delete-emptydir-data
kubectl taint nodes <node> <key>=<value>:<effect>
# 概览:STATUS 列反映节点是否健康
kubectl get nodes
kubectl get nodes -o wide
# 带标签查看(定位节点池、可用区、机型)
kubectl get nodes --show-labels
kubectl get nodes -l node-role.kubernetes.io/control-plane=
kubectl get nodes -L topology.kubernetes.io/zone
# 单个节点的完整信息:地址、容量、可分配资源、Conditions、Taints、已分配资源
kubectl describe node <node-name>
# 只看 Conditions
kubectl get node <node-name> -o jsonpath='{.status.conditions[*].type}{"\n"}'
kubectl get node <node-name> -o json | jq '.status.conditions'
# 节点上的 Pod:按 spec.nodeName 过滤,而不是按命名空间
kubectl get pods -A --field-selector spec.nodeName=<node-name>
kubectl get pods -A --field-selector spec.nodeName=<node-name>,status.phase=Running
# 节点资源用量(需要 metrics-server)
kubectl top node
kubectl top node --sort-by=memory
kubelet 与节点控制器会持续维护一组 Conditions,kubectl describe node 里的 Conditions 段就是它们。任一 Condition 异常,节点都可能被判定为不适合继续承载工作负载:
| Condition | 含义 | 异常后的动作 |
|---|---|---|
Ready |
True 表示 kubelet 健康且可接收 Pod |
打 not-ready/unreachable 污点 |
MemoryPressure |
节点内存不足 | 打 memory-pressure:NoSchedule 污点并驱逐 Pod |
DiskPressure |
节点文件系统或镜像分区空间/inode 不足 | 打 disk-pressure:NoSchedule 污点并驱逐 Pod |
PIDPressure |
可用 PID 不足 | 打 pid-pressure:NoSchedule 污点并驱逐 Pod |
NetworkUnavailable |
节点网络未就绪 | 通常表示 CNI 插件未安装或未就绪 |
注意 NetworkUnavailable 不由 kubelet 维护,而是由 CNI 网络插件写入。全新集群里节点几乎都显示 NetworkUnavailable=True,装好网络插件才会转为 False。
# 快速判断节点为何 NotReady
kubectl get node <node-name> -o jsonpath='{range .status.conditions[*]}{.type}{"\t"}{.status}{"\t"}{.reason}{"\n"}{end}'
# 查看节点上的污点(kubelet 施加的压力污点也会出现在这里)
kubectl describe node <node-name> | grep -A5 Taints
kubectl get node <node-name> -o jsonpath='{.spec.taints}'
# 封锁:标记不可调度,已运行的 Pod 不受影响
kubectl cordon <node-name>
# 解除封锁
kubectl uncordon <node-name>
# 驱逐:先自动 cordon,再逐个安全驱逐 Pod(默认走 Eviction API)
kubectl drain <node-name> --ignore-daemonsets --delete-emptydir-data
# 只驱逐匹配标签的 Pod
kubectl drain <node-name> --ignore-daemonsets --pod-selector='app=nginx'
# 先看会发生什么,再决定是否执行
kubectl drain <node-name> --ignore-daemonsets --delete-emptydir-data --dry-run=server
常用标志:
--ignore-daemonsets 忽略 DaemonSet 管理的 Pod(drain 不会删除它们)
--delete-emptydir-data 允许驱逐使用 emptyDir 的 Pod(本地数据会丢失)
--force 允许驱逐没有控制器管理的「裸 Pod」
--grace-period=-1 优雅终止时间,负数表示沿用 Pod 自身设置
--timeout=0 整体超时,0 表示不限制
--disable-eviction 改用 DELETE 直接删除,绕过 PodDisruptionBudget(慎用)
--dry-run=server 只走服务端校验,不真正驱逐
--pod-selector 只驱逐匹配标签的 Pod
--skip-wait-for-delete-timeout 等待删除超过 N 秒后跳过
--chunk-size=500 分批返回列表,避免大集群下一次拉取过多
污点(Taint)打在节点上,容忍(Toleration)写在 Pod 上,二者匹配才能调度上去。
# 添加污点
kubectl taint nodes <node-name> dedicated=gpu:NoSchedule
kubectl taint nodes <node-name> dedicated=gpu:NoExecute
# 删除污点:键后面加一个减号
kubectl taint nodes <node-name> dedicated=gpu:NoSchedule-
kubectl taint nodes <node-name> dedicated-
# 修改已有污点必须加 --overwrite
kubectl taint nodes <node-name> dedicated=cpu:NoSchedule --overwrite
# 查看
kubectl get node <node-name> -o jsonpath='{.spec.taints}'
三种 effect 的行为差异:
| effect | 对已运行 Pod | 对新调度 Pod |
|---|---|---|
NoSchedule |
不驱逐 | 不容忍则不调度 |
PreferNoSchedule |
不驱逐 | 尽量不调度(软约束) |
NoExecute |
不容忍则立即驱逐 | 不容忍则不调度 |
对应的容忍写法:
tolerations:
- key: "dedicated"
operator: "Equal"
value: "gpu"
effect: "NoSchedule"
- key: "dedicated"
operator: "Exists"
effect: "NoExecute"
tolerationSeconds: 3600
这些污点由控制平面或 kubelet 自动维护,排查调度问题时优先看它们:
| 污点 | effect | 来源 |
|---|---|---|
node.kubernetes.io/not-ready |
NoExecute |
节点控制器,节点 NotReady 时 |
node.kubernetes.io/unreachable |
NoExecute |
节点控制器,无法与节点通信时 |
node.kubernetes.io/memory-pressure |
NoSchedule |
kubelet,内存压力 |
node.kubernetes.io/disk-pressure |
NoSchedule |
kubelet,磁盘压力 |
node.kubernetes.io/pid-pressure |
NoSchedule |
kubelet,PID 压力 |
node.kubernetes.io/network-unavailable |
NoSchedule |
网络插件,网络未就绪 |
node.kubernetes.io/unschedulable |
NoSchedule |
kubectl cordon |
node.kubernetes.io/out-of-service |
NoExecute |
管理员手动添加,用于节点非体面关机 |
普通 Pod 会被自动注入 not-ready 与 unreachable 的容忍,tolerationSeconds 默认为 300 秒 —— 这正是「节点失联 5 分钟后 Pod 才开始被驱逐」的原因。
标准的「下线一台机器做维护」流程:
# 1. 确认节点上没有不能被中断的工作负载
kubectl get pods -A --field-selector spec.nodeName=<node-name>
# 2. 封锁并驱逐(drain 会自动先 cordon)
kubectl drain <node-name> --ignore-daemonsets --delete-emptydir-data
# 3. 确认 Pod 已迁走,此时通常只剩 DaemonSet 的 Pod
kubectl get pods -A --field-selector spec.nodeName=<node-name>
# 4. 执行机器维护(重启、换内核、扩容磁盘等)
# 5. 恢复调度
kubectl uncordon <node-name>
kubectl get nodes
若机器维护后需要重新加入集群,务必确认 kubelet 已启动、容器运行时正常、CNI 配置仍然存在。
按下面的顺序逐层收敛,不要一上来就重启 kubelet:
# 1. 节点自身的 Conditions
kubectl describe node <node-name> | grep -A20 Conditions
kubectl get node <node-name> -o jsonpath='{.status.conditions[?(@.type=="Ready")]}'
# 2. 登录节点看 kubelet 日志(最直接的线索)
sudo journalctl -u kubelet -n 100 --no-pager
sudo journalctl -u kubelet -p err --since "10 minutes ago"
# 3. 容器运行时是否可用
sudo systemctl status containerd
sudo crictl info
# 4. kubelet 是否在正常续约
kubectl get lease -n kube-node-lease | grep <node-name>
# 5. 磁盘是否写满(最常见的隐性原因)
df -h /var/lib/kubelet /var/lib/containerd
# 6. 证书是否过期
sudo openssl x509 -in /var/lib/kubelet/pki/kubelet-client-current.pem -noout -dates
# 7. 是否存在资源压力
kubectl describe node <node-name> | grep -i pressure
常见根因排序:kubelet 进程挂掉 > 容器运行时挂掉 > 磁盘写满触发 DiskPressure > 客户端证书过期 > 网络分区导致心跳中断 > CNI 异常。
cordon 不影响已经在运行的 Pod。它只阻止新 Pod 调度过来,已有 Pod 照常提供服务,也不会有任何通知。drain 默认不驱逐 DaemonSet 的 Pod,不加 --ignore-daemonsets 会直接报错退出;而且即使加了这个标志,DaemonSet 的 Pod 也不会被删除 —— 删了也会被 DaemonSet 控制器立刻在同一个节点上重建。drain 遇到使用 emptyDir 的 Pod 会中止,必须显式加 --delete-emptydir-data。该标志的旧名字是 --delete-local-data,老脚本里见到不要当成另一个参数。drain 会尊重 PodDisruptionBudget,可能永久阻塞。PDB 的 minAvailable / maxUnavailable 不允许再少一个副本时,驱逐请求会一直返回 429,drain 就一直卡住不返回。此时应先用 kubectl get pdb -A 检查,而不是加 --disable-eviction 硬来 —— 后者绕过 PDB,会把可用副本直接打到水位以下。--force 才能驱逐。这类 Pod 删掉后不会被重建,drain 默认拒绝执行以免误删。node.kubernetes.io/out-of-service 只能在确认机器已关机或断电后添加。机器还活着时打这个污点,可能出现同一个 Pod 在两个节点上同时运行,导致文件系统损坏;恢复后必须手动移除该污点。not-ready/unreachable 容忍的 tolerationSeconds 决定。调小这个值能加快故障转移,但也会让网络抖动的节点被误判。MemoryPressure/DiskPressure/PIDPressure 为 True 时 kubelet 会主动驱逐 Pod,而且不遵守 PodDisruptionBudget、也不遵守 terminationGracePeriodSeconds;使用硬驱逐阈值时宽限期固定为 0 秒,Pod 会被立即杀掉。uncordon 不会把 Pod 迁回来。被驱逐的 Pod 已由各自的控制器在别处重建,恢复调度只影响之后新建的 Pod。kubectl delete node 只是把对象从 API 中移除;如果机器上的 kubelet 仍在运行且能连上 apiserver,它会重新注册该节点。kubectl — Kubernetes集群管理工具kubelet — 节点代理,汇报节点状态并管理 Podkubeadm — Kubernetes集群安装工具crictl — 容器运行时调试工具etcd — 集群数据存储,Node 对象最终落在这里