k8s节点NotReady排查
## 一、速查急救包
遇到节点异常,先别慌,这几条命令能帮你快速定位方向:
```bash
# 1. 查看集群节点状态
kubectl get nodes -o wide | grep -i notready
# 2. 查看节点详情(重点关注 Conditions 和 Events)
kubectl describe node <node-name>
# 3. SSH 到问题节点后,第一时间看 kubelet 日志
journalctl -u kubelet --no-pager -n 100
# 4. 检查容器运行时是否正常
systemctl status containerd
crictl ps
# 5. 检查节点资源压力
df -h /var/lib/kubelet
free -m
top -b -o %MEM | head -20
```
## 二、快速判断:NotReady 还是 Unknown?
拿到一个异常的节点,第一步先分清是哪种情况:
| 看到的节点状态 | 优先排查方向 |
|---|---|
| `NotReady`(`Ready=False`) | kubelet 日志、容器运行时(containerd/docker)、PLEG 状态 |
| `Unknown`(`Ready=Unknown`) | 网络连通性(ping API Server)、安全组/防火墙、API Server 可达性 |
**`NotReady`** 表示 kubelet **知道自己不健康**(比如容器运行时挂了、PLEG 不健康),主动上报了 `Ready=False`。**`Unknown`** 表示**节点控制器在规定时间内没收到 kubelet 的心跳**。这个判断能帮你节省大量时间,别上来就盲目查日志。
## 三、NotReady 到底是怎么产生的?
### 3.1 节点状态的生命周期
Kubernetes 中,每个节点都有一个 `Node` 资源对象,其状态由 **节点生命周期控制器(Node Lifecycle Controller)** 和 **kubelet** 共同维护。
kubelet 通过 **Lease 对象** 向 API Server 发送心跳——每个节点在 `kube-node-lease` 命名空间下都有一个同名的 Lease 对象,kubelet 每次心跳就是更新这个 Lease 对象的 `spec.renewTime` 字段。**Lease 的更新间隔是硬编码的 10 秒**。
> ⚠️ **勘误**:原文称“节点 `.status` 本身的更新频率默认是 **5 分钟**”——这个说法需要澄清。`--node-status-update-frequency` 的**默认值是 10 秒**(kubelet 参数),而非 5 分钟。5 分钟是节点控制器(Node Controller)对节点状态进行 reconcile 的相关参数之一。**节点状态上报频率本身是 10 秒**,与 Lease 心跳频率一致。
kube-controller-manager 的 `--node-monitor-period` 参数(默认 **5 秒**)控制节点控制器检查每个节点状态的周期。`--node-monitor-grace-period` 的默认值**在不同发行版中可能不同**:
- **kubeadm** 部署的集群,该参数默认一直是 **40 秒**
- 部分云服务商的托管集群可能有不同的默认值
> ⚠️ **勘误**:原文称“从 v1.29 开始调整为 **50 秒**”——这个说法不准确。官方文档并未在 v1.29 将默认值统一改为 50 秒,具体默认值取决于发行版和安装工具。**建议通过查看 kube-controller-manager 的启动参数来确认实际值。**
官方对 Ready 条件的定义如下:
| 状态 | 含义 |
|---|---|
| `True` | 节点健康,已准备好接收 Pod |
| `False` | 节点不健康,不能接收 Pod |
| `Unknown` | 节点控制器在最近 `node-monitor-grace-period` 内没有收到节点的消息 |
### 3.2 Ready 条件的四个子状态
`kubectl describe node` 输出的 Conditions 里,除了 `Ready` 本身,还有四个子条件:
| Condition | True 的含义 | 后果 |
|---|---|---|
| `MemoryPressure` | 节点内存不足(可用内存低于阈值)| 添加 `NoSchedule` 污点,阻止新 Pod 调度 |
| `DiskPressure` | 节点磁盘空间不足 | 添加 `NoSchedule` 污点,阻止新 Pod 调度 |
| `PIDPressure` | 节点进程数过多 | 添加 `NoSchedule` 污点,阻止新 Pod 调度 |
| `NetworkUnavailable` | CNI 网络插件未就绪 | 节点无法提供 Pod 网络 |
> ⚠️ **重要勘误**:原文称“如果任意一个压力条件为 True,节点可能直接变为 NotReady”——**这是错误的**。`MemoryPressure`、`DiskPressure`、`PIDPressure` 为 `True` 时,kubelet 会**自动添加对应的调度污点**(如 `node.kubernetes.io/memory-pressure`,Effect=`NoSchedule`),阻止新 Pod 调度到该节点。**但 `Ready` 条件本身不受影响**——只要 kubelet 能正常向 API Server 发送心跳,`Ready` 依然保持 `True`。
**只有当压力严重到导致 kubelet 无法发送心跳**(进程卡死、OOM 等)时,才会间接导致 `NotReady`——这是**间接结果**,而非 `MemoryPressure=True` 的直接映射。
### 3.3 污点与驱逐机制
当 `Ready` 状态保持 `Unknown` 或 `False` 的时间超过 `NodeMonitorGracePeriod` 时,控制平面会自动添加对应的污点:
- `Ready=False` → 添加 `node.kubernetes.io/not-ready` 污点
- `Ready=Unknown` → 添加 `node.kubernetes.io/unreachable` 污点
> ⚠️ **勘误**:原文称“这些污点带有 **NoSchedule** 效果……也可能带有 **NoExecute** 效果”——**表述不准确**。`node.kubernetes.io/not-ready` 和 `node.kubernetes.io/unreachable` 这两个污点**同时具有 NoSchedule 和 NoExecute 两种效果**,是**默认就同时具备**的,不是“可能带有”。
**注意**:污点是在 `node-monitor-grace-period` 超时**之后**才添加的,不是 `NotReady` 的瞬间就打上去的。
**驱逐机制的关键参数**:
在 Kubernetes v1.13 及更高版本中,**基于污点的驱逐(Taint-Based Evictions)默认开启**。Pod 默认会对 `node.kubernetes.io/not-ready` 和 `node.kubernetes.io/unreachable` 这两个污点添加容忍度,`tolerationSeconds` 默认值为 **300 秒**(5 分钟)。
这个默认值由 **kube-apiserver** 的两个参数控制:
- `--default-not-ready-toleration-seconds`:默认 300 秒
- `--default-unreachable-toleration-seconds`:默认 300 秒
### 3.4 从节点故障到 Pod 被驱逐的时间线
从节点实际出问题到 Pod 被驱逐,时间线大致如下(以 kubeadm 部署、`node-monitor-grace-period=40s` 为例):
| 时间点 | 事件 |
|---|---|
| 0 秒 | 节点出问题,kubelet 停止上报心跳 |
| 40 秒 | `node-monitor-grace-period` 超时 → 节点被标记为 NotReady/Unknown,打上对应污点(同时具备 NoSchedule + NoExecute) |
| 5 分 40 秒 | 污点添加后等待 300 秒容忍度超时 → Taint Manager 开始驱逐 Pod |
> 💡 **关键理解**:`tolerationSeconds` 是从污点被添加到节点的那一刻开始计时的,不是从节点出问题的那一刻开始。
### 3.5 关于 `Unknown` 状态的污点与驱逐
当节点变为 `Unknown` 状态时,节点已经“失联”了。但**“打污点”和“驱逐 Pod”这两个动作,并非由失联的节点自身执行,而是由依然健康运行的控制平面(Control Plane)组件来完成的**。
控制平面在确认节点失联后,主动采取一系列“隔离”与“清理”措施:
1. **第 40 秒**(`node-monitor-grace-period` 到期):控制平面**同时**给该节点打上**两个** `node.kubernetes.io/unreachable` 污点:
- `Effect=NoSchedule`:**立即生效**,禁止新 Pod 调度到该节点
- `Effect=NoExecute`:**立即标记**,但因为 Pod 默认带有 300 秒的容忍度,驱逐倒计时从此刻开始
2. **第 340 秒**(40 秒 + 300 秒):Pod 的默认容忍度到期,控制平面开始执行驱逐动作。
## 四、分类排查:根据现象找根因
### 4.1 现象一:`kubectl describe node` 显示 `Ready=False`,Message 里有具体原因
这是最友好的一种情况——kubelet 自己知道出了什么问题,直接告诉你了。
#### 4.1.1 PLEG is not healthy
**现象**:节点 NotReady,`kubectl describe node` 的 Ready 条件里出现类似 `"PLEG is not healthy: pleg was last seen active 3m46s ago; threshold is 3m0s"` 的错误。
**原理**:PLEG(Pod Lifecycle Event Generator)是 kubelet 的一个核心组件,负责定期向容器运行时(CRI)查询 Pod 和容器的状态。当容器运行时响应变慢、卡死或完全不可用时,PLEG 就无法完成定期 `relist` 操作。
**排查命令**:
```bash
# 测试容器运行时响应速度
time crictl --runtime-endpoint unix:///run/containerd/containerd.sock ps
# 检查容器运行时日志
journalctl -u containerd -n 100
```
**解决方案**:
- 如果 `crictl ps` 超时或极慢,重启容器运行时:`systemctl restart containerd`
- 检查磁盘 I/O(`iostat -x 1`),看是不是存储性能问题
- 检查节点上 Pod 数量是否过多,考虑疏散部分工作负载
#### 4.1.2 NetworkPluginNotReady
**现象**:`kubectl describe node` 显示 `NetworkUnavailable=True`,Message 类似 `"Network plugin returns error: cni plugin not initialized"`。
**原理**:CNI 插件(Calico、Flannel、Cilium 等)的 DaemonSet Pod 没有正常运行,或者 CNI 配置文件(`/etc/cni/net.d/`)有问题。
**排查命令**:
```bash
# 检查 CNI 配置文件
ls -la /etc/cni/net.d/
cat /etc/cni/net.d/*.conf
# 查看 CNI 插件 Pod 状态
kubectl get pods -n kube-system --field-selector spec.nodeName=<node-name>
# 查看 kubelet 日志中的 CNI 相关错误
journalctl -u kubelet | grep -i cni
```
**解决方案**:
- 重启 CNI 插件的 DaemonSet Pod:`kubectl delete pod -n kube-system <cni-pod-name>`
- 如果配置文件损坏,可以从其他正常节点拷贝一份
- 检查 CNI 插件的镜像是否能正常拉取
#### 4.1.3 证书过期
**现象**:kubelet 进程在运行,但节点 NotReady。查看 kubelet 日志,出现 `"certificate has expired"` 或 `"x509: certificate signed by unknown authority"`。
**原理**:kubelet 使用证书与 Kubernetes API 进行认证。默认情况下,这些证书的有效期为一年。
> ⚠️ **勘误**:原文称“证书轮换的触发时机在**证书剩余有效期的 30% 到 10% 之间**”——这个说法有争议。根据 Kubernetes 官方源码(kubelet 证书管理器),轮换的触发时机是**当证书剩余有效期低于总有效期的 30% 时开始尝试轮换**,而非“30% 到 10% 之间”这个区间描述。更准确的说法是:**在剩余有效期低于 30% 时触发轮换**,如果轮换失败,会在后续继续重试直到低于 10% 的紧急阈值。
**排查命令**:
```bash
# 检查 kubelet 客户端证书有效期
openssl x509 -in /var/lib/kubelet/pki/kubelet-client-current.pem -noout -dates
# 查看是否有 Pending 的 CSR
kubectl get csr | grep Pending
```
**解决方案**:
- 如果是 kubeadm 部署的集群,可以通过 `kubeadm init phase kubelet-finalize enable-client-cert-rotation` 启用客户端证书轮换
- 手动批准 Pending 的 CSR:`kubectl certificate approve <csr-name>`
- 如果自动轮换失效,可以重启 kubelet 触发重新申请:`systemctl restart kubelet`
### 4.2 现象二:`kubectl describe node` 显示某个 Pressure 条件为 True
这种情况说明节点资源吃紧。**但请注意**:压力条件为 `True` 本身**不会导致 `NotReady`**——它只会触发添加 `NoSchedule` 污点。只有当压力严重到影响 kubelet 心跳时,才会间接导致 `NotReady`。
#### 4.2.1 DiskPressure=True
**原理**:kubelet 监控 `/var/lib/kubelet` 所在分区的磁盘使用率。默认的硬驱逐阈值(`evictionHard`)包括:
- `nodefs.available < 10%`(主文件系统可用空间低于 10%)
- `imagefs.available < 15%`(镜像文件系统可用空间低于 15%)
- `nodefs.inodesFree < 5%`
- `memory.available < 100Mi`
**kubelet 的自动回收流程**:
当 `DiskPressure=True` 时,kubelet 会启动自动的、分阶段的资源回收流程:
1. **第一步:温和回收(垃圾回收)**
- **镜像垃圾回收器**被激活,删除不再被任何运行中容器引用的镜像
- 触发阈值:磁盘使用率达到 **85%**(默认的 `image-gc-high-threshold`)
- 回收目标:删除镜像直到磁盘使用率降至 **80**%(默认的 `image-gc-low-threshold`)以下
- kubelet 也会定期清理已停止运行(Exited)的容器
2. **第二步:如果回收无效,触发 Pod 驱逐**
- 当磁盘使用情况达到 `--eviction-hard` 或 `--eviction-soft` 定义的阈值时,kubelet 开始驱逐 Pod
- 硬驱逐会**立即终止 Pod**,不等待优雅终止期限
> 💡 **整个过程是 kubelet 内置的自动化闭环控制**:监控 → 分级响应(先垃圾回收,再 Pod 驱逐)→ 压力解除后自动将 `DiskPressure` 恢复为 `False`。
**排查命令**:
```bash
# 查看磁盘使用情况
df -h /var/lib/kubelet
du -sh /var/lib/kubelet/* | sort -rh | head -20
# 清理未使用的镜像
crictl rmi --prune
# 清理已退出的容器
crictl rm $(crictl ps -a -q --state exited)
```
**解决方案**:
- 紧急清理:删除不需要的镜像和已退出的容器
- 长期方案:配置日志轮转(logrotate)、将容器日志挂载到独立磁盘、增加节点磁盘容量
- 调整驱逐阈值:在 kubelet 配置中修改 `evictionHard` 参数
> ⚠️ **重要警告**:看到 `DiskPressure` 就去删 `/var/lib/kubelet` 下面的文件?**千万别!** `/var/lib/kubelet` 里存的是 Pod 的 Volume 数据,乱删会导致 Pod 数据丢失。正确的做法是用 `crictl rmi --prune` 清理未使用的镜像。
#### 4.2.2 MemoryPressure=True
**排查命令**:
```bash
# 查看内存使用情况
free -m
# 找出内存占用最高的进程
top -b -o %MEM | head -20
# 查看 kubelet 驱逐阈值配置
cat /var/lib/kubelet/config.yaml | grep -A5 eviction
```
**解决方案**:
- 检查是否有 Pod 内存泄漏,重启异常 Pod
- 调整 Pod 的 `resources.limits.memory`,避免单个 Pod 耗尽节点内存
- 考虑扩容节点或增加节点内存
### 4.3 现象三:kubelet 进程挂了
**现象**:节点 NotReady,SSH 上去后发现 `systemctl status kubelet` 显示服务已停止。
**排查命令**:
```bash
# 查看 kubelet 服务状态
systemctl status kubelet
# 查看 kubelet 崩溃前的日志
journalctl -u kubelet --since "1 hour ago"
# 检查是否被 OOM Killer 杀死
dmesg | grep -i "killed process" | grep kubelet
```
**解决方案**:
- 如果是 OOM 导致,检查节点内存是否充足,调整 kubelet 的 `--eviction-hard` 参数
- 如果是配置错误,检查 `/var/lib/kubelet/config.yaml` 和 `/etc/kubernetes/kubelet.conf`
- 修复后重启 kubelet:`systemctl restart kubelet`
### 4.4 现象四:节点状态在 Ready 和 NotReady 之间反复横跳
这是最恶心的一种情况——节点时好时坏,Pod 被反复驱逐又重新调度,业务时断时续。
**常见原因**:
- 网络不稳定:节点与 API Server 之间的网络间歇性丢包
- API Server 负载过高,无法及时响应 kubelet 的心跳
- 节点资源刚好在驱逐阈值附近振荡(软驱逐场景)
- kubelet 上报频率和节点控制器等待时间的比例失调
**排查思路**:
- 从其他节点 ping API Server 的 IP,看延迟和丢包率
- 检查 API Server 的 Pod 资源使用情况:`kubectl top pod -n kube-system | grep apiserver`
- 查看 kubelet 日志中是否有 `"Failed to post node status"` 之类的网络错误
- 如果是云环境,检查安全组/防火墙规则是否有变动
- 确认 `--node-status-update-frequency` 和 `--node-monitor-grace-period` 的配置是否合理
## 五、手动驱逐:如果等不了 5 分钟怎么办?
默认情况下,从节点失联(0 秒)到 Pod 被驱逐(5 分 40 秒后),中间有接近 6 分钟的空窗期。如果业务不能等这么久,可以**手动介入**:
### 方案一:使用 `kubectl drain`(最推荐)
```bash
kubectl drain <node-name> --ignore-daemonsets --delete-emptydir-data --force
```
- `--ignore-daemonsets`:忽略 DaemonSet 管理的 Pod
- `--delete-emptydir-data`:允许删除使用 `emptyDir` 的 Pod
- `--force`:即使节点已处于 `NotReady` 状态,也强制驱逐
### 方案二:使用 `out-of-service` 污点(Kubernetes v1.26+)
这是 Kubernetes 1.28 版本正式 GA 的特性,专门用于处理节点非体面关闭的场景:
```bash
kubectl taint nodes <node-name> node.kubernetes.io/out-of-service=nodeshutdown:NoExecute
```
添加此污点后,控制平面会**立刻驱逐**节点上所有没有容忍此污点的 Pod。
### 方案三:直接强制删除 Pod(最后手段)
```bash
kubectl delete pod <pod-name> --grace-period=0 --force
```
⚠️ **风险**:可能导致“脑裂”(同一服务两个实例同时运行)、存储卷无法卸载、临时数据丢失。
## 六、验证节点恢复正常
修复完成后,按以下步骤验证:
```bash
# 1. 确认节点状态变为 Ready
kubectl get nodes
# 2. 查看节点 Conditions 全部为 False(NetworkUnavailable 应为 False)
kubectl describe node <node-name> | grep -A10 Conditions
# 3. 确认节点上的 Pod 正常运行
kubectl get pods --field-selector spec.nodeName=<node-name> -A
# 4. 如果之前有 Pod 被驱逐,确认它们已在新节点上重新调度
kubectl get pods -A | grep -v Running | grep -v Completed
```
## 七、⚠️ 两个能坑死你的误区
### 误区一:看到节点 NotReady 就重启节点
很多刚接触 K8s 的同学,看到节点 NotReady 第一反应是 **“重启节点”** 。这个操作在云环境下可能触发自动修复流程,但在自建集群里,直接重启物理机或虚拟机往往会带来更大的麻烦:
- 节点重启后,kubelet 需要重新初始化和同步缓存,大规模节点上这个过程会产生大量 API Server 请求
- 重启期间,节点会短暂处于 NotReady 状态
- 如果节点上有 StatefulSet 的 Pod(尤其是 etcd、数据库等),粗暴重启可能导致数据不一致
**建议**:除非你确认节点操作系统已经卡死(SSH 都连不上),否则永远**先 SSH 进去查日志、看状态**,不要急着重启。
### 误区二:压力条件 = NotReady
**这是最常见的技术误解**。`MemoryPressure=True`、`DiskPressure=True`、`PIDPressure=True` **并不会直接导致 `Ready=False`**。
正确的关系是:
- 压力条件为 `True` → kubelet 添加 `NoSchedule` 污点(阻止新 Pod 调度)
- 压力严重到导致 kubelet 无法发送心跳 → 节点控制器将 `Ready` 置为 `False` 或 `Unknown`
**两者是解耦的独立条件**,不要混淆。
---
参考文章:https://mp.weixin.qq.com/s/su6l_eLARBLNkQ20I6b4rw

浙公网安备 33010602011771号