节点 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,观察控制面与节点各自看到什么。实验见文末。
一、一个节点是怎样就绪的
用 kubeadm 搭建的集群(kind 也是)里,节点的启动链是:
- systemd 拉起 containerd 和 kubelet;
- kubelet 读取本地的静态 Pod 目录
/etc/kubernetes/manifests,里面是 etcd、kube-apiserver、kube-controller-manager、kube-scheduler 四个清单。kubelet 直接把它们跑起来,再在 API 里为它们创建镜像 Pod,kubernetes.io/config.source注解是file; - kubelet 向 apiserver 注册节点,定期上报节点条件,并续约
kube-node-lease里的 Lease 作为心跳; - 容器运行时从
/etc/cni/net.d读取 CNI 配置,调用/opt/cni/bin里的插件。配置加载成功,运行时报告网络就绪,kubelet 才把节点报为Ready; - 节点就绪后,
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 秒。
- 停止后 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 个节点以下的小集群里会直接停止驱逐——大面积失联更可能是控制面自己的网络问题,这时再驱逐只会让情况更糟。
五、排查顺序
kubectl get node <节点> -o jsonpath='{.status.conditions}',先看Ready是False还是Unknown。False:kubelet 在线,读原因和消息。网络插件未就绪,查/etc/cni/net.d与 CNI 的 DaemonSet Pod;还要一起看MemoryPressure、DiskPressure、PIDPressure等条件。Unknown:控制面听不到 kubelet。到节点上查systemctl status kubelet containerd、journalctl -u kubelet,以及节点到 apiserver 的网络和 kubelet 证书是否过期;同时看kube-node-lease里该节点 Lease 的续约时间。- Pod 起不来时,先看它有没有被分配节点:没有,是调度问题(污点、资源);有,是节点上创建沙箱、拉镜像或启动容器的问题。
- 节点都 Ready 但 Pod 之间不通:在不同节点各起一个测试 Pod,直接用 Pod IP 互访,把 DNS、Service 与 Pod 网络分开验证。
- 处理失联节点上的有状态实例之前,先确认旧容器真的停了。
配套实验
- codesphere-labs/kubernetes/node-notready:kind 集群(Kubernetes 1.36.4,关闭默认 CNI)中的静态 Pod 与启动链、没有 CNI 时的节点条件与两种 Pod 卡住的位置、手工写入 CNI 配置与跨节点路由、停止 kubelet 后的节点状态、驱逐时间与仍在运行的旧容器(验证记录)
参考资料