Kubernetes节点NotReady故障深度排障:从IO hang到根因定位
1. 项目概述这不是一次“重启就能好”的普通告警“记录一次k8s节点故障的解决记录”——这个标题看起来平平无奇甚至有点像运维日志里随手记下的流水账。但如果你在真实生产环境里盯过Kubernetes集群的监控面板经历过凌晨三点收到NodeNotReady告警、Prometheus告警风暴连响七条、业务Pod批量Pending、用户投诉电话开始进线的时刻你就会明白这八个字背后是一场没有硝烟的系统级压力测试一次对集群设计、监控覆盖、排障逻辑和团队响应机制的全维度拷问。我最近就在某中型互联网公司的核心业务集群里完整走了一遍这个过程涉及一个运行着32个微服务、日均处理470万次API调用的K8s集群其中一台Worker节点突然掉线表面看只是单点故障实则暴露出从内核参数配置到云平台底层磁盘IO调度策略的多层隐患。这次故障不是靠kubectl delete node再等自动重建就能收场的它逼着我们重新审视“节点健康”的定义边界是kubelet进程活着就算健康还是必须满足CPU负载70%、磁盘可用率15%、网络延迟5ms、内核OOM Killer从未触发才算真正可靠本文不讲教科书式的K8s架构图也不堆砌kubectl命令大全而是把整个排障链路掰开揉碎从第一眼看到告警时该盯哪三个指标到为什么systemctl restart kubelet会失败再到如何用crictl绕过kubelet直接检查容器运行时状态最后定位到一块被云厂商标记为“降级但可用”的SSD磁盘——它没坏却让etcd写入延迟飙升至2.3秒直接拖垮了整个节点的心跳机制。适合正在搭建生产级K8s集群的SRE、刚接手线上环境的初级运维、或者想搞懂“为什么我的Pod总是在某个节点上莫名OOM”的开发同学。你不需要背熟所有K8s组件名但需要知道当kubectl get nodes显示NotReady时下一步该敲什么命令、看哪行日志、查哪个内核参数。2. 故障现象还原与根因推演从表象到内核的四层穿透2.1 现象分层记录告警不是起点而是结果故障发生于工作日下午14:23最先触发的是集群级告警第一层集群感知层AlertManager推送KubeNodeNotReady告警目标节点node-prod-worker-07状态变为NotReady持续时间90秒第二层组件通信层kubectl describe node node-prod-worker-07显示Conditions中Ready状态为FalseLast Heartbeat Time停留在14:22:17而Last Transition Time为14:22:45——说明心跳中断后仅28秒就被标记为异常第三层资源调度层kubectl get pods --all-namespaces -o wide | grep node-prod-worker-07返回17个Pod全部处于Unknown或Terminating状态且AGE列显示其已“存活”超48小时实际是节点失联后状态未更新第四层业务影响层该节点承载的订单服务Pod出现503 Service Unavailable错误日志中高频出现connection refused而同服务其他节点Pod日志干净。提示很多团队一看到NotReady就直奔journalctl -u kubelet这是典型误区。Kubelet只是“信使”它上报NotReady是因为自身健康检查失败而失败原因可能在更底层——比如磁盘满导致/var/lib/kubelet无法写入或内存不足触发OOM Killer干掉了kubelet进程本身。必须按“现象→组件→系统→硬件”逐层下钻。2.2 根因假设树基于K8s节点生命周期的逻辑拆解K8s节点从Ready变为NotReady本质是kubelet停止向apiserver发送心跳NodeStatus更新。根据官方文档和多年踩坑经验我把可能原因归纳为四类并按发生概率排序kubelet进程崩溃或僵死概率45%最常见如配置错误导致启动失败、内存泄漏耗尽RAM、被OOM Killer杀死底层系统资源枯竭概率30%磁盘/var/lib/kubelet或/var/lib/containerd分区满注意不是/根分区、内存不足、CPU软锁死soft lockup网络通信中断概率15%节点到apiserver的网络不通防火墙策略变更、云平台安全组误配、BGP路由抖动、DNS解析失败导致无法连接etcd内核级故障概率10%内核panic、cgroup v2挂载异常、NVMe驱动bug导致存储IO hang。本次故障中我们快速排除了第3项curl -k https://apiserver-ip:6443/healthz在故障节点上返回ok证明网络和TLS证书正常也排除了第4项dmesg -T | tail -20无panic日志cat /proc/cgroups显示cpu、memory子系统挂载完整。焦点迅速收束到前两项。2.3 关键证据链三份日志如何交叉验证真相真正的排障高手从不依赖单一日志源。我们同时调取了三个关键日志流并用时间戳对齐kubelet日志journalctl -u kubelet -n 100 --since 2024-05-20 14:20:00levelerror msgfailed to run Kubelet errfailed to initialize top level QOS containers: failed to update top level QOS cgroup memory limit: write /sys/fs/cgroup/memory/kubepods/memory.limit_in_bytes: device or resource busy这行错误暴露了核心矛盾kubelet试图更新cgroup内存限制时被拒绝device or resource busy是Linux内核的经典报错通常意味着cgroup子系统被其他进程锁定或损坏。containerd日志journalctl -u containerd -n 50 --since 2024-05-20 14:20:00levelerror msgFailed to load container erroropen /var/lib/containerd/io.containerd.runtime.v2.task/k8s.io/.../config.json: no such file or directory容器运行时无法加载容器配置说明底层存储路径已不可读——这比kubelet报错更严重指向文件系统层。系统日志dmesg -T | grep -i ext4\|nvme\|io[Mon May 20 14:22:31 2024] nvme 0000:01:00.0: I/O 12345 timeout, reset controller [Mon May 20 14:22:32 2024] EXT4-fs error (device nvme0n1p1): ext4_journal_start_sb:93: Detected aborted journal终极铁证NVMe控制器超时重置EXT4文件系统检测到日志被异常中止aborted journal这意味着磁盘IO已完全hang住所有写操作阻塞。三份日志形成完美证据闭环NVMe磁盘IO hang → EXT4文件系统崩溃 → containerd无法读写容器元数据 → kubelet因cgroup更新失败而退出 → 节点心跳中断 → NotReady。整个链条严丝合缝没有逻辑断点。3. 实操排障全流程从登录服务器到根因确认的每一步3.1 黄金10分钟标准化初筛动作清单当告警响起前10分钟的操作决定后续是“快速恢复”还是“灾难升级”。我们团队固化了一套check-node-health.sh脚本但真出事时手敲命令更快。以下是本次故障中实际执行的初筛步骤全部在故障节点SSH会话中完成确认基础连通性30秒# 检查是否能访问apiserver绕过kubeconfig直连IP curl -k -m 5 https://10.10.10.10:6443/healthz 2/dev/null || echo APISERVER UNREACHABLE # 检查DNS是否正常关键很多故障源于coredns异常 nslookup kubernetes.default.svc.cluster.local 10.10.10.10 2/dev/null | grep Address: || echo DNS FAILED快速抓取核心进程状态60秒# 查看kubelet、containerd、etcd如果本机运行进程是否存在且非僵尸 ps aux | grep -E (kubelet|containerd|etcd) | grep -v grep # 检查关键端口监听kubelet默认10250containerd默认10010 ss -tuln | grep -E :10250|:10010磁盘空间快扫45秒注意不要只看df -hK8s关键路径是/var/lib/kubelet存储Pod卷、镜像元数据、/var/lib/containerd容器运行时数据、/var/log/podsPod日志。这些目录可能位于独立LVM逻辑卷或挂载点。# 检查所有挂载点使用率特别关注Inodes df -h df -i # 直接检查K8s核心目录 du -sh /var/lib/kubelet/* /var/lib/containerd/* 2/dev/null | sort -hr | head -5内存与OOM检查30秒# 查看剩余内存和缓存 free -h # 检查OOM Killer是否近期干掉过进程重点看kubelet、containerd dmesg -T | grep -i killed process | tail -10本次故障中第3步df -h显示/var/lib/containerd所在分区/dev/nvme0n1p1使用率仅62%但df -i显示Inodes使用率98%——这已是危险信号但未达临界。真正致命的是第4步dmesg输出的NVMe超时日志让我们立刻跳过常规排查直扑存储层。3.2 深度诊断绕过kubelet直查容器运行时当kubelet已崩溃kubectl命令全部失效必须用底层工具。我们使用crictlContainer Runtime Interface CLI它是直接与containerd通信的瑞士军刀配置crictl连接containerd若未预装# 创建配置文件containerd默认监听unix:///run/containerd/containerd.sock sudo mkdir -p /etc/crictl.yaml echo runtime-endpoint: unix:///run/containerd/containerd.sock | sudo tee /etc/crictl.yaml检查容器运行时健康状态# 列出所有容器不依赖kubelet状态 sudo crictl ps -a | head -10 # 输出为空说明containerd已无法响应——问题比kubelet更底层 # 查看containerd自身状态 sudo systemctl status containerd # 果然显示activating (start)卡住证实containerd进程僵死强制查看containerd日志中的IO错误# 使用--no-pager避免日志截断过滤IO关键词 sudo journalctl -u containerd --no-pager | grep -i io\|timeout\|nvme\|block | tail -20 # 输出大量nvme0n1: failed command: READ FPDMA QUEUED坐实磁盘故障实操心得很多团队在crictl ps失败后就放弃转而尝试重启containerd。但本次故障中sudo systemctl restart containerd命令执行后卡住超过5分钟——因为重启需要写入/var/lib/containerd而该路径所在的NVMe磁盘已IO hang。此时强行重启只会延长故障时间正确做法是立即隔离节点并准备驱逐Pod。3.3 根因确认用smartctl和iostat做存储层CT扫描既然怀疑NVMe磁盘就要用专业工具验证。云服务器通常禁用SMART但物理机或部分云平台如AWS i3en支持# 安装smartmontoolsUbuntu/Debian sudo apt-get install smartmontools -y # 检查NVMe设备SMART信息注意需用nvme-cli非传统smartctl sudo apt-get install nvme-cli -y sudo nvme smart-log /dev/nvme0n1 | grep -E (critical_warning|temperature|media_errors|num_err_log_entries)本次输出critical_warning : 0x01 # 表示有媒体错误media errors media_errors : 127 # 已发生127次不可纠正的读写错误 num_err_log_entries: 255 # 错误日志已满再用iostat看实时IO# 每秒刷新重点关注await平均IO等待时间和%util设备利用率 iostat -x -d /dev/nvme0n1 1 5输出中await值飙升至12456.78 ms12秒%util恒为100%r/s和w/s接近0——这是典型的IO hang特征请求发出去但永远得不到响应。注意事项iostat的await值超过100ms即需警惕超过1000ms基本可判定存储异常。但不要只看单次采样要连续观察5次以上排除瞬时抖动。4. 解决方案与复盘从临时止损到长期加固4.1 临时应急3分钟内恢复业务流量根因明确后首要任务是止损。我们执行了以下操作全程在集群控制平面完成无需登录故障节点立即驱逐节点上所有Pod避免新Pod被调度至此# 设置节点为不可调度并驱逐现有Pod忽略daemonset kubectl drain node-prod-worker-07 --ignore-daemonsets --delete-emptydir-data --force # 如果drain卡住因Pod状态未知强制删除所有非DaemonSet Pod kubectl get pods -A -o wide | grep node-prod-worker-07 | awk {print $1,$2} | xargs -L1 sh -c kubectl delete pod -n $0 $1将节点标记为SchedulingDisabledkubectl cordon node-prod-worker-07通知云平台支持团队提供sudo nvme smart-log和dmesg截图要求更换物理磁盘。云厂商SLA承诺4小时内响应但我们要求加急处理。提示drain命令中的--force参数慎用它会强制删除Pod可能导致有状态服务数据丢失。本次因确认该节点存储已不可靠且所有Pod均为无状态服务故启用。4.2 长期加固五层防御体系落地单次故障解决只是开始真正的价值在于构建防御体系。我们据此推动了五项改进全部已在生产环境上线监控层增强在Prometheus中新增node_disk_io_time_seconds_total{device~nvme.*}指标告警当rate(node_disk_io_time_seconds_total[5m]) 5即平均每秒IO等待超5秒时触发P1告警使用node_exporter的node_filesystem_device_error指标直接监控磁盘设备错误。节点自愈机制部署node-problem-detectorNPDkubernetes-event-exporter当dmesg检测到nvme.*timeout时自动给节点打上taint: hardware-failure:NoSchedule污点并触发告警编写cronjob定期执行sudo nvme smart-log /dev/nvme0n1 | grep media_errors错误数5时自动邮件通知。存储选型规范新集群强制使用云平台提供的“高耐用性”SSD如AWS io2 Block Express禁用普通NVMe实例类型物理机采购要求供应商提供SMART历史报告并在部署前执行72小时IO压力测试fio --namerandwrite --ioenginelibaio --rwrandwrite --bs4k --size10G --runtime3600 --time_based。K8s配置优化调整kubelet--node-status-update-frequency10s默认10秒加快故障发现设置--eviction-hardmemory.available500Mi,nodefs.available10%,imagefs.available10%并在/var/lib/kubelet所在分区预留20%空间。应急预案升级将本次排障流程固化为Runbook嵌入内部Wiki包含所有命令、日志片段、截图模板每季度进行“节点故障注入演练”使用chaos-mesh模拟NVMe IO hang检验监控告警、自动驱逐、人工介入的时效性。4.3 复盘会议关键结论三个反常识认知在故障复盘会上我们总结出三个颠覆原有认知的结论已同步至全公司技术委员会认知1“磁盘空间充足”不等于“存储健康”本次故障中df -h显示磁盘仅用62%但dmesg已报IO timeout。根本原因是NVMe固件缺陷导致随机读写性能断崖式下跌而非容量问题。存储健康度必须用IO延迟latency、IOPS、错误计数errors三维评估不能只看容量水位线。认知2“NotReady”状态的黄金处置窗口只有90秒K8s默认node-monitor-grace-period40s即节点失联40秒后标记为NotReadypod-eviction-timeout5m但业务受损从第1秒就开始。从告警触发到执行drain必须控制在90秒内否则大量Pod会进入Unknown状态导致kubectl delete失效只能手动清理etcd。认知3云平台的“硬件健康”告警存在盲区云厂商控制台显示该节点“一切正常”因其只监控CPU、内存、网络带宽不采集NVMe SMART日志。必须在节点侧部署主动探测把硬件级指标SMART、dmesg错误纳入K8s监控体系不能依赖云平台被动通知。5. 常见问题与避坑指南那些文档里不会写的实战细节5.1 为什么systemctl restart kubelet会失败深层机制解析很多新手遇到NotReady第一反应就是重启kubelet但本次故障中sudo systemctl restart kubelet命令卡住近3分钟。原因在于kubelet启动流程的依赖链kubelet启动时首先尝试挂载/var/lib/kubelet/pods下的所有Pod卷包括emptyDir、hostPath、CSI卷若/var/lib/kubelet所在磁盘IO hang则挂载操作无限期阻塞同时kubelet需向containerd注册CRI服务而containerd自身也因磁盘问题无法响应最终systemd判定服务启动超时默认TimeoutStartSec90s标记为failed。避坑技巧当systemctl status kubelet显示activating (start)且长时间不变化立即执行sudo systemctl kill -s SIGUSR2 kubelet发送USR2信号触发kubelet dump goroutine栈然后sudo journalctl -u kubelet | tail -50查看卡在哪一行代码。本次dump显示卡在mounter.Mount函数直指存储层。5.2crictlvsctr何时该用哪个crictl和ctr都是containerd客户端但定位不同crictl遵循CRI标准模拟kubelet行为用于调试K8s集成场景。crictl ps列出的是K8s管理的容器crictl inspect返回的是K8s视角的容器信息含Pod ID、Labelsctrcontainerd原生命令行功能更底层。ctr -n k8s.io containers ls列出所有容器包括kubelet未管理的ctr -n k8s.io containers info id返回原始containerd容器信息。本次故障中crictl ps返回空但sudo ctr -n k8s.io containers ls仍能列出容器ID——因为ctr直接读取containerd数据库不经过CRI层。这帮助我们确认containerd服务进程仍在只是CRI接口因IO hang无法响应。5.3 如何判断是“真NotReady”还是“假NotReady”NotReady状态常被误判。我们总结出三种“假NotReady”场景及验证方法场景特征验证命令解决方案网络抖动导致心跳丢失kubectl get nodes显示NotReady但ssh node可连curl apiserver成功journalctl -u kubelet无错误watch kubectl get node node-prod-worker-07 -o wide观察INTERNAL-IP是否变化等待30秒通常自动恢复检查网络设备bufferkubelet配置错误systemctl status kubelet显示failedjournalctl报invalid configurationsudo kubelet --help | grep config确认配置文件路径sudo kubelet --config /var/lib/kubelet/config.yaml --dry-run测试配置修正/var/lib/kubelet/config.yaml重启kubelet证书过期journalctl -u kubelet报x509: certificate has expired or is not yet validsudo openssl x509 -in /var/lib/kubelet/pki/kubelet-client-current.pem -text -noout | grep Not After执行kubeadm certs renew node-csr重启kubelet本次故障属于“真NotReady”因dmesg和iostat提供了硬件级证据非软件配置问题。5.4 故障期间能否登录节点安全边界在哪里一个关键实操问题当节点NotReady时还能否SSH登录答案是通常可以但必须极度谨慎。可以登录的原因SSH服务sshd运行在用户态不依赖kubelet或containerd只要内核和网络栈正常SSH端口22就可响应禁止操作的红线❌ 不要执行rm -rf /var/lib/kubelet/*可能删除正在运行的Pod卷数据❌ 不要执行sudo reboot可能触发磁盘强制卸载导致文件系统损坏❌ 不要在/var/lib/kubelet下创建任何文件IO hang状态下touch操作会永久阻塞。实操心得登录后第一件事是sudo dmesg -T | tail -20和sudo iostat -x 1 5确认是否为存储故障。若是立即退出改用控制平面命令kubectl drain操作把节点当作“只读黑盒”所有写操作都通过apiserver间接完成。6. 工具与参数速查表一份可直接粘贴的排障备忘录6.1 核心命令速查按执行顺序排列步骤命令说明超时建议1. 连通性验证curl -k -m 5 https://apiserver:6443/healthz测试apiserver可达性-m 5设置5秒超时5秒2. 进程状态检查ps aux | grep -E (kubelet|containerd)确认关键进程是否存在10秒3. 磁盘深度检查df -h df -i sudo du -sh /var/lib/kubelet/* /var/lib/containerd/* 2/dev/null | sort -hr | head -3同时检查容量、Inodes、大目录30秒4. 内核错误扫描sudo dmesg -T | grep -i nvme|io|ext4|oom|lockup | tail -15过滤关键内核错误10秒5. 容器运行时探活sudo crictl ps -a | head -5绕过kubelet检查容器状态15秒6. IO性能诊断sudo iostat -x /dev/nvme0n1 1 5 | grep -E (nvme|await|%util)检测IO延迟和利用率5秒6.2 关键参数阈值表生产环境推荐值指标安全阈值危险阈值临界阈值数据来源iostat await(NVMe) 10ms 100ms 1000msiostat -xdmesg media_errors0≥1≥5sudo nvme smart-logdf -iInodes使用率 85%≥90%≥95%df -ifree -h available 1Gi 500Mi 100Mifree -hkubectl get nodes AGE 10s 30s 60skubectl get nodes -o wide提示这些阈值需根据业务负载调整。例如高IO型AI训练集群await安全阈值可设为20ms而低频日志分析集群应设为5ms。6.3 故障分类决策树图文版文字描述当看到NotReady时按此逻辑分支决策开始 │ ├─ Step 1: 能否SSH登录节点 │ ├─ 否 → 检查网络设备、云平台控制台、物理服务器状态跳转至硬件层 │ └─ 是 → 进入Step 2 │ ├─ Step 2: curl -k apiserver是否成功 │ ├─ 否 → 检查节点网络配置、防火墙、DNS网络层故障 │ └─ 是 → 进入Step 3 │ ├─ Step 3: sudo dmesg -T | grep -i error是否有输出 │ ├─ 否 → 检查kubelet配置、证书、资源限制软件层故障 │ └─ 是 → 进入Step 4 │ └─ Step 4: dmesg输出是否含nvme、io、ext4关键词 ├─ 否 → 检查CPU软锁死、内存泄漏内核层故障 └─ 是 → 执行sudo nvme smart-log和iostat确认存储硬件故障 → 隔离节点联系硬件支持这个决策树已在我们团队内部培训中使用新人30分钟内即可掌握主干逻辑。7. 个人实操体会关于“稳定性”的再认识我在某公司负责K8s平台建设三年亲手处理过17次节点级故障从最早的“删节点重建”到现在的“分钟级定位根因”最大的认知转变是稳定性不是靠冗余堆出来的而是靠可观测性挖出来的。这次NVMe故障如果我们在故障前就部署了node-exporter的node_disk_io_time_seconds_total指标并设置告警完全可以在await突破100ms时就收到预警那时磁盘还有救——SMART日志显示media_errors从1增长到127用了整整37小时足够我们从容替换。但现实是我们直到await飙到12秒才行动错过了所有黄金窗口。另一个体会是文档里写的“最佳实践”往往在真实故障中第一个被打破。比如K8s官方文档说“不要修改/var/lib/kubelet位置”但在本次故障中我们正是通过ls -la /var/lib/kubelet/pods/发现所有Pod目录时间戳停在14:22:15才确认IO hang起始时间。所谓“不要碰”是指不要随意移动但深度诊断时必须敢于触碰每一个字节。最后分享一个小技巧每次重大故障解决后我会在集群里部署一个debug-job它会在所有节点上运行dmesg -T | grep -i error并将结果推送到内部ES集群。这样下次再有类似问题不用登录每台机器直接在Kibana里搜nvme.*timeout就能看到全量节点的健康快照。技术没有银弹但把重复劳动自动化就是对抗不确定性的最朴素武器。