凌晨两点,手机在床头柜上震动个不停。我眯着眼点开群消息,值班同事在群里发了一张截图:生产环境某个节点状态变成了NotReady,一堆业务 Pod 卡在了Pending或者ContainerCreating。这是我这几年来最熟悉的一幕,几乎每个玩过自建 Kubernetes 集群的人,都至少遭遇过一次节点假死、被调度器“抛弃”的情况。紧接着另一个同事补了一条命令输出:kubectl describe node里出现了node.kubernetes.io/not-ready:NoSchedule的 taint。
这种 taint 一出现,很多刚接触 K8s 的同学第一反应就是“删掉它”。但实际情况远比表面上复杂:taint 只是系统的一种自我保护标记,真正的问题藏在 kubelet 心跳、容器运行时、网络插件甚至磁盘空间里。如果只删 taint 不治根因,过几分钟它还会自动冒出来,节点继续在 NotReady 和 Ready 之间反复横跳,业务 Pod 跟着遭殃。
这篇文章我想把“K8S 节点 taint-NotReady 问题”这件事完整拆开,从 taint 的产生机制、排查定位、临时缓解、根因治理到常见坑位一次讲清楚。无论你是刚接手集群的运维新人,还是已经踩过几次坑的 SRE,文中提到的命令、流程和思路都可以直接拿去用。
1. 先把机制说透:taint 与 NotReady 到底什么关系
1.1 taint、toleration 与节点状态之间的三角关系
很多人会把 taint 和节点状态混为一谈,实际上它们是两套独立却又经常同时出现的机制。节点状态(Ready、NotReady、Unknown)表示的是 kubelet 与 API Server 之间的健康同步情况;而 taint 是节点上的一个“污点标记”,调度器看到这个标记之后,默认不会把 Pod 调度到该节点上。
这两者的联动逻辑是这样的:API Server 检测到某个节点的心跳超过阈值(默认node-monitor-grace-period是 40 秒),就会把节点状态置为NotReady,同时由节点生命周期控制器自动给节点打上node.kubernetes.io/not-ready:NoExecute或NoSchedule的 taint。也就是说,taint 是节点异常之后的“善后动作”,是结果而不是原因。
这里有一个关键点:NoExecute的 taint 会触发节点上已有 Pod 的驱逐。Kubernetes 默认给每个 Pod 注入了对not-ready和unreachable两个 taint 的容忍,tolerations默认tolerationSeconds是 300 秒。所以当节点失联 5 分钟后,存量 Pod 才会被陆续驱逐,这也是为什么节点 NotReady 之后业务不会瞬间全部挂掉,而是会先“挣扎”几分钟。
1.2 K8s 为什么一定要用 taint 这种设计
用一个生活化的例子来类比:节点就像一线岗位上的员工,API Server 是后勤调度中心。员工突然联系不上了,中心不会第一时间把人开除,而是先挂上一个“该员工失联,不要再给他派新任务”的标记,同时给这个员工手头的旧任务一个宽限期,等确认他回不来之后再逐步把任务转给别人。
那个“标记”就是 taint。K8s 用这个机制避免了两件可怕的事情:一是调度器继续向失联节点派发新 Pod,导致大量 Pod 永久卡在Pending;二是存量 Pod 无限期停留在失联节点上,服务迟迟不恢复。这套“标记 + 容忍 + 宽限期”的设计,本质上是分布式系统里经典的故障隔离思路,它宁可牺牲少量 Pod,也要保证集群整体的调度可控。
所以,以后看到 taint 先不要急着骂它,它是帮我们拦住更大灾难的“安全带”。真正要解决的是节点为什么会失联,而不是删掉安全带继续裸奔。
2. 五分钟定位:先分清“表象”和“病因”
2.1 三步快速确认是不是 taint 在作怪
当群里有人喊“节点 NotReady 了”,我一般按下面的固定动作来排查,每一步都讲究顺序,避免凭感觉操作。
第一步,看全局状态:
kubectl get nodes -o wide这一步确认是单个节点异常还是多个节点同时异常。如果是多个节点同时 NotReady,大概率是网络分区、API Server 负载过高或机房级别的故障,问题定位方向完全不同。
第二步,查看目标节点的 taint 和条件状态:
kubectl describe node <节点名> | sed -n '/Taints/,/Unschedulable/p' kubectl get node <节点名> -o jsonpath='{.status.conditions}'这一步能同时拿到两个信息:节点被打上了哪些 taint,以及当前各个Condition的真实状态。我见过有人只看 taint 就断定“节点磁盘满了”,结果实际是 kubelet 挂了,属于典型的被表象带偏。
第三步,查节点相关事件和 kubelet 日志:
kubectl get events --field-selector involvedObject.name=<节点名> -n default journalctl -u kubelet -f --since "10 minutes ago"注意,kubectl get events只能看到 API Server 侧的记录,kubelet 侧的真实报错还要靠journalctl去看。这一步才是揪出病根的关键所在。
2.2 别把 NotReady 全算在 taint 头上
为了让大家直观理解各种“病根”和“表象”的对应关系,我整理了一张我做运维时反复对照的速查表,实测很管用:
| 表象 | 常见 taint | 可能根因 | 优先排查位置 |
|---|---|---|---|
节点NotReady | not-ready/unreachable | kubelet 异常、心跳中断 | journalctl -u kubelet |
节点Ready但 Pod 不调度 | 手误或自定义 taint | 节点被打上 NoSchedule | describe node 的 Taints 字段 |
| 节点磁盘压力 | node.kubernetes.io/disk-pressure | 容器目录或系统盘占用过高 | df -h / du -sh /var/lib/containerd |
| 节点内存压力 | node.kubernetes.io/memory-pressure | 宿主机内存不足 | free -m / top |
| 网络不可用 | node.kubernetes.io/network-unavailable | CNI 插件异常 | kubectl get pods -n kube-system、查看 flannel/calico 日志 |
大多数情况下,not-ready这个 taint 是节点异常的结果,而disk-pressure、network-unavailable这类的 taint,更接近“直接的病根描述”。排查的时候要用这个表倒推,不要看到NotReady就直奔“删 taint”而去。
3. 说人话的解决方案:从临时解除到安全恢复
3.1 临时移除 taint 的正确姿势
有时候业务比较重要,等不及慢慢排查,需要先把 Pod 调度恢复起来。这时候你会想要临时移除节点上的not-readytaint,让它重新接收 Pod。我的建议是,移除可以,但必须记录现场,不能无脑删。
先看当前节点上所有 taint 的原值:
kubectl get node <节点名> -o jsonpath='{.spec.taints}'把输出复制保存到本地或粘贴到值班记录里,这是一个非常容易忽略的步骤。因为你删掉的 taint,很可能在节点恢复后需要原样加回来,或者后续排查时需要对比确认系统到底加了什么。
确认记录之后,移除某个特定 taint:
kubectl taint nodes <节点名> node.kubernetes.io/not-ready:NoSchedule-注意末尾的减号表示删除某种 taint,而不是整个节点的所有 taint。如果你希望移除所有带NoExecute效果的not-ready容忍,命令要按实际字段精确匹配,宁可多敲几次 describe 也不要图省事用通配符把control-plane的 taint 也顺手删了。
这里还要说一个原则:临时恢复调度和真正修复节点是两件事。移除 taint 之后,节点依然可能处于底层异常状态,kubelet 如果还在重启,过不了几分钟控制器会把 taint 重新打上。所以临时移除只适合“先让部分业务喘口气”的场景,后续必须接着查根因。
3.2 d滴滴还是直接 删?先搞懂驱逐和调度的顺序
我在实际运维中看到最多翻车的操作,是节点一 NotReady 就有人直接kubectl delete node,或者直接删 taint,结果 Pod 散落一地,数据卷还卸载不干净。正确的处理顺序应该是:
- 先
cordon节点,确保新 Pod 不会调度上去。 - 再根据情况决定是否
drain,把存量 Pod 安全驱逐到其他节点。 - 修复节点底层问题(kubelet、磁盘、网络等)。
- 节点恢复
Ready后,uncordon恢复调度。 - 最后再根据现场记录,决定是否把之前删掉的
not-readytaint 恢复回去。
drain 的时候建议带上这些参数:
kubectl drain <节点名> --ignore-daemonsets --delete-emptydir-data --force--ignore-daemonsets是必须的,否则 DaemonSet 的 Pod 永远没法驱逐干净;--delete-emptydir-data要确认业务可以接受 emptyDir 数据丢失再使用;--force适合有 ReplicaSet / Deployment 托管的 Pod,但不要对裸 Pod 滥用。
为什么不能跳过 drain 直接 delete pod?因为正常驱逐流程会走 PreStop hook 和 terminationGracePeriodSeconds,给业务优雅下线的时间。直接删除 Pod 和不给业务“整理桌面”就断电是一样的道理,这在有状态服务上尤其致命。
3.3 自动化清理 taint 的脚本思路
如果是自建集群,节点数量多的时候,光靠人工敲命令很容易漏。我写过一个简单的运维巡检脚本,思路分享给大家。它做的事情很简单:定期检测 NotReady 节点,如果该节点上所有 Pod 都可以被驱逐,就把not-ready和unreachable两个 taint 暂时移除,并在本地记录原始 taint,等节点恢复 Ready 后再补回去。
核心逻辑大致是这样的伪代码:
# 巡检脚本核心逻辑(伪代码) for node in $(kubectl get nodes | awk '$2=="NotReady"{print $1}'); do # 记录原始 taint kubectl get node $node -o jsonpath='{.spec.taints}' > /tmp/taint_$node.json # 移除 not-ready 和 unreachable taint kubectl taint nodes $node node.kubernetes.io/not-ready:NoSchedule- || true kubectl taint nodes $node node.kubernetes.io/unreachable:NoExecute- || true # 等待节点恢复 # 状态恢复 Ready 后,根据原始记录选择性恢复 taint done这里有几个要注意的坑:脚本必须做成“单点执行 + 并发限制”,防止多个节点同时被恢复调度导致 Pod 雪崩调度到同一个剩余节点上;另外恢复 taint 时不要生硬地把所有原值都加回,如果原始 taint 是control-plane这种有特殊作用的,需要额外判断。更稳妥的做法是用一个带状态管理的 Operator 或巡检平台,而不是裸脚本,但裸脚本作为应急手段完全够用。
4. 追根因:节点为什么反复 NotReady
4.1 从 taint 反推事件链
not-readytaint 出现之后,第一件事千万不要只是删,而是看它复发的频率。如果删掉之后 10 分钟又冒出来,说明 kubelet 与 API Server 之间仍然无法正常心跳。K8s 的节点心跳默认由 kubelet 发起,每 10 秒上报一次(--node-status-update-frequency),超过 40 秒未更新状态就会被判定为 NotReady。
这时候我会做一条完整的事件链回溯:
- 节点状态转为
NotReady的时间点; - 该时间点前后,kubelet 日志里有没有
Failed to update node status、connection refused、context deadline exceeded之类的关键字; - 该时间点前后,宿主机上有没有发生内核 OOM、磁盘 I/O 飙升、网络栈异常;
- 该时间点前后,kube-system 里的网络插件有没有重启。
用这条事件链,几乎都能把“谁先病、谁后病”梳理清楚。我见过最典型的场景是:底层磁盘先写满,容器运行时 hang 住,kubelet 连带失联,API Server 才打上 not-ready taint。这时候你只删 taint,等于给病人吃退烧药却不消炎,体温降了但感染还在。
4.2 高频根因逐个过:磁盘、运行时、网络、资源
我复盘过大量 NotReady 案例,发现根因基本集中在四个方向,按出现频率大概如下:
磁盘空间问题是第一名。节点上/var/lib/containerd或/var/lib/docker写满,或者根分区满了,会导致容器运行时无法创建临时文件、kubelet 写不了状态文件,继而整个节点陷入假死。排查时先跑df -h,再重点看docker system df或crictl stats,不要只看系统盘。
容器运行时异常是第二名。containerd或dockerd进程挂掉、卡死,或者与 kubelet 之间的 CRI 通信超时,都会让 kubelet 误判节点不可用。这种时候journalctl -u kubelet里通常能看到ListPodSandbox failed或grpc timeout之类的报错,重启运行时进程往往能快速缓解。
网络插件问题算是第三名。flannel、calico 这类 CNI 插件一旦出现 Pod 重启、etcd 失联、BGP 会话中断,节点状态可能会显示NotReady。这种情况单纯看节点 taint 是发现不了的,要把目光放到kube-system命名空间里对应 Pod 的日志上。
资源不足问题排在第四但也很常见。宿主机内存耗尽触发内核 OOM,或者节点上NodeAllocatable被系统预留资源挤占,会导致 Pod 因memory-pressure被驱逐,节点进入异常状态。连续多次 OOM 之后 kubelet 会变得极其不稳定,这种场景要先解决宿主机资源层面问题,再谈恢复调度。
4.3 预防措施:把“救火”变成“防火”
回到开头的场景,那次我们最终花了 3 个小时处理完节点故障,第二天复盘时发现,如果提前做了下面几件事,故障影响范围可以缩小一半以上。
监控层面,必须对每个节点做四类指标采集:磁盘使用率、内存使用率、kubelet 进程状态、容器运行时健康状态。其中磁盘和内存用 node_exporter 就能覆盖,kubelet 和运行时的健康检查建议用独立的探针脚本,不要只依赖 K8s 自带的节点状态。
预案层面,建议在团队内部固化“节点 NotReady 处理手册”。手册中明确规定什么场景可以临时删 taint、什么场景必须强制 drain、什么场景要联系底层基础设施负责人。没有预案的团队容易在故障时发挥出“三个和尚没水吃”的混乱效果。
业务层面,合理配置 PodDisruptionBudget(PDB)和拓扑分布约束,让核心服务至少有多个副本分散在不同节点上。这样即使单个节点 NotReady,业务也只是短暂抖动而不是整体不可用。PDB 还能在 drain 节点时保护重要服务不被一次性全部驱逐。
5. 实战答疑:我踩过的坑与排查速查表
5.1 高频问题速查表
为了方便大家直接“抄作业”,我把这些年遇到的典型问题整理成一个速查表,症状、原因、解决办法一目了然:
| 问题现象 | 可能原因 | 最直接的解决办法 |
|---|---|---|
节点 NotReady,且一直有not-readytaint | kubelet 与 API Server 心跳中断 | 先检查 kubelet 日志、磁盘、运行时,逐项恢复后再移除 taint |
| 删掉 taint 后几分钟又冒出来 | 根因未修复,控制器重新标记 | 不要反复删 taint,转到根因排查流程 |
节点Ready,但业务 Pod 一直 Pending | 节点存在 NoSchedule taint 或未设置 toleration | 查看 taint 来源,区分是手误还是策略需要 |
| drain 节点时卡住,Pod 一直 Terminating | PDB 限制、Pod 强中断、emptyDir 数据量大 | 检查 PDB,确认后按需添加--force和超时参数 |
| 删除 taint 后误伤 master 节点 | 手误删除了control-planetaint | 参照官方默认值重新打上:node-role.kubernetes.io/control-plane:NoSchedule |
节点内存不足触发memory-pressure | 宿主机内存耗尽或 Cgroup 限制异常 | 扩容内存、清理大内存 Pod,必要时重启 kubelet |
这张表的核心思想是:每个表象背后都要问三层为什么。第一层是“什么标记出现了”,第二层是“为什么这个标记会出现”,第三层是“怎么让系统重新自愈”。做完三层追问,基本就不会在同一个坑里栽两次。
5.2 几个容易翻车的操作细节
第一个容易被忽略的细节是,kubectl taint nodes <node> key:effect-只能删掉完全匹配的 taint。如果你本来想删的是NoSchedule,实际节点上的是NoExecute,这条命令会静默失败或者报错。建议每次删除之前都用describe node看一遍完整字段。
第二个坑是,不要在 master/control-plane 节点上随手执行“移除所有 taint”的操作。控制面节点的control-planetaint 是为了保证系统组件不跟业务 Pod 争抢资源,删掉它会带来无法预料的调度风险。我在测试环境里干过这事,结果一批业务 Pod 直接调度到了 master 上,kube-system 组件的响应速度肉眼可见地变慢。
第三个坑是,节点异常后业务 Pod 处于Terminating状态时,不要过度使用强制删除。尤其是有状态应用挂载了云盘或本地卷,强制删除后可能留下孤儿卷。比较稳妥的做法是,确认底层存储已经完全释放后再走kubectl delete pod --force --grace-period=0,并且要在业务低峰期操作。
第四个细节跟恢复顺序有关。节点恢复Ready之后,先把 kubelet 状态稳定观察一段时间,再恢复调度。我遇到过节点刚恢复就立刻 uncordon,结果 kubelet 还在初始化,新调度的 Pod 又全部失败,节点再次被打回 NotReady。等 5 分钟再恢复,整个集群会平稳很多。
5.3 从故障中总结的几点个人体会
做 K8s 运维这两年,我越来越觉得节点 taint 这件事像是一个“报警器”而不是“故障本身”。很多人看到not-ready就焦虑,第一反应是赶紧把这个标记摘除掉,让节点看起来正常。但报警器响了,摘掉报警器并不能阻止火势蔓延,只有找到火源才能真正解除警报。
如果让我给刚入门的朋友一个实操建议,那就是:在你的集群里提前演练一次“节点故障演练”。挑一个业务低峰期,手动停掉某个节点的 kubelet 或容器运行时,然后完整跑一遍“观察状态 -> 查看事件 -> 处理根因 -> 恢复调度”的流程。演练过一次之后,真实故障来临时你至少不会手忙脚乱,也更容易在五分钟内判断出到底是不是 taint 在作怪、背后是什么原因。
最后再分享一个小技巧:把kubectl get nodes -o wide、kubectl describe node、journalctl -u kubelet这三条命令的执行结果,做成一个一键脚本存到团队工具库。每次节点出现异常,先跑这脚本,把输出贴到故障群里,再开始讨论怎么处理。信息同步这一步做好了,后面排查效率至少提升一半。