Skip to content

节点 NotReady:先分清是 False 还是 Unknown ​

新加的节点一直 NotReady,kubectl describe node 里写着 cni plugin not initialized;另一个节点半夜变成 NotReady,状态却是 Unknown,原因只有一句 Kubelet stopped posting node status.。两者在 kubectl get nodes 里显示得一模一样,查的方向完全不同。

节点的 Ready 条件有两个写入方:kubelet 定期上报自己的状态,写的是 True 或 False,并附上原因;kubelet 长时间没有心跳时,控制面的节点控制器替它写成 Unknown。所以排查 NotReady 的第一步不是看日志,而是看这个条件的值:False 说明 kubelet 还活着,并且告诉了你它为什么认为自己没准备好;Unknown 说明控制面已经听不到这个节点了。

本文用 kind 搭建一个 1 个控制面、2 个工作节点的 Kubernetes 1.36.4 集群,关闭默认的 CNI,手工完成节点就绪的每一步,再停掉一个节点的 kubelet,观察控制面与节点各自看到什么。实验见文末。

一、一个节点是怎样就绪的 ​

systemd拉起 containerd、kubeletkubelet 读静态 Pod 目录/etc/kubernetes/manifestsetcd、apiserver 等宿主机网络,不需要 CNIkubelet 注册节点上报条件与心跳容器运行时加载 CNI 配置/etc/cni/net.d + /opt/cni/bin节点 Readynot-ready 污点被移除调度器按污点与资源选节点创建 Pod 沙箱CNI 插件分配 IP、接好网卡跨节点 Pod 路由CNI 的职责,Ready 不检查没有 CNI 配置:节点 False KubeletNotReady(cni plugin not initialized),普通 Pod 被 not-ready 污点挡在调度阶段容忍污点的 Pod 能被调度上去,但卡在 ContainerCreating(NetworkNotReady)有配置、缺跨节点路由:节点全部 Ready,跨节点请求超时,DNS 解析失败
图 1 · 控制面组件是 kubelet 从本地目录拉起的静态 Pod,用的是宿主机网络,不需要 CNI;节点的 Ready 由 kubelet 上报,其中网络一项只看容器运行时能否加载 CNI 配置。跨节点的 Pod 路由由 CNI 负责,不在 Ready 的判断里——节点全部 Ready,跨节点的 Pod 仍然可能不通

用 kubeadm 搭建的集群(kind 也是)里,节点的启动链是:

  1. systemd 拉起 containerd 和 kubelet;
  2. kubelet 读取本地的静态 Pod 目录 /etc/kubernetes/manifests,里面是 etcd、kube-apiserver、kube-controller-manager、kube-scheduler 四个清单。kubelet 直接把它们跑起来,再在 API 里为它们创建镜像 Pod,kubernetes.io/config.source 注解是 file;
  3. kubelet 向 apiserver 注册节点,定期上报节点条件,并续约 kube-node-lease 里的 Lease 作为心跳;
  4. 容器运行时从 /etc/cni/net.d 读取 CNI 配置,调用 /opt/cni/bin 里的插件。配置加载成功,运行时报告网络就绪,kubelet 才把节点报为 Ready;
  5. 节点就绪后,node.kubernetes.io/not-ready 污点被移除,调度器开始往上面放普通 Pod。

控制面组件使用宿主机网络,不依赖 CNI。所以一个没有 CNI 的集群,控制面照样能工作,kubectl 也能用,只是节点永远不会就绪。

二、没有 CNI:节点 False,Pod 卡在两个不同的地方 ​

集群刚创建、还没有 CNI 配置时,工作节点的 /etc/cni/net.d 是空的:

  • 节点条件:False,原因 KubeletNotReady,消息里是 container runtime network not ready ... cni plugin not initialized;
  • 节点带 node.kubernetes.io/not-ready:NoSchedule 污点;
  • kube-system 里,etcd、apiserver 等静态 Pod 与每个节点上的 kube-proxy 都是 Running——它们用宿主机网络;CoreDNS 是 Pending。

一个普通 Pod 停在 Pending,事件是 FailedScheduling: 0/3 nodes are available: 3 node(s) had untolerated taint(s),问题在调度阶段。

另一个 Pod 容忍了 not-ready 污点,被放到了工作节点上,却停在 ContainerCreating,事件是 NetworkNotReady ... cni plugin not initialized:它通过了调度,卡在 kubelet 创建 Pod 沙箱这一步,因为没有 CNI 就无法给它分配 IP、接好网卡。

同样是 Pod 起不来,看 Pod 有没有被分配到节点(NODE 列),就能区分这两层。

三、Ready 只说明配置存在,不说明网络通了 ​

kind 节点镜像里有 ptp、host-local、loopback、portmap 四个 CNI 插件。给控制面节点写一份最小的配置:ptp 为每个 Pod 创建一对 veth,host-local 从节点的 Pod 网段(spec.podCIDR)里分配 IP。

  • 写入后 0.3 秒,控制面节点变为 Ready,两个工作节点仍是 False KubeletNotReady:就绪是按节点判断的,每个节点都要有自己的 CNI 配置——这也是 CNI 通常以 DaemonSet 部署的原因;
  • CoreDNS 被调度到控制面节点上并运行。

再给两个工作节点写同样的配置,三个节点全部 Ready,之前那个普通 Pod 被调度到工作节点上,变成 Running。看起来一切正常,但从这个 Pod 访问控制面节点上的 CoreDNS:

  • 直连 CoreDNS 的 Pod IP:超时;
  • 解析 kubernetes.default:connection timed out; no servers could be reached。

工作节点的路由表里只有本节点 Pod 的路由,没有其他节点 Pod 网段的路由,发往 10.244.0.0/24 的包走默认路由出去就没有下文了。在每个节点上为其他节点的 Pod 网段加一条路由(下一跳是那个节点的 IP)之后,直连返回 OK,DNS 解析出 10.96.0.1。

真实的 CNI 插件做的就是这几件事:在每个节点写配置、放插件,给 Pod 分配 IP,再用路由、隧道或 BGP 让不同节点的 Pod 网段互通。节点的 Ready 只检查了第一件。所以「节点都 Ready,但部分 Pod 之间不通、DNS 时好时坏」这类问题,要去看 CNI 自己的 Pod 和日志,并用跨节点的 Pod IP 直接测试,而不是看节点状态。

四、kubelet 停止:Unknown、驱逐,以及仍在运行的旧容器 ​

在工作节点上执行 systemctl stop kubelet。节点上的 containerd 和已经运行的容器不受影响,只是不再有人向控制面报告了。一个单副本的 Deployment 正运行在这个节点上,它对 unreachable、not-ready 污点的容忍时间设为 20 秒。

0s15s30s45s60s75s90s节点 Ready(心跳已停,控制面还不知道)Unknown + unreachable 污点旧 Pod RunningAPI 中 Terminating旧容器一直在原节点上运行,并响应请求新 Pod 在另一节点运行停止 kubelet从 71.7 秒到 kubelet 恢复:同一个副本有两个实例在处理请求
图 2 · 停掉工作节点的 kubelet:约 51.5 秒后节点变为 Unknown 并加上 unreachable 污点;实验把容忍时间设为 20 秒,71.7 秒时旧 Pod 被标记删除,73.1 秒时新 Pod 在另一个节点上运行。旧 Pod 在 API 里停在 Terminating,它的容器仍在原节点上运行并响应请求,直到 kubelet 恢复。默认容忍 300 秒时,驱逐要等到约 6 分钟
  • 停止后 51.5 秒,节点状态变为 Unknown,原因 NodeStatusUnknown: Kubelet stopped posting node status.。节点控制器的 --node-monitor-grace-period 默认 50 秒:超过这段时间没有心跳,节点才被判定为失联;
  • 节点被加上 node.kubernetes.io/unreachable 的 NoSchedule 和 NoExecute 两个污点;
  • 71.7 秒时,容忍时间到,旧 Pod 被标记删除;73.1 秒时,新 Pod 在另一个工作节点上运行;
  • 这时 API 里有两个 Pod:一个 Running,一个一直停在 Terminating。旧 Pod 的容器还在原节点上运行,从控制面节点访问旧 Pod 的 IP,它照样返回自己的主机名。只有 kubelet 才能真正停掉容器,而 kubelet 不在;
  • 恢复 kubelet 后 7.9 秒,kubelet 发现这个 Pod 已被删除,停掉容器,旧 Pod 从 API 消失。

这段时间里,同一个副本有两个实例在处理请求。对无状态服务,这通常无害;对依赖「同一时刻只有一个实例」的程序——持有文件锁、作为主节点写入、消费固定分区——就是脑裂。这也是 StatefulSet 在节点失联时不会自动创建替代 Pod 的原因:控制面无法确认旧实例已经停止,需要人工确认节点确实关机后强制删除,或者由能隔离节点的机制来处理。

实验把容忍时间改成了 20 秒。默认情况下,每个 Pod 会自动获得这两个污点的 300 秒容忍(实验中普通 Pod 的 spec.tolerations 里能看到),也就是节点失联后,上面的 Pod 要在约 50 秒的判定时间之外再等 5 分钟才会被驱逐重建。对需要快速切换的服务,可以按需缩短;但太短会让短暂的网络抖动也触发大规模迁移。另外,官方文档说明,同一可用区内不健康节点的比例达到 55% 时驱逐会降速,在 50 个节点以下的小集群里会直接停止驱逐——大面积失联更可能是控制面自己的网络问题,这时再驱逐只会让情况更糟。

五、排查顺序 ​

  1. kubectl get node <节点> -o jsonpath='{.status.conditions}',先看 Ready 是 False 还是 Unknown。
  2. False:kubelet 在线,读原因和消息。网络插件未就绪,查 /etc/cni/net.d 与 CNI 的 DaemonSet Pod;还要一起看 MemoryPressure、DiskPressure、PIDPressure 等条件。
  3. Unknown:控制面听不到 kubelet。到节点上查 systemctl status kubelet containerd、journalctl -u kubelet,以及节点到 apiserver 的网络和 kubelet 证书是否过期;同时看 kube-node-lease 里该节点 Lease 的续约时间。
  4. Pod 起不来时,先看它有没有被分配节点:没有,是调度问题(污点、资源);有,是节点上创建沙箱、拉镜像或启动容器的问题。
  5. 节点都 Ready 但 Pod 之间不通:在不同节点各起一个测试 Pod,直接用 Pod IP 互访,把 DNS、Service 与 Pod 网络分开验证。
  6. 处理失联节点上的有状态实例之前,先确认旧容器真的停了。

配套实验

  • codesphere-labs/kubernetes/node-notready:kind 集群(Kubernetes 1.36.4,关闭默认 CNI)中的静态 Pod 与启动链、没有 CNI 时的节点条件与两种 Pod 卡住的位置、手工写入 CNI 配置与跨节点路由、停止 kubelet 后的节点状态、驱逐时间与仍在运行的旧容器(验证记录)

参考资料

文章以 CC BY-NC-SA 4.0 授权 · 代码片段以 MIT 授权