
先扔个场景大白天线上突然冒出来一片 502刷新几次又偶尔能通再刷新又挂了。你第一反应是不是直接翻 Ingress 日志我以前也这样后来被现实教育过几次发现很多 502 根本不是网关的问题真正的病根在节点上尤其是节点磁盘空间不足这种“慢性杀手”。这篇文章就把一次 K8S 节点磁盘写满导致 502 的完整排查过程、原理和处置方案拆开讲清楚适合正在维护 K8S 集群的运维、SRE 和需要自己折腾集群的开发同学参考。1. 故障现场与排查思路定调1.1 线上 502 的真实表现先说现象。那次故障很典型某个核心服务的域名开始间歇性返回 502Nginx Ingress 的日志里能看到一堆upstream connect error、connect failed、偶尔还有reset。乍一看像是后端 Pod 挂了但实际上服务并没有完全挂掉而是表现出一种“薛定谔的可用”状态——一部分请求能通一部分直接 502。这种不确定性的杀伤力比全挂还大。因为全挂了大家第一反应就是看服务本身而这种间歇性故障会让人在“网络问题”“网关配置”“服务自身问题”之间反复横跳浪费大量时间。我当时的第一反应也是去看 Ingress 配置、看后端 Service 的 Endpoints 是否正常绕了半圈才意识到问题出在节点层面。所以要记住一个判断基准502 是网关层能拿到的最“模糊”的错误之一它只告诉你“网关连不上后端或者后端给了无效响应”。真正的原因可能在应用、可能在网络、可能在运行时也可能在节点操作系统本身。排障的时候先把范围扩大再逐步收窄不要一上来就扎进某个具体组件里。1.2 先别急着查日志理顺排查顺序分布式系统的故障排查有一条铁律从底层往上层查从系统到容器再到应用。道理很简单底层出了问题上层所有表现都是“并发症”。如果你盯着应用日志找原因看到的多半是“连接被拒绝”“写入超时”“OOM”这类莫名其妙的报错反而容易被带着跑偏。我自己总结了一套快速体检的顺序上线两年多基本没变过先看集群层kubectl get nodes、kubectl describe node确认节点是否 Ready、是否有异常 Condition。再看资源层df -h、free -g、uptime确认节点 CPU、内存、磁盘、inode 有没有明显异常。再看运行时层crictl ps -a或者docker ps -a确认容器能不能正常创建和启动。再看编排层kubelet 日志里有没有驱逐记录、调度失败记录。最后才看应用层业务日志、探针状态。正常这套流程跑一遍也就两分钟但能帮你省下后面至少两小时的迷茫。2. 磁盘空间不足是如何一步步演变成 502 的2.1 Kubelet 的驱逐机制不是“罪魁祸首”而是“保护者”磁盘空间不足导致故障最核心的机制就是 Kubelet 的驱逐机制。Kubelet 会持续监控节点上的资源使用情况一旦超过你设定的阈值就会给节点打上DiskPressure之类的 Condition并尝试驱逐Eviction一些 Pod 来释放空间。Kubernetes 的驱逐阈值通常由 kubelet 的这几个参数控制--eviction-hard、--eviction-soft、--eviction-minimum-reclaim。比较常见的默认配置类似这样--eviction-hardimagefs.available15%,nodefs.available10%含义是当镜像文件系统的可用空间低于 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或者直接看/proclsof -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。第二步看详细 Conditionkubectl describe node node-name在输出里直接搜Conditions段落找DiskPressure这一行是否为True。如果为True基本可以确定问题方向了。第三步看集群里有没有大量异常 Podkubectl 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-nameservice-name对比一下可用的 Endpoints 数量和期望的副本数如果少了一大截502 的根因就清楚了。3.2 上机后的磁盘体检手册确定可疑节点后登上去做深度检查我习惯按这套顺序来df -h df -idf -h看空间df -i看 inode。inode 耗尽是个小众但致命的坑磁盘空间明明还剩不少但文件系统无法创建新文件现象和磁盘满一模一样。尤其是有大量小文件的目录比如容器运行时的 overlay 层很容易把 inode 吃完。接着按占用从高到低扫目录du -h --max-depth1 /var/lib 2/dev/null | sort -hr | head du -h --max-depth1 /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 -20lsof输出的第 7 列是文件大小把它排个序能直接揪出那些“占着茅坑不拉屎”的删除文件。我当时在那台故障节点上跑完这套命令很快看到/var/lib/containerd占了超过 60Gjournal 占了 10G 多此外还有一个 MySQL 实例的临时文件被删掉了但进程句柄还开着占了接近 20G。这几个加起来直接把根分区顶爆了。3.3 结合 kubelet 日志确认驱逐事件上机之后不要光顾着清磁盘先把“为什么变成这样”的证据保留下来。kubelet 的日志里会留下明确的驱逐记录方便你后面复盘和写报告。不同发行版 kubelet 日志位置不一样。systemd 管理的用 journalctljournalctl -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-size500M journalctl --vacuum-time3d然后清理容器日志。这一步操作不复杂但要知道自己删的是什么。旧容器日志文件一般躺在/var/log/containers/和/var/lib/docker/containers/下。可以直接 truncate也可以删除但要确认对应的容器已经退出或者你能接受运行中的容器写日志到原句柄而文件被删干净的结果。我更建议用 truncate 而不是rmfind /var/log/containers -name *.log -size 100M -exec truncate -s 0 {} \;原因是运行时可能还持有打开的文件句柄truncate 之后容器还能继续写只是旧数据被清空直接rm反而会让文件句柄继续占空间除非容器重启。这也是很多人删了日志但df -h没变化的原因。最后清旧镜像。containerd 用crictlcrictl rmi --prunedocker 用docker image prune -a --filter until24h注意--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-hardnodefs.available15%,imagefs.available15%同时配合--eviction-minimum-reclaimnodefs.available5%,imagefs.available5%让驱逐之后至少能回收出 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 驱逐、容器运行时依赖磁盘这些底层逻辑再遇到类似的“网关报错但网关无辜”的情况你就能更快找到真正的病根。