集群节点出现卡顿或者频繁告警的时候,最容易犯的错误是一上来就盯住 CPU 使用率。CPU 高确实是一种异常,但很多时候 CPU 只是表象,真正的压力可能来自磁盘排队、内存回收、句柄耗尽或者网络软中断。系统状态检查要想做得有效率,必须有一套从底层到上层的排查顺序,先分清是节点问题还是集群调度问题,再根据资源类型逐一排除。

从系统层快速判断节点是否处于健康状态
任何集群排障的第一步都应该回到节点本身。无论上层是 Kubernetes 还是自研调度系统,底层操作系统一旦出现资源争抢,所有容器和进程都会被拖慢。一个比较高效的切入方式是使用 uptime 查看负载平均值,再结合 CPU 核心数来判断负载是否真的过高。很多人误以为负载均值超过 1 就是有问题,实际上八核机器在负载 6 的时候依然可以正常工作,重点是观察趋势而非绝对值。如果负载在持续爬升,即使当前数值不高,也要警惕演进中的资源瓶颈。
接下来需要把内存和磁盘的检查做细。建议按如下顺序执行一组命令,每条命令解决一个维度的疑问:内存是否够用、内存是否发生换页、磁盘是否繁忙、inode 是否耗尽。使用 free -h 观察 available 列,而不是只盯着 total 和 used,因为 Linux 内核会把空闲内存用作 page cache,used 偏高并不必然代表内存紧张。真正需要警惕的是 swap 的使用量,如果 swap 持续增长且 si/so 列长期不为零,说明内存回收压力已经传导到了磁盘换页层面。
# 查看系统负载均值,判断短时与长期趋势 uptime # 查看内存总量、可用量与 swap 使用情况 free -h # 查看内存换页、CPU 上下文切换、IO 等待等关键指标 vmstat 1 5 # 查看磁盘使用率,同时关注 inode 剩余量 df -h df -i # 查看块设备级别的读写吞吐与等待时间 iostat -x 1 3
执行完上述命令后,需要重点分析 iostat 输出中的 %util 与 await 两列。如果 %util 已接近 100% 而 await 也很高,说明磁盘确实成为瓶颈。如果是 SSD 阵列,%util 达到 100% 时往往伴随明显的写放大效应,此时需要检查是否存在日志型应用在频繁刷盘。对于使用网络存储的集群节点,还要关注 r_await 与 w_await 的差距,读等待显著高于写等待,通常意味着存储服务端或网络链路存在问题。
文件句柄数也是一个极易被忽略的指标。容器化应用在频繁创建连接或打开文件时,如果宿主机的 fs.file-max 设置偏小,会引发 Too many open files 一类的报错。此时建议使用 sysctl fs.file-nr 查看已分配句柄数、未使用句柄数与最大句柄数,同时使用 lsof -p 定位到具体进程的句柄占用情况。这里需要特别提醒:句柄数检查不区分进程版本,无论是 Java 应用还是 Nginx 网关,只要句柄耗尽都会导致新连接无法建立。
从集群视角检查资源水位与调度异常
系统层的资源状态确认无误后,下一步要切换到集群视角。在 Kubernetes 环境中,可使用 kubectl describe node 查看节点的资源分配情况,重点观察 Allocated resources 部分。很多实际案例表明,节点的分配率并未达到 100%,但 Pod 依然出现 CPU 节流,这通常是因为可压缩资源的 limit 值导致 cgroup 层面对 CPU 的配额限制。此时需要计算节点的 CPU 请求总量与可分配量之间的关系,并判断是否存在大量的 Burstable Pod 在争抢 CPU 时间片。
除了资源分配率,节点状态与污点也同样值得关注。如果一个节点长时间处于 NotReady 状态,优先排查 kubelet 服务是否存活,以及容器运行时(如 containerd 或 dockerd)是否发生假死。而如果节点显示 MemoryPressure 或 DiskPressure 状态,则说明节点已触发驱逐条件,kubelet 会根据 QoS 等级优先停止 BestEffort 类型的 Pod。理解驱逐机制有助于快速判断哪些 Pod 会先被杀掉,从而确定保护关键业务的方法。
# 查看节点整体状态与污点信息 kubectl get nodes -o wide # 查看节点的资源分配率、条件状态与驱逐信号 kubectl describe node node-01 # 查看 kubelet 服务的运行状态与最近日志 systemctl status kubelet journalctl -u kubelet --since "10 minutes ago" --no-pager | grep -i evict # 查看节点上的全部 Pod 及资源实际使用情况 kubectl top node # 查看节点各 Pod 的资源消耗排名 kubectl top pod -A --sort-by=cpu | head -20
需要说明的是,kubectl top 依赖 metrics-server 提供数据,如果该组件不可用则无法执行。在没有 metrics-server 的环境里,可以通过 kubectl describe node 中记录的 Non-terminated Pods 数量以及请求量做粗略评估。更精确的做法是登录节点,使用 systemd-cgtop 查看 cgroup 级别的资源消耗,这比单纯依赖 kubectl top 看到的数据更接近真实宿主机视角。如果多个节点同时出现内存紧张,还要检查 kubelet 的 --system-reserved 与 --kube-reserved 参数是否预留了足够资源给系统进程。
对于大规模集群,节点状态检查不能单靠人工执行命令,建议编写一个简洁的巡检脚本,定期采集节点的负载、内存、磁盘与 kubelet 状态。将脚本输出到文件或接入监控系统后,后续排查时只需对比历史趋势,就能快速发现资源使用率的突变点。巡检脚本本身不必过于复杂,可以按如下模式实现,将各维度数据输出为带时间戳的文本记录。
#!/bin/bash # 定期采集节点核心指标,用于集群资源趋势分析 while true; do echo "==== $(date '+%Y-%m-%d %H:%M:%S') ====" uptime free -m | awk 'NR==1 || NR==2' df -h / /var/lib/docker 2>/dev/null cat /proc/loadavg sleep 60 done
深入工作负载层定位反复重启与启动失败问题
当节点基础资源和调度状态都没有明显异常时,问题往往隐藏在工作负载本身。以 Kubernetes 为例,容器反复进入 CrashLoopBackOff 或长时间处于 Pending 状态,其背后的原因不尽相同。对于 Pending 状态的 Pod,先检查是否存在未满足的节点选择器、资源请求大于节点可分配量、或者存储卷无法挂载等调度限制。对于 CrashLoopBackOff,则需要拿到容器日志和退出码,退出码 137 通常代表被 OOM Killer 杀死,而 143 往往与 SIGTERM 有关。
镜像拉取失败是另一个高频问题。特别在离线环境或使用私有镜像仓库时,ImagePullBackOff 错误会反复出现。处理逻辑很简单:先确认镜像标签是否存在、仓库地址是否可达,再检查节点上的容器运行时凭据配置是否正确。使用 crictl images 与 crictl pull 可以绕过 Kubernetes 层直接测试容器运行时的拉取能力,这有助于快速区分是 kubelet 配置问题还是运行时网络问题。
# 查看 Pod 运行状态与所在节点 kubectl get pods -o wide -A | grep -E 'Pending|CrashLoop' # 查看 Pod 详细事件,关注调度失败或拉取失败原因 kubectl describe pod <pod-name> -n <namespace> # 查看容器上一次退出的日志 kubectl logs <pod-name> -n <namespace> --previous # 进入容器后查看进程树与资源占用 kubectl exec -it <pod-name> -n <namespace> -- /bin/sh # 检查容器运行时侧的镜像与容器状态 crictl ps -a | head -20 crictl images | grep <image-name> crictl logs <container-id>
容器内部视角与宿主机视角存在明显差异。容器内执行 free -m 看到的内存数据实际上反映的是宿主机的全局内存,而不是容器自身的限制值。正确的方式是读取容器的 cgroup 文件,例如 /sys/fs/cgroup/memory.max 与 /sys/fs/cgroup/memory.current,这些文件记录的是容器真实的内存上限与当前使用量。对于 CPU 同样如此,cat /sys/fs/cgroup/cpu.max 可以查看容器获得的 CPU 配额,据此判断是否因配额过小而导致容器内线程频繁被节流。
在排查 Pod 反复重启的同时,还要注意一个容易忽略的细节:容器内的时区与日志时间戳。很多应用在容器中默认使用 UTC 时间,导致日志时间与监控系统的本地时间对不上,排障时极容易误判事件发生的先后顺序。建议在描述故障时间线时先统一时区口径,将容器日志与节点内核日志的时间戳放在同一参照系下对比。另外,journalctl -k 中的内核日志往往带有 OOM 事件、驱动报错和文件系统异常信息,这些线索对定位系统级故障有决定性作用。
从事件与指标联动中还原故障根因
集群故障很少是单一维度造成的,事件与指标联动分析才能准确还原当时的系统状态。举例来说,节点负载突然飙升时,如果只查看监控系统中的 CPU 曲线,可能只能看到一条平滑上升的线,却无法解释上升的原因。此时需要将节点事件、Pod 调度记录、内核日志以及应用日志四类数据放在同一时间轴上来对照分析。如果监控系统支持标签关联,优先以节点名为维度拉取所有关联数据,而不是单独查看某一个指标。
下面给出一个实战场景:某集群中的一个节点频繁出现 NotReady,每次持续时间约为两分钟后自动恢复。初步检查 CPU 和内存均无异常,但在内核日志中发现 blocked for more than 120 seconds 的提示,说明内核线程被磁盘 I/O 阻塞。进一步排查后发现,该节点存在一个数据清理任务,短时间内产生大量随机写操作,导致磁盘响应时间飙升。这里真正的根因是磁盘 I/O 排队,而不是 CPU 或内存容量不足。
# 查看内核日志中因 IO 阻塞导致的异常事件 journalctl -k --since "1 hour ago" | grep -i "blocked for more than" # 查看进程的 IO 等待时间与磁盘读写速率 pidstat -d 1 10 # 查看系统平均负载与 CPU 上下文切换的高频指标 vmstat 1 10 # 查看进程级别的上下文切换与运行队列 cat /proc/<pid>/status | grep -E "voluntary|nonvoluntary"
从这个案例可以提炼出一个更为通用的排障思路:不要只做一个统计员,要做一个侦探。每看到一个异常指标,都要追问这个指标的上游是什么。CPU 高要问是哪类进程消耗的,内存高要问是 page cache 还是匿名页占用的,磁盘繁忙要问是读多还是写多、对应哪个应用。只有把指标层层下钻到进程和线程级别,才能把集群资源的排查工作做到真正闭环。
在联动分析过程中,dmesg 也是一个不可忽视的信息来源。内核在检测到 OOM 时会在内核环形缓冲区中记录被终止进程的名称、PID 和内存占用。当 K8s 的 OOMKilled 事件与内核日志的 OOM 记录同时出现时,可以迅速锁定目标进程。需要留意的是,内核日志记录的内容在容器运行时层面可能不会完整呈现给应用,因此需要在宿主机上直接查看。将 dmesg 输出按时间保存为文件,并与容器重启时间对比,是还原根因的常用做法。
把被动救火转化成常态巡检
排障工作如果只停留在故障处理层面,集群的稳定性很难有本质提升。一个成熟的集群运维体系应该把上述排查思路定期化、自动化。例如每周检查一次节点文件句柄数的增长趋势,每天对 kubelet 的错误日志做关键词告警,定期对内核日志中的异常信息做归档分析。主动巡检能帮助团队在资源耗尽之间发现问题,避免将故障放大到用户可感知的层面。
巡检脚本需要结合集群自身特征来定制。例如以运行大数据任务为主的集群,需要重点监控磁盘吞吐和临时目录的空间使用情况;而以在线 Web 服务为主的集群,则要关注连接数、句柄数与 CPU 节流情况。以下巡检脚本模板按天维度运行,将关键数据记录到日志文件,供后续趋势分析使用,同时满足轻量与实用的要求。
#!/bin/bash # 集群节点日常巡检脚本,建议配合 crontab 每日执行 LOG_DIR="/var/log/cluster-inspect" mkdir -p $LOG_DIR DATE=$(date '+%Y%m%d') LOG_FILE="$LOG_DIR/inspect-$DATE.log" echo "==== System Load ====" > $LOG_FILE uptime >> $LOG_FILE echo "==== Memory ====" >> $LOG_FILE free -m >> $LOG_FILE echo "==== Disk Usage ====" >> $LOG_FILE df -h >> $LOG_FILE echo "==== Inode Usage ====" >> $LOG_FILE df -i >> $LOG_FILE echo "==== Top 10 CPU Processes ====" >> $LOG_FILE ps -eo user,pid,pcpu,pmem,comm --sort=-pcpu | head -11 >> $LOG_FILE echo "==== Kubelet Status ====" >> $LOG_FILE systemctl is-active kubelet >> $LOG_FILE echo "==== Recent Kernel Errors ====" >> $LOG_FILE journalctl -k --since "24 hours ago" -p err --no-pager | tail -50 >> $LOG_FILE
最后,还是想强调文档沉淀的重要性。每一次集群资源排障结束之后,都应该把完整的排查过程、关键命令输出、根因分析结论整理成排障文档。长期积累下来,这些文档就是团队最宝贵的知识库。当后续再次出现类似问题时,可以直接检索文档中的关键词,快速匹配到相似的故障特征,从而把平均恢复时间控制在分钟级别。资源检查的目的从来不是看数据,而是通过数据读懂系统当前的状态,并预判下一步可能发生的问题。
集群规模越大,资源状态的复杂度就越高,仅靠单一的监控面板无法覆盖所有视角。将节点系统命令、Kubernetes 对象状态、cgroup 限制、内核日志和进程调度信息结合起来,构建一套完整的检查路径,才能真正掌握集群的实时健康度。对任何一位运维工程师或 SRE 来说,这套方法论比死记硬背某条命令更能应对多变的故障场景。