
凌晨三点被刺耳的告警叫醒,Kubernetes 集群某个节点状态突然变成了 NotReady,上面的 Pod 被驱逐,服务开始抖动——这种场景相信每个 K8s 运维都经历过。节点 NotReady 是 Kubernetes 运维中最常见也最令人头疼的问题之一,原因五花八门:可能是 kubelet 挂了,可能是 Docker/containerd 卡死了,可能是网络插件出问题了,也可能是节点资源耗尽了。
这篇文章将我们团队遇到过的各种 NotReady 场景整理成一份系统化的自查手册,旨在帮助你在 15 分钟内定位问题根因。建议收藏,下次出问题直接按清单排查。
技术特点
- 系统化排查:按照优先级从高到低的顺序排查,避免遗漏关键点。
- 快速定位:每个检查点都给出具体命令和预期输出,一看就知道问题在哪。
- 覆盖全面:涵盖 kubelet、容器运行时、网络、存储、资源等各种可能原因。
- 实战验证:所有内容均从实际生产故障中总结而来,绝非纸上谈兵。
适用场景
- 场景一:节点突然 NotReady,需要快速定位原因。
- 场景二:节点频繁在 Ready 和 NotReady 之间切换(Flapping)。
- 场景三:新节点加入集群后无法变成 Ready 状态。
- 场景四:集群升级或节点维护后,节点无法恢复正常。
环境要求
| 组件 |
版本要求 |
说明 |
| Kubernetes |
1.20+ |
本文以 1.28 为例 |
| 容器运行时 |
containerd 1.6+ 或 Docker 20+ |
推荐使用 containerd |
| 操作系统 |
Ubuntu 20.04+ / CentOS 7+ |
内核版本 4.15+ |
| 网络插件 |
Calico/Flannel/Cilium |
根据实际环境选择 |
二、详细排查步骤
2.1 准备工作
◆ 2.1.1 收集基本信息
首先,我们需要确认问题的范围和节点的基本信息。
# 首先确认哪些节点 NotReady
kubectl get nodes
# 示例输出:
# NAME STATUS ROLES AGE VERSION
# node-01 Ready control-plane 90d v1.28.2
# node-02 NotReady <none> 90d v1.28.2
# node-03 Ready <none> 90d v1.28.2
# 查看 NotReady 节点的详细信息
kubectl describe node node-02
# 重点看 Conditions 部分
kubectl get node node-02 -o jsonpath='{.status.conditions}' | jq .
◆ 2.1.2 理解 Node Conditions
节点的健康状态由几个核心的 Condition 决定,任何一个异常都可能导致节点不可用。
| Condition |
正常值 |
含义 |
| Ready |
True |
节点健康,可以接受 Pod |
| MemoryPressure |
False |
节点内存充足 |
| DiskPressure |
False |
节点磁盘空间充足 |
| PIDPressure |
False |
节点 PID 充足 |
| NetworkUnavailable |
False |
网络插件正常配置 |
2.2 核心排查流程(按优先级)
◆ 2.2.1 第一步:检查 kubelet 状态
kubelet 是节点上最重要的组件,负责向 API Server 汇报节点状态。如果 kubelet 挂了,节点肯定会变成 NotReady。
# SSH 到问题节点
ssh node-02
# 检查 kubelet 服务状态
systemctl status kubelet
# 如果 kubelet 未运行
systemctl start kubelet
systemctl enable kubelet
# 查看 kubelet 日志,寻找错误原因
journalctl -u kubelet -f --since "10 minutes ago"
# 过滤常见错误关键字
journalctl -u kubelet | grep -E "error|failed|unable" | tail -50
说明:kubelet 日志是排查问题的第一手资料。常见的 kubelet 问题包括:证书过期、配置文件错误、无法连接 API Server 等。对于复杂的集群运维问题,系统化的日志分析是关键。
◆ 2.2.2 第二步:检查容器运行时
如果 kubelet 正常,接下来需要检查容器运行时(如 containerd 或 Docker)是否健康。
# containerd 环境
systemctl status containerd
crictl info
crictl ps -a | head -20
# Docker 环境
systemctl status docker
docker info
docker ps -a | head -20
# 检查运行时是否响应(containerd)
crictl version
# 如果 crictl 命令超时不响应,说明 containerd 可能卡住了
# 需要尝试重启 containerd
systemctl restart containerd
容器运行时常见问题:
- containerd/Docker 进程僵死,需要重启。
- 存储驱动问题导致容器无法创建。
- 容器数量过多导致运行时性能下降。
# 检查容器数量
crictl ps -a | wc -l
# 如果有大量 Exited 状态的容器,可以清理一下
crictl rm $(crictl ps -a -q --state exited)
◆ 2.2.3 第三步:检查网络插件
节点需要正常的网络才能与集群其他部分通信。网络插件(CNI)故障是导致 NotReady 的常见原因。
# 查看 CNI 插件 Pod 状态
kubectl get pods -n kube-system -l k8s-app=calico-node # Calico
kubectl get pods -n kube-system -l app=flannel # Flannel
kubectl get pods -n kube-system -l k8s-app=cilium # Cilium
# 在节点上检查 CNI 配置文件
ls -la /etc/cni/net.d/
# 检查 CNI 二进制文件
ls -la /opt/cni/bin/
# 查看 CNI 插件日志
kubectl logs -n kube-system calico-node-xxxxx -c calico-node --tail=100
# 检查节点网络接口和路由
ip addr show
ip route show
网络插件常见问题:
- CNI 配置文件损坏或缺失。
- CNI Pod 异常退出。
- IPAM 地址池耗尽。
- 网络策略阻止了关键通信。
# 检查节点是否能访问 API Server(在节点上执行)
curl -k https://10.96.0.1:443/healthz
# 检查 Pod CIDR 分配
kubectl get node node-02 -o jsonpath='{.spec.podCIDR}'
◆ 2.2.4 第四步:检查系统资源
资源耗尽(如磁盘、内存、PID)会直接导致节点压力状态异常,进而变为 NotReady。
# 检查内存
free -h
# 如果 available 内存很低,可能是内存压力
# 检查磁盘空间和 inode
df -h
df -i # inode 使用情况也要看
# 检查 CPU 负载
top -bn1 | head -20
uptime
# 检查 PID 使用情况
cat /proc/sys/kernel/pid_max
ls /proc | grep -E '^[0-9]+$' | wc -l
资源相关常见问题:
- 磁盘空间不足(特别是
/var/lib/docker 或 /var/lib/containerd)。
- inode 耗尽(通常是生成了大量小文件)。
- 内存不足触发 OOM(Out-Of-Memory Killer)。
- PID 耗尽。
# 查看最近的 OOM 记录
dmesg | grep -i "out of memory" | tail -10
journalctl -k | grep -i "oom" | tail -10
# 查看哪个目录占用空间最多
du -sh /var/lib/* 2>/dev/null | sort -rh | head -10
# 清理容器日志(临时解决方案)
find /var/lib/docker/containers -name "*.log" -size +100M -exec truncate -s 0 {} \;
◆ 2.2.5 第五步:检查证书和时间
证书过期或节点时间与集群不同步,会导致身份验证失败,从而使节点失联。
# 检查 kubelet 客户端证书是否过期
openssl x509 -in /var/lib/kubelet/pki/kubelet-client-current.pem -noout -dates
# 检查系统时间是否正确
date
timedatectl status
# 如果时间不同步,重启时间同步服务
systemctl restart chronyd # 或 systemd-timesyncd
# 检查与 API Server 的时间差
# 时间差太大会导致证书验证失败
证书和时间常见问题:
- kubelet 证书过期。
- 系统时间与集群控制平面不同步。
- CA 根证书变更后,节点未及时更新。
2.3 快速恢复操作
◆ 2.3.1 常规重启流程
如果通过上述排查找到了可疑组件,可以尝试按顺序重启。
# 按顺序重启组件(建议先在业务低峰期操作)
systemctl restart containerd # 或 docker
sleep 10
systemctl restart kubelet
# 等待并观察节点恢复状态
watch kubectl get node node-02
# 如果节点恢复后还是 NotReady,检查是否有异常的 Taint
kubectl describe node node-02 | grep -A5 Taints
◆ 2.3.2 节点重新加入集群
如果上述方法都无法解决问题,可以考虑让节点重新加入集群(这是最后的手段)。
# 在控制平面节点上,先安全驱逐并删除节点
kubectl drain node-02 --ignore-daemonsets --delete-emptydir-data
kubectl delete node node-02
# 在 node-02 上重置 Kubernetes 组件
kubeadm reset -f
rm -rf /etc/cni/net.d/*
rm -rf /var/lib/kubelet/*
# 重新加入(需要 token,如果过期需要重新生成)
# 在控制平面节点上生成新的 join 命令
kubeadm token create --print-join-command
# 在 node-02 上执行上一步生成的 join 命令
kubeadm join ...
三、示例代码与配置
3.1 完整配置示例
◆ 3.1.1 自动化排查脚本
将排查步骤脚本化,可以极大提高效率。以下是一个综合检查脚本示例:
#!/bin/bash
# 文件名:k8s-node-check.sh
# 功能:Kubernetes 节点健康检查脚本
set -e
NODE_NAME=$1
if [ -z "$NODE_NAME" ]; then
echo "用法: $0 <node-name>"
exit 1
fi
echo "=========================================="
echo "Kubernetes 节点健康检查: $NODE_NAME"
echo "=========================================="
# 1. 基本状态
echo ""
echo "[1/8] 节点基本状态"
echo "-------------------------------------------"
kubectl get node $NODE_NAME -o wide
# 2. Node Conditions
echo ""
echo "[2/8] Node Conditions"
echo "-------------------------------------------"
kubectl get node $NODE_NAME -o jsonpath='{range .status.conditions}{.type}: {.status} ({.reason}) - {.message}{"\n"}{end}'
# 3. 资源使用情况
echo ""
echo "[3/8] 资源使用情况"
echo "-------------------------------------------"
kubectl top node $NODE_NAME 2>/dev/null || echo "metrics-server 未安装或不可用"
# 4. 运行的 Pod 数量
echo ""
echo "[4/8] 运行的 Pod 数量"
echo "-------------------------------------------"
kubectl get pods --all-namespaces --field-selector spec.nodeName=$NODE_NAME --no-headers | wc -l
# 5. 系统 Pod 状态
echo ""
echo "[5/8] 系统 Pod 状态"
echo "-------------------------------------------"
kubectl get pods -n kube-system --field-selector spec.nodeName=$NODE_NAME
# 6. 节点事件
echo ""
echo "[6/8] 最近节点事件"
echo "-------------------------------------------"
kubectl get events --field-selector involvedObject.name=$NODE_NAME --sort-by='.lastTimestamp' | tail -10
# 7. Taints
echo ""
echo "[7/8] 节点 Taints"
echo "-------------------------------------------"
kubectl get node $NODE_NAME -o jsonpath='{.spec.taints}' | jq . 2>/dev/null || echo "无 Taints"
# 8. 容量和分配
echo ""
echo "[8/8] 节点容量和可分配资源"
echo "-------------------------------------------"
echo "Capacity:"
kubectl get node $NODE_NAME -o jsonpath='{.status.capacity}' | jq .
echo ""
echo "Allocatable:"
kubectl get node $NODE_NAME -o jsonpath='{.status.allocatable}' | jq .
echo ""
echo "=========================================="
echo "检查完成"
echo "=========================================="
◆ 3.1.2 节点监控告警规则(Prometheus)
预防胜于治疗。配置监控告警可以在节点出现问题时第一时间通知你。
# prometheus-node-alerts.yaml
groups:
- name: kubernetes-node
rules:
- alert: KubernetesNodeNotReady
expr: kube_node_status_condition{condition="Ready",status="true"} == 0
for: 5m
labels:
severity: critical
annotations:
summary: "Kubernetes 节点 NotReady"
description: "节点 {{ $labels.node }} 已经 NotReady 超过 5 分钟"
- alert: KubernetesNodeMemoryPressure
expr: kube_node_status_condition{condition="MemoryPressure",status="true"} == 1
for: 5m
labels:
severity: warning
annotations:
summary: "节点内存压力"
description: "节点 {{ $labels.node }} 存在内存压力"
- alert: KubernetesNodeDiskPressure
expr: kube_node_status_condition{condition="DiskPressure",status="true"} == 1
for: 5m
labels:
severity: warning
annotations:
summary: "节点磁盘压力"
description: "节点 {{ $labels.node }} 存在磁盘压力"
- alert: KubernetesNodePIDPressure
expr: kube_node_status_condition{condition="PIDPressure",status="true"} == 1
for: 5m
labels:
severity: warning
annotations:
summary: "节点 PID 压力"
description: "节点 {{ $labels.node }} 存在 PID 压力"
- alert: KubeletDown
expr: up{job="kubelet"} == 0
for: 3m
labels:
severity: critical
annotations:
summary: "Kubelet 不可达"
description: "节点 {{ $labels.instance }} 的 kubelet 无法访问"
3.2 实际应用案例
◆ 案例一:containerd 僵死导致 NotReady
场景描述:某天早上发现一个节点 NotReady,上面的 Pod 全部被驱逐。登录节点后发现 kubelet 进程在运行,但日志里一直报 containerd 调用超时。
排查过程:
# 查看 kubelet 日志
journalctl -u kubelet --since "1 hour ago" | grep -i error
# 发现大量这样的错误:
# "failed to get container status" err="rpc error: code = DeadlineExceeded desc = context deadline exceeded"
# 检查 containerd 状态
systemctl status containerd
# Active: active (running) 但实际上已经僵死
# 尝试执行 crictl 命令,卡住不返回
timeout 10 crictl ps
# 超时
# 检查 containerd 进程
ps aux | grep containerd
# 进程在但 CPU 使用为 0,说明僵死了
解决方案:
# 强制停止 containerd 及相关进程
systemctl stop containerd
pkill -9 containerd-shim
pkill -9 containerd
# 清理可能的运行时残留文件
rm -rf /run/containerd/*
# 重启服务
systemctl start containerd
systemctl restart kubelet
# 验证恢复
crictl ps
kubectl get node
根因分析:事后分析发现,是某个容器挂载的 volume 出现问题,导致 containerd 在处理该容器时卡死。建议定期巡检并清理异常状态的容器。
◆ 案例二:磁盘空间不足导致 NotReady
场景描述:节点 NotReady,kubectl describe node 显示 Condition 中 DiskPressure 为 True。
排查过程:
# 查看磁盘使用
df -h
# /var/lib/docker 200G 195G 5G 98% /var/lib/docker
# 找出大文件或目录
du -sh /var/lib/docker/* | sort -rh | head
# 100G /var/lib/docker/overlay2
# 80G /var/lib/docker/containers
# 检查容器日志大小
ls -lhS /var/lib/docker/containers/*/
# 发现某个容器的日志文件达到了 50G
解决方案:
# 临时方案:截断过大的日志文件
truncate -s 0 /var/lib/docker/containers/xxx/*-json.log
# 清理未使用的镜像、容器和卷
docker system prune -a -f
# 长期方案:配置 Docker 日志轮转
# 编辑 /etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "3"
}
}
# 重启 Docker 使配置生效
systemctl restart docker
实现步骤:
- 为容器运行时配置日志轮转策略。
- 设置磁盘使用率监控告警(例如 80% 预警,90% 告警)。
- 建立定期清理无用镜像和容器的运维流程。
四、最佳实践与注意事项
4.1 最佳实践
◆ 4.1.1 预防性监控
◆ 4.1.2 安全与维护
- 安全的节点维护流程:任何可能影响 Pod 的操作前,务必先隔离节点。
kubectl cordon node-02 # 禁止调度新 Pod
kubectl drain node-02 --ignore-daemonsets --delete-emptydir-data # 安全驱逐现有 Pod
# ... 执行维护操作 ...
kubectl uncordon node-02 # 恢复调度
- 定期轮换证书:定期检查并更新集群证书,避免过期。
kubeadm certs check-expiration
kubeadm certs renew all
4.2 注意事项与常见错误
警告:切勿在没有执行 drain 操作的情况下,直接重启节点的 kubelet 或容器运行时,这可能导致运行中的 Pod 被意外终止。
| 错误现象 |
原因分析 |
解决方案 |
| “failed to run kubelet: misconfiguration” |
kubelet 配置文件语法错误 |
检查 YAML 格式,使用 kubelet --config 验证配置 |
| “Unable to register node with API Server” |
无法连接 API Server |
检查网络连通性、防火墙规则和路由 |
| “certificate has expired” |
证书过期 |
使用 kubeadm certs renew 更新证书 |
| “PLEG is not healthy” |
Pod 生命周期事件生成器异常 |
通常是容器运行时问题,尝试重启 containerd/docker |
| “failed to garbage collect” |
垃圾回收失败 |
检查磁盘空间,手动清理未使用的镜像和容器 |
五、总结与进阶
5.1 技术要点回顾
- 排查顺序是关键:按照 kubelet → 容器运行时 → 网络插件 → 系统资源 → 证书/时间 的优先级进行排查,效率最高。
- 日志是破案线索:
journalctl -u kubelet 和容器运行时日志是定位根因的第一现场。
- 监控预防是上策:配置完善的监控告警,在用户感知前发现问题。
- 维护操作要规范:操作节点前先
cordon 和 drain,是对线上业务的基本尊重。
5.2 进阶学习方向
- Node Problem Detector:深入学习如何自动检测节点层面的各类问题,实现初步自愈。
- Cluster Autoscaler:了解如何根据负载自动伸缩集群节点数量,应对流量波动。
- 深入 kubelet 原理:阅读 kubelet 源码或设计文档,理解其与容器运行时、CNI 的交互机制,从根本上提升运维深度。
节点 NotReady 的排查是 Kubernetes 运维的基本功,希望这份手册能成为你工具箱中的得力助手。在云原生的复杂世界里,系统性的方法和持续的学习是应对挑战的最佳途径。如果你在实践中遇到了更棘手的问题或有独到的经验,欢迎在云栈社区与广大开发者交流分享。