news 2026/9/29 18:26:27

K8S节点磁盘写满引发502:原理、排查与处置全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
K8S节点磁盘写满引发502:原理、排查与处置全解析

先扔个场景:大白天线上突然冒出来一片 502,刷新几次又偶尔能通,再刷新又挂了。你第一反应是不是直接翻 Ingress 日志?我以前也这样,后来被现实教育过几次,发现很多 502 根本不是网关的问题,真正的病根在节点上,尤其是节点磁盘空间不足这种“慢性杀手”。这篇文章就把一次 K8S 节点磁盘写满导致 502 的完整排查过程、原理和处置方案拆开讲清楚,适合正在维护 K8S 集群的运维、SRE 和需要自己折腾集群的开发同学参考。

1. 故障现场与排查思路定调

1.1 线上 502 的真实表现

先说现象。那次故障很典型:某个核心服务的域名开始间歇性返回 502,Nginx Ingress 的日志里能看到一堆upstream connect error、connect failed、偶尔还有reset。乍一看像是后端 Pod 挂了,但实际上服务并没有完全挂掉,而是表现出一种“薛定谔的可用”状态——一部分请求能通,一部分直接 502。

这种不确定性的杀伤力比全挂还大。因为全挂了大家第一反应就是看服务本身,而这种间歇性故障会让人在“网络问题”“网关配置”“服务自身问题”之间反复横跳,浪费大量时间。我当时的第一反应也是去看 Ingress 配置、看后端 Service 的 Endpoints 是否正常,绕了半圈才意识到问题出在节点层面。

所以要记住一个判断基准:502 是网关层能拿到的最“模糊”的错误之一,它只告诉你“网关连不上后端,或者后端给了无效响应”。真正的原因可能在应用、可能在网络、可能在运行时,也可能在节点操作系统本身。排障的时候,先把范围扩大,再逐步收窄,不要一上来就扎进某个具体组件里。

1.2 先别急着查日志,理顺排查顺序

分布式系统的故障排查有一条铁律:从底层往上层查,从系统到容器再到应用。道理很简单,底层出了问题,上层所有表现都是“并发症”。如果你盯着应用日志找原因,看到的多半是“连接被拒绝”“写入超时”“OOM”这类莫名其妙的报错,反而容易被带着跑偏。

我自己总结了一套快速体检的顺序,上线两年多基本没变过:

  1. 先看集群层:kubectl get nodes、kubectl describe node,确认节点是否 Ready、是否有异常 Condition。
  2. 再看资源层:df -h、free -g、uptime,确认节点 CPU、内存、磁盘、inode 有没有明显异常。
  3. 再看运行时层:crictl ps -a或者docker ps -a,确认容器能不能正常创建和启动。
  4. 再看编排层:kubelet 日志里有没有驱逐记录、调度失败记录。
  5. 最后才看应用层:业务日志、探针状态。

正常这套流程跑一遍也就两分钟,但能帮你省下后面至少两小时的迷茫。

2. 磁盘空间不足是如何一步步演变成 502 的

2.1 Kubelet 的驱逐机制:不是“罪魁祸首”,而是“保护者”

磁盘空间不足导致故障,最核心的机制就是 Kubelet 的驱逐机制。Kubelet 会持续监控节点上的资源使用情况,一旦超过你设定的阈值,就会给节点打上DiskPressure之类的 Condition,并尝试驱逐(Eviction)一些 Pod 来释放空间。

Kubernetes 的驱逐阈值通常由 kubelet 的这几个参数控制:--eviction-hard、--eviction-soft、--eviction-minimum-reclaim。比较常见的默认配置类似这样:

--eviction-hard=imagefs.available<15%,nodefs.available<10%

含义是:当镜像文件系统的可用空间低于 15%、节点文件系统的可用空间低于 10% 时,kubelet 会进入磁盘压力状态并开始驱逐 Pod。

这里有一个关键认知:即便你的 Pod 请求和限制都设置得很小,它也可能被驱逐。驱逐的顺序优先级是:BestEffort 优先被驱逐,然后是 Burstable,最后才是 Guaranteed。也就是说,那些没有设置资源请求/限制的 Pod 最容易被“牺牲”。所以磁盘一旦告急,最先倒下的往往是你最没保护的 Pod。

另一个容易被忽略的点是:被驱逐的 Pod 不会自动重新调度。它会处于Evicted状态,除非它由 Deployment、StatefulSet 这类控制器管理,控制器才会帮你重建一个 Pod,但新建的 Pod 如果还是被调度回这个磁盘已满的节点,依然会失败或再次被驱逐,形成循环。

2.2 磁盘满之后的连锁反应

磁盘满不只是驱逐 Pod 这么简单,它会触发一整串连锁反应,我看着像一张多米诺骨牌:

第一张牌是容器运行时无法正常工作。containerd 或 docker 需要写日志、拉镜像、解压镜像层、创建容器可写层,这些全部依赖磁盘空间。磁盘一满,容器启动直接报错,日志里会出现no space left on device。

第二张牌是现有容器进入亚健康状态。进程还活着,但日志写不进去、临时文件创建失败、打开的文件描述符无法正常写入,业务表现为“请求卡住、超时、频繁报错”。这时候如果探针只是检查 TCP 端口或 HTTP 状态码,很可能还能通过,但真正的业务逻辑已经坏了。Ingress 把流量转发过来,应用进程接不住,或者反回几个不完整的响应,网关层自然就表现为 502 或 504。

第三张牌是 Pod 删除卡住。Kubernetes 删除 Pod 时,容器运行时需要停止容器、清理挂载和目录。磁盘写满时这些清理操作同样会超时或失败,导致 Pod 一直处于Terminating状态,Endpoints 控制器可能来不及把不可用的后端摘除,流量继续打进一个实际上已经不可用的 Pod,然后又是 502。

如果 etcd 或者 API Server 恰好也跑在同一个磁盘爆满的节点上,那就更热闹了——整个集群的读写都会出问题,甚至kubectl get nodes都卡住。所以磁盘满这件事,从“局部服务 502”到“整个集群不可用”,只有一步之遥。

2.3 最容易忽略的“隐形磁盘占用”

这是个非常实操的细节,我必须单独拉出来说。很多时候df -h显示磁盘已经 100%,但你在根目录下du -sh *一通扫,发现占用加起来和磁盘容量对不上。这种“空间神秘消失”的错觉,通常来自三类情况:

第一类是被删除但仍然被进程占用的文件。比如某个应用打开了一个大日志文件,然后被rm删掉了,但进程没释放文件描述符。这时候文件占用的空间不会消失,直到进程退出或文件句柄被关闭。排查方法是用lsof或者直接看/proc:

lsof -nP | grep '(deleted)'

或者按占用大小排序:

find /proc/*/fd -type l -lname "*(deleted)*" 2>/dev/null | xargs -I {} ls -la {} 2>/dev/null | sort -k5 -rn | head

这类文件经常出现在 Java 应用中、MySQL 临时文件、容器日志被 logrotate 轮转但容器还在写的情况下。

第二类是容器日志本身。标准 Docker/containerd 的 json-file 日志驱动会把所有容器 stdout 日志写到宿主机文件里,位置大概在/var/lib/docker/containers/<container_id>/或者/var/lib/containerd/下,文件名通常是<container_id>-json.log。单个容器如果疯狂打日志,一天写几十 GB 完全可能。

第三类是容器运行时和镜像层缓存。镜像本身占空间,还得加上 overlay 的可写层、临时挂载层。尤其是频繁发布、构建新镜像的集群,旧镜像不清理的话,/var/lib/docker或/var/lib/containerd膨胀速度非常快。

除了这些,systemd journal 日志也经常被忽视。长期运行的机器上/var/log/journal可能攒了几个 G 甚至几十 G 的日志。很多发行版默认不限制 journal 大小,这是节点磁盘的一颗定时炸弹。

3. 完整排查实操:按步骤抄作业

3.1 三分钟定位磁盘异常节点

进入现场后别慌,先用 kubectl 快速扫一遍集群状态。虽然集群规模大的时候节点可能很多,但故障节点通常会有明显的“异常标签”。

第一步,看节点状态:

kubectl get nodes -o wide

重点关注STATUS列有没有NotReady、SchedulingDisabled、或者Ready后面带着,DiskPressure之类的 Condition。

第二步,看详细 Condition:

kubectl describe node <node-name>

在输出里直接搜Conditions段落,找DiskPressure这一行是否为True。如果为True,基本可以确定问题方向了。

第三步,看集群里有没有大量异常 Pod:

kubectl get pods -A | grep -E "Evicted|Error|CrashLoopBackOff|ContainerCreating"

正常情况下集群里有一些 Evicted Pod 不算稀奇,但如果你发现几十个 Pod 全是 Evicted 且集中在同一个节点,那几乎就是磁盘问题的铁证。

第四步,看服务的后端列表:

kubectl get endpoints <service-name> -n <namespace> kubectl get endpointslices -n <namespace> -l kubernetes.io/service-name=<service-name>

对比一下可用的 Endpoints 数量和期望的副本数,如果少了一大截,502 的根因就清楚了。

3.2 上机后的磁盘体检手册

确定可疑节点后,登上去做深度检查,我习惯按这套顺序来:

df -h df -i

df -h看空间,df -i看 inode。inode 耗尽是个小众但致命的坑,磁盘空间明明还剩不少,但文件系统无法创建新文件,现象和磁盘满一模一样。尤其是有大量小文件的目录(比如容器运行时的 overlay 层),很容易把 inode 吃完。

接着按占用从高到低扫目录:

du -h --max-depth=1 /var/lib 2>/dev/null | sort -hr | head du -h --max-depth=1 /var/log 2>/dev/null | sort -hr | head

再单独看容器运行时目录和日志目录的大小:

du -sh /var/lib/containerd /var/lib/docker 2>/dev/null journalctl --disk-usage

最后一步,用前面提到的方式查 deleted 文件:

lsof -nP 2>/dev/null | grep '(deleted)' | awk '{print $7, $1, $2, $4}' | sort -rn | head -20

lsof输出的第 7 列是文件大小,把它排个序,能直接揪出那些“占着茅坑不拉屎”的删除文件。

我当时在那台故障节点上跑完这套命令,很快看到/var/lib/containerd占了超过 60G,journal 占了 10G 多,此外还有一个 MySQL 实例的临时文件被删掉了但进程句柄还开着,占了接近 20G。这几个加起来,直接把根分区顶爆了。

3.3 结合 kubelet 日志确认驱逐事件

上机之后不要光顾着清磁盘,先把“为什么变成这样”的证据保留下来。kubelet 的日志里会留下明确的驱逐记录,方便你后面复盘和写报告。

不同发行版 kubelet 日志位置不一样。systemd 管理的用 journalctl:

journalctl -u kubelet --since "2 hours ago" | grep -iE "evict|disk|pressure"

如果是二进制方式部署的,去/var/log/kubelet.log或者/var/log/kubernetes/kubelet.log里搜:

grep -iE "evict|disk pressure|imagefs" /var/log/kubelet.log | tail -100

能看到类似这样的记录:

eviction_manager: eviction thresholds have been met, evicting Pod eviction_manager: pod "xxx" evicted due to nodefs pressure

这些日志信息量很大,能帮你确认两个关键事实:一是磁盘压力是从什么时候开始的,二是哪些 Pod 是被驱逐的,哪些是自己挂掉的。

4. 应急恢复与根源治理

4.1 应急三板斧:先把空间释放出来

应急阶段的目标不是“优雅地根除”,而是“尽快恢复可用性”。释放空间有三个高性价比的切入点,按我个人的偏好排序:清 journal → 清容器日志 → 清旧镜像。

清 journal 是最安全的,不会影响任何运行中的容器。注意大型环境里,还是设置合理的保留时间或大小上限,通常保留两三天就足够了:

journalctl --vacuum-size=500M journalctl --vacuum-time=3d

然后清理容器日志。这一步操作不复杂,但要知道自己删的是什么。旧容器日志文件一般躺在/var/log/containers/和/var/lib/docker/containers/下。可以直接 truncate,也可以删除,但要确认对应的容器已经退出,或者你能接受运行中的容器写日志到原句柄而文件被删干净的结果。我更建议用 truncate 而不是rm:

find /var/log/containers -name "*.log" -size +100M -exec truncate -s 0 {} \;

原因是运行时可能还持有打开的文件句柄,truncate 之后容器还能继续写,只是旧数据被清空;直接rm反而会让文件句柄继续占空间,除非容器重启。这也是很多人删了日志但df -h没变化的原因。

最后清旧镜像。containerd 用crictl:

crictl rmi --prune

docker 用:

docker image prune -a --filter "until=24h"

注意--prune只删未被容器使用的悬空镜像,-a会删除所有未被容器引用的镜像。稳妥起见,生产环境可以先只 prune 悬空镜像,确认空间还是不够再上-a。

我这里有个小经验:清理镜像要按 tag 维度慎用,尤其注意那些“多个 tag 指向同一个 image id”的镜像。只删 tag 而不删底层镜像层,空间释放有限;真正占空间的其实是共享的镜像层,而image prune -a才能真正把未被引用的层删掉。

紧急情况下,如果空间释放后节点还是不 Ready,再考虑重启 kubelet。但重启 kubelet 会让节点上的 Pod 经历一轮重新连接或重建,是一个有“动静”的操作,最好先确认关键业务都在多副本并且在别的节点有可用备份,再动手。

4.2 配置层面根治:别让磁盘再满

应急恢复只是治标,根子在配置和管理上。我梳理了几件非常值得做的事:

第一件是独立分区。把/var/lib/containerd或/var/lib/docker、/var/log、/var/lib/etcd分开挂载,避免一个目录把整个根分区顶爆。这在部署 K8S 前就应该规划好。

第二件是配置日志轮转。给容器运行时加日志上限是很有效的防护手段。拿 containerd 来说,需要修改/etc/containerd/config.toml里的配置:

[plugins."io.containerd.grpc.v1.cri"] max_container_log_line_size = -1 [plugins."io.containerd.grpc.v1.cri".containerd] default_runtime_name = "runc" [plugins."io.containerd.grpc.v1.cri".log] max_relative_size = "20MiB" max_files = 5

如果集群还是老的 Docker 运行时,那就配置 docker daemon:

{ "log-driver": "json-file", "log-opts": { "max-size": "20m", "max-file": "5" } }

如果不同 Pod 想用不同的日志上限,可以通过 Pod 的 annotation 控制。

第三件是配置合理的驱逐阈值。默认的 10%/15% 对生产环境来说有点紧张,因为磁盘满的“惯性”很大,等降到 10% 再驱逐,可能容器运行时已经没法正常干活了。我建议把硬阈值适当调高一点,比如:

--eviction-hard=nodefs.available<15%,imagefs.available<15%

同时配合--eviction-minimum-reclaim=nodefs.available=5%,imagefs.available=5%,让驱逐之后至少能回收出 5% 的缓冲空间。这种配置能让 kubelet 在更早、更从容的情况下采取行动。

第四件是加监控告警。这一步的价值在故障发生的时候体现得最彻底。用 Prometheus + node_exporter + Alertmanager + Grafana 是很常见的组合,磁盘相关的核心指标就两类:

node_filesystem_avail_bytes node_filesystem_size_bytes

告警表达式可以直接这样配:

- alert: NodeDiskSpaceLow expr: (node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"} / node_filesystem_size_bytes{fstype!~"tmpfs|overlay"}) < 0.2 for: 10m labels: severity: warning annotations: summary: "节点磁盘空间不足"

我的习惯是:根分区可用率低于 20% 发 warning,低于 10% 发 critical。别等到 5% 才告警,那时候已经到晚高峰期了,手忙脚乱找应急手段的滋味不好受。

4.3 面向未来的容量规划与故障演练

配置改完了,还有两块比较“偏软”但同样重要的事情——容量规划和故障演练。

容量规划的核心思路是:磁盘空间的增长是确定性的,只要你有监控数据,就能预测它什么时候会满。比如容器日志平均每天涨 5G,镜像缓存每周发布涨 8G,那你至少应该给根分区预留出未来 3-6 个月的余量。云环境下直接扩容云盘是最通用的方案,虚拟机的数据盘扩容一般几步就能完成;物理机则需要提前规划好 RAID 或 LVM 的扩展空间。

如果集群节点本身是弹性伸缩的(比如通过 node group 或 cluster autoscaler),还可以给磁盘使用率配一个节点扩容的策略,让新节点在旧的快满之前加进来。

故障演练这件事,很多团队觉得“没必要”“太麻烦”,但每次出问题时它们就后悔没做过。演练场景不需要很复杂,最基础的就是“把某个节点的磁盘写满,观察会不会自动恢复、告警是否及时、是否有应急预案可用”。真演练过一次之后,你会对 Kubelet 的驱逐、Pod 重建、监控告警的响应速度都有直观的体感,这些是看文档学不来的。

5. 常见问题速查与排障锦囊

5.1 高频故障速查表

这部分我整理成一张排查速查表,平时我贴在自己工作笔记的第一页:

症状可能原因快速定位命令处置方式
多个 Pod 集中被驱逐节点磁盘压力,触发 kubelet 驱逐kubectl describe node查看 DiskPressure清理空间、调整驱逐阈值
df -h显示满但目录不占被删除但进程还占用的文件lsof -nP grep deleted重启相应进程或截断文件
单个容器写 GB 级日志容器日志无大小限制du -sh /var/lib/docker/containers配置日志轮转、清空超大日志
inode 耗尽海量小文件占满 inodedf -i清临时文件、重建目录
Pod 一直Terminating容器停止卡住在写盘journalctl -u kubelet释放磁盘后强制删除 Pod
磁盘没满但 Pod 仍被驱逐达到驱逐阈值但还有大量未回收空间检查eviction-minimum-reclaim和镜像层占用清理 imagefs、调整回收阈值

5.2 几条独家经验,踩过才懂

先唠一个“表面合理但实际有害”的操作:很多新人在磁盘满的时候,第一反应是rm -rf /var/lib/docker或者rm -rf /var/lib/containerd。这个操作会直接毁掉节点上所有容器的运行状态,导致大量 Pod 不可用。如果不是到了集群马上要崩的绝境,不要碰这个目录,宁可先清日志和镜像。

再说说“被驱逐的 Pod 不会自动重建”这个误会。被驱逐的 Pod 如果有控制器(比如 Deployment),控制器确实会重建它,但重建出来的 Pod 可能又调度回原节点,又因为磁盘满而失败。所以每次驱逐之后,盯着看新 Pod 是否成功 Running 非常重要,不要以为“驱逐完就万事大吉”。

关于持久化配置驱逐阈值,也要注意一点:很多发行版用 kubelet 的 systemd 方式,你把参数加到配置文件里之后需要重启 kubelet 生效。所以生产环境调整驱逐阈值要选在低峰期,并确认重启对存量 Pod 的影响。这方面做不好,容易从磁盘故障引发“次生灾害”。

还有一个细节容易被忽略:docker system prune或crictl rmi --prune并不会清理掉仍在被使用的镜像,但如果你先删除了 Pod,后面再清理镜像,逻辑上没问题;不过在运行中的节点上大批量清理镜像,可能会影响正在启动的 Pod 从 containerd 拉取镜像时的缓存信息,所以尽量分批、打时间间隔执行。

最后补一个个人习惯:我在看节点磁盘的时候,永远会同时df -h和df -i。每一次磁盘告警背后,不一定都是空间不够,inode 耗尽同样会出现一样的业务现象,但它排查起来更容易让人掉头发。

6. 结尾再补两句掏心窝的话

我自己经手过三四次类似的磁盘故障之后,最大的改变不是记牢了命令,而是开始敬畏“默认配置”。Kubernetes 的默认驱逐阈值是按“能跑”设计的,不是按“跑得好”设计的,生产环境必须主动调优。日志轮转、磁盘监控、独立分区这些事,看起来琐碎,但任何一个缺失都可能在未来某个周四下午给你一个大惊喜。

再送一个小技巧:如果你在排查中发现某个节点反复因为磁盘被驱逐 Pod,但又找不到具体是哪个应用在疯狂写盘,可以批量统计一下容器日志的增长速率。最简单的办法是隔五分钟跑一次du -sh /var/log/containers,看看差值,增长最快的那几个日志文件名对应的 Pod 就是“嫌疑犯”。这招虽然笨,但在生产环境里非常顶用。

故障排查从来都是一个“从模糊到精确”的收敛过程,K8S 节点磁盘不足导致 502 只是众多连锁故障中的一种,但只要理解了 kubelet 驱逐、容器运行时依赖磁盘这些底层逻辑,再遇到类似的“网关报错但网关无辜”的情况,你就能更快找到真正的病根。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 18:26:13

UE5 Slate与UMG底层机制解析:Widget生命周期与渲染管线

1. 为什么UE5的UMG/Slate不是“另一个Vue”——从热词误判切入的真实定位最近在几个技术社区里反复看到一句高频吐槽&#xff1a;“vue3引入所有的ui框架都不生效”&#xff0c;紧接着就有人把这句话生搬硬套到Unreal Engine上&#xff0c;发帖问“UMG是不是也像Vue3一样突然不…

作者头像 李华
网站建设 2026/9/29 18:26:13

大模型重构货运广告链路:货拉拉营销文案生成与智能投放实践

我刚接手“大模型在货拉拉营销广告的应用实践”这个项目时&#xff0c;心里其实没底。货拉拉的营销场景和传统电商完全不一样&#xff1a;用户不是“逛”出来的&#xff0c;而是被“要搬家、要拉货、要发急件”这种确定性需求推过来的。广告物料既要打动货车司机&#xff0c;又…

作者头像 李华
网站建设 2026/9/29 18:25:56

C#宿舍管理系统开发实战:表结构设计、WinForms实现与避坑指南

简介&#xff1a;一份面向C#课程设计场景的宿舍管理系统完整源码包&#xff0c;以Visual Studio项目为主体&#xff0c;配套文档、流程图与SQL数据库脚本&#xff0c;适用于需要完成同类课程设计或进行WinForm开发练习的初学者。系统按学生与宿管双角色设计&#xff0c;覆盖公告…

作者头像 李华
网站建设 2026/9/29 18:25:28

拟南芥根尖scATAC-seq实操指南:从染色质可及性到细胞类型注释

1. 这不是“高通量测序入门课”&#xff0c;而是一份根尖细胞核里真实发生的染色质松动地图 scATAC-seq——单细胞染色质可及性测序&#xff0c;这个词听起来像实验室黑板上的一行公式&#xff0c;但落到拟南芥根尖上&#xff0c;它讲的是一个活生生的生物学故事&#xff1a;当…

作者头像 李华
网站建设 2026/9/29 18:25:13

AgentScope实战指南:核心机制、Java 2.0与RAG服务化

1. 为什么我要把AgentScope放进推荐清单最近在选多智能体框架&#xff0c;前前后后对比了LangChain、CrewAI、AutoGen&#xff0c;还有微软的Semantic Kernel&#xff0c;最后让我停下脚步的是AgentScope。先说结论&#xff1a;如果团队里有人问你"多智能体项目该用什么框…

作者头像 李华
网站建设 2026/9/29 18:25:11

Unity解密游戏期末大作业:交互闭环与谜题机制实现指南

简介&#xff1a;这是一份面向Unity学习者的期末大作业参考包&#xff0c;聚焦解密类游戏从设计到实现的完整流程&#xff0c;适合K12阶段学生、高校选修课学员及初次尝试游戏开发的新手。这类游戏通常通过观察、推理和实验来破解谜题&#xff0c;因此项目中特意强化了关卡设计…

作者头像 李华