K8s节点突然NotReady,10分钟定位根因:我的排查SOP全公开
先搞清楚NotReady到底意味着什么
很多人一看到NotReady就慌,其实这个状态本身不是故障,是一个信号。
Kubernetes的node-controller每隔一段时间检查节点的心跳。具体来说,kubelet会持续更新两个东西:一个是Node对象的.status.conditions,一个是kube-node-lease命名空间下的Lease对象。默认配置下,如果超过40秒没收到更新(node-monitor-grace-period),controller-manager就会把节点标记为NotReady。
所以NotReady的本质是:控制平面和节点之间的通信断了。
断的原因千奇百怪——kubelet挂了、containerd卡了、网络抖了、磁盘满了、内核panic了,甚至是证书过期了。但排查路径是可以固化的。
第一步:看Node的Conditions,别瞎猜
拿到告警第一件事不是SSH到节点上去,而是先看控制平面能告诉你什么:
kubectl get nodes
kubectl describe node <node-name>重点看Conditions这一段:
Conditions:
Type Status Reason Message
---- ------ ------ -------
MemoryPressure True KubeletHasInsufficientMemory kubelet has insufficient memory available
DiskPressure True KubeletHasDiskPressure kubelet has disk pressure
PIDPressure False KubeletHasSufficientPID kubelet has sufficient PID available
Ready False KubeletNotReady container runtime network not ready这几个Condition能直接告诉你大方向:
- MemoryPressure=True → 内存不够了,节点开始OOM Kill
- DiskPressure=True → 磁盘快满了,image gc和日志清理跟不上
- PIDPressure=True → 进程数爆了,通常是fork bomb或日志进程泄露
- Ready=False + 各种message → 要看具体message
另外看一下Events:
kubectl get events --field-selector involvedObject.name=<node-name> --sort-by='.lastTimestamp'以及Lease的最后更新时间:
kubectl get lease -n kube-node-lease <node-name> -o jsonpath='{.spec.renewTime}'如果Lease的renewTime很久没更新了,说明kubelet确实不在汇报了。
第二步:SSH上去看kubelet
90%的NotReady都能从kubelet状态找到线索。
systemctl status kubelet
journalctl -u kubelet --since "10 minutes ago" --no-pager | tail -100常见的几种日志表现:
场景A:kubelet直接挂了
Active: inactive (dead) since ...这种最简单,systemctl restart kubelet拉起来再看。但要查为什么挂的——通常是OOM Killer把它干掉了:
dmesg | grep -i "oom\|killed" | tail -20如果看到Out of memory: Killed process xxxx (kubelet),说明节点内存确实爆了。
场景B:kubelet活着但不干活
日志里反复刷:
PLEG is not healthy: pleg was last seen active 3m30s ago; threshold is 3m0sPLEG(Pod Lifecycle Event Generator)卡住了。这个组件负责定期检查所有容器的状态,如果容器运行时响应慢,它就会卡住,进而导致kubelet标记自己不健康。
场景C:证书问题
x509: certificate has expired or is not yet validkubeadm集群的证书默认一年过期。如果集群快一年了还没做过证书轮换,大概率就是这个。
第三步:查容器运行时
kubelet依赖containerd(或者CRI-O)来管理容器。运行时出问题,kubelet就废了。
systemctl status containerd
crictl info
crictl ps -a | head -20crictl info会返回运行时的状态信息,如果连这个命令都超时了,说明containerd已经卡死了。
几个高频问题:
containerd的shim进程泄露
ps aux | grep containerd-shim | wc -l正常情况下shim进程数应该和容器数差不多。如果shim进程远多于容器数,说明有僵尸shim没清理。这种情况containerd的内存和fd会持续增长,最终卡死。
解法:
# 找到孤儿shim
crictl ps -a -q | sort > /tmp/running_containers
ps aux | grep 'containerd-shim' | awk '{print $2}' | sort > /tmp/shim_pids
# 对比后手动kill孤儿进程,或者直接重启containerd
systemctl restart containerd磁盘满导致containerd无法创建新容器
df -h /var/lib/containerd
df -h /var/log/var/lib/containerd满了的话,镜像拉取和容器创建都会失败。/var/log满了的话,日志写入失败也会导致运行时异常。
第四步:网络层面排查
网络问题导致NotReady有两种情况:节点和API Server之间的通信断了,或者CNI插件出问题了。
检查节点到API Server的连通性:
# 查看kubelet配置的API Server地址
cat /etc/kubernetes/kubelet.conf | grep server
# 测试连通性
curl -k https://<api-server>:6443/healthz
# 如果用了负载均衡器
curl -k https://<lb-address>:6443/healthz检查CNI:
ls /etc/cni/net.d/
cat /etc/cni/net.d/10-*.conflist
# 看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 app=cilium-agent # Ciliumdescribe node的时候如果看到这个:
container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady说明CNI没准备好。可能是CNI的配置文件被误删了,也可能是CNI的DaemonSet Pod在这个节点上异常了。
第五步:资源压力排查
这一步主要处理MemoryPressure、DiskPressure、PIDPressure这三种情况。
内存:
free -h
cat /proc/meminfo | grep -i "memavailable\|memtotal\|swaptotal"
# 查看谁在吃内存
ps aux --sort=-%mem | head -20
# 查看cgroup限制和实际使用
cat /sys/fs/cgroup/memory/kubepods/memory.usage_in_bytes
cat /sys/fs/cgroup/memory/kubepods/memory.limit_in_byteskubelet默认会在Available Memory低于100Mi时触发MemoryPressure,开始驱逐Pod。这个阈值可以通过--eviction-hard参数调整。
磁盘:
df -h
du -sh /var/lib/containerd/*
du -sh /var/log/pods/* | sort -rh | head -10
du -sh /var/lib/kubelet/pods/* | sort -rh | head -10
# 快速清理已退出容器的日志
crictl rmi --prunekubelet的默认磁盘阈值:imagefs可用空间<15%或者nodefs可用空间<10%就会触发DiskPressure。
清理方向:
- 清理无用镜像:
crictl rmi --prune - 清理已终止Pod的日志:检查
/var/log/pods/下的大文件 - 清理过期的container:
crictl rm $(crictl ps -a -q --state exited)
PID:
# 当前系统PID使用
cat /proc/sys/kernel/pid_max
ls /proc | grep -c '^[0-9]'
# 查看哪个cgroup的进程最多
find /sys/fs/cgroup/pids -name "pids.current" -exec sh -c 'echo "$(cat {}) {}"' \; | sort -rn | head -10真实案例:凌晨三台节点同时NotReady
时间线:
- 02:03 — Prometheus告警:node-1、node-2、node-3同时NotReady
- 02:04 — 登录kubectl,describe node看到三台都是
Ready=False,message是kubelet stopped posting node status - 02:05 — Lease的renewTime确实停在了02:02左右
- 02:06 — SSH到node-1,
systemctl status kubelet显示active,但日志里在疯狂刷PLEG not healthy - 02:07 —
crictl info卡住不返回,systemctl status containerd显示active但CPU占用99% - 02:08 —
ps aux | grep containerd-shim | wc -l返回487,但crictl ps -q | wc -l只有23个容器。大量孤儿shim进程 - 02:10 — 定位根因:前一天有人部署了一个Job,配置了
restartPolicy: Always加上一个必定失败的initContainer,导致containerd疯狂创建/销毁sandbox,shim进程堆积 - 02:12 — 删除问题Job,然后
systemctl restart containerd - 02:15 — kubelet重新汇报状态,节点恢复Ready
- 02:17 — 三台节点都恢复,Pod自动重新调度
根因:一个错误配置的Job触发了containerd的shim泄露,containerd陷入高CPU状态无法响应kubelet的CRI调用,PLEG超时,kubelet标记自己为NotReady。
避坑清单
不要在节点NotReady时直接drain:
节点已经不响应了,drain会挂起等待。如果你需要紧急迁移Pod,直接delete Pod让controller在其他节点重建。
不要无脑重启kubelet:
先看日志确认原因。如果是内存爆了,重启kubelet只是拖延,过几分钟还会被OOM Kill。要先处理内存问题。
不要忽略时间同步:
NTP飘了会导致证书校验失败、Lease续期失败。养成习惯先查:
timedatectl status
chronyc tracking不要在生产环境用restartPolicy: Always配Job:
Job应该用OnFailure或Never。Always会导致Job的Pod无限重启,极端情况下能把节点资源耗尽。
千万别忘了设置Pod的resource requests和limits:
没有limits的Pod是OOM的最大隐患。一个内存泄露的应用能把整个节点吃死。
我的10分钟排查SOP清单
贴出来直接可以存到手机备忘录的版本:
[0-1min] kubectl get nodes / describe node → 看Conditions和Events
[1-2min] kubectl get lease -n kube-node-lease → 确认心跳是否停止
[2-3min] SSH节点 → systemctl status kubelet + journalctl -u kubelet
[3-4min] systemctl status containerd + crictl info
[4-6min] 根据线索分支排查:
- kubelet挂了 → dmesg | grep oom → 重启+处理内存
- PLEG不健康 → crictl ps/info → containerd问题
- 证书过期 → kubeadm certs check-expiration
- 网络断了 → curl api-server + 检查CNI
[6-8min] 资源压力:free -h / df -h / pid统计
[8-10min] 执行修复 + 确认恢复几个提升排查速度的工具和配置
kubectl-node-shell
不用单独SSH,直接通过kubectl进入节点:
kubectl node-shell <node-name>原理是起一个特权Pod挂载节点的rootfs,比传统SSH方便。特别是在托管K8s(EKS/AKS/GKE)上没有直接SSH入口时非常有用。
kubelet的verbose日志
临时开启:
# 动态调整日志级别,不需要重启kubelet
curl -X PUT "http://localhost:10248/debug/flags/v" -d "4"排查完记得改回来,v=4的日志量非常大。
节点问题检测器 node-problem-detector
这是Google出的一个DaemonSet,能检测内核死锁、文件系统损坏、容器运行时异常等底层问题,并把结果写入Node的Condition。装上之后很多问题不用SSH就能在describe node里看到。
kubectl apply -f https://raw.githubusercontent.com/kubernetes/node-problem-detector/master/deployment/node-problem-detector.yamlPrometheus + kube-state-metrics监控
提前配好告警规则,在NotReady发生前就能发现趋势:
# 磁盘使用率>80%提前告警
- alert: NodeDiskRunningFull
expr: (node_filesystem_avail_bytes{mountpoint="/var/lib/containerd"} / node_filesystem_size_bytes{mountpoint="/var/lib/containerd"}) < 0.2
for: 5m
labels:
severity: warning
annotations:
summary: "Node {{ $labels.instance }} containerd disk usage > 80%"
# 内存可用<500Mi提前告警
- alert: NodeMemoryLow
expr: node_memory_MemAvailable_bytes < 500 * 1024 * 1024
for: 2m
labels:
severity: warning写在最后
节点NotReady这事,说白了就是那么几个原因来回转。kubelet挂了、runtime卡了、资源爆了、网络断了、证书过期了。只要排查路径固化下来,形成肌肉记忆,处理起来不会太慌。
但真正重要的不是"出了事怎么查",而是"怎么让这事少发生"。资源水位监控要做好,Job的配置要review,证书轮换要自动化,磁盘清理策略要提前设置。这些预防措施做到位了,半夜被叫醒的概率能降一大半。
你要是也有过被NotReady支配的恐惧,评论区聊聊你的故事。
公众号:耕云躬行录
个人博客:躬行笔记