1. 先定个调:这个报错和“磁盘满了”不完全是一回事
凌晨两点,手机连着震了好几下。测试环境的监控告警弹出来:某台机器上的docker容器起不来了,报错信息就一句——“no space left on device”。我打开电脑,先敲了df -h,结果愣住了:根分区明明还剩十几个G,/home也还有空余,怎么看都不像“磁盘满”该有的样子。当时第一反应是“机器被黑了?还是磁盘坏了?”但试了试touch /tmp/test.txt,一样报同样的错误。
后来才知道,这个报错在Linux系统里根本不只代表“磁盘剩余空间不足”这一种情况。你面对的可能是文件系统块空间耗尽,可能是inode索引节点耗尽,可能是进程占用已删除文件导致空间不释放,也可能是Docker的overlay2目录把某个挂载点给塞爆了。不同原因,表现相近,排查路径却完全不同。
用一句话总结我的教训:df -h只是第一层侦查,它正常不代表文件系统真的还能写入。真正定位这个问题,需要你把视线从“还剩多少空间”转移到“空间到底被谁占着、为什么删了还不释放、文件系统还能不能创建新文件”这几件事上。这篇文章就想把这个排查链条完整捋一遍,包括我在生产环境踩过的坑、用过的命令、以及最后沉淀下来的预防手段。如果你是刚接触Linux运维、或者已经被Docker的报错折磨过几回的人,这篇应该能帮你省下半夜爬起来翻文档的时间。
2. 先别急着清理:两个命令先确定你面对的是哪一类“满”
2.1 排查前先区分:是块空间满,还是inode满
Linux文件系统里有两个很容易混为一谈的“容量”概念。
第一个是块空间,也就是数据本身占的位置。你存了一张照片、拉了一个镜像、写了一份日志,这些内容都要落在磁盘的block块上。df -h看的就是这个,单位是G、M,直观好懂。
第二个是inode(索引节点),你可以把它理解成文件系统给每个文件发的“户口本”。文件名、文件权限、所有者、数据块地址这些元数据都记在inode里。重点是:在绝大多数文件系统(ext4、xfs)里,inode数量是在格式化时就固定好的,不随磁盘空间动态增加。一个inode对应一个文件(目录、软链、socket也算),哪怕每个文件只有1字节,只要文件数量超过inode上限,系统就再也建不出新文件了。
我打个比方。块空间像一个仓库的货架,inode是货架上的编号标签。货架还剩一大半空位,但标签已经全部贴完了,新货就进不来。对应到系统上,就是df -h还有余量,但touch一个空文件都会告诉你“no space left on device”。
2.2 两个命令,五秒钟分清局面
遇到这个报错,第一步永远是这两个命令:
df -h df -idf -h看块空间使用率,df -i看inode使用率。如果df -i显示/dev/vda1的IUse%已经跑到100%,那问题就清晰了——不是磁盘块不够,是文件数量爆炸了。如果df -i正常,df -h也正常,那就要考虑是不是某个进程占用了已删除文件(后面专门讲)。
这里给一张对照表,方便你按症状快速归类:
| 现象 | 可能原因 | 下一步动作 |
|---|---|---|
df -h显示100%,创建文件报错 | 块空间不足 | 用du定位大目录,清理日志/镜像/缓存 |
df -i显示100%,块空间正常 | inode耗尽 | 按目录统计文件数量,找小文件温床 |
| 两个都正常,但仍然报错 | 进程占用已删除文件 | lsof +L1找deleted状态文件 |
| Docker目录撑爆了挂载点 | overlay2/容器日志膨胀 | docker system df看镜像/容器/卷占用 |
这套判断逻辑我建议你记下来,因为它能帮你省掉80%的无头绪排查。我第一次遇到inode耗尽时,还在反复删大文件、清/tmp,忙活了半个多小时才意识到方向错了——真正的问题在几百公里外的一台NFS挂载目录里,几百万个session小文件把inode吃了个精光。
3. 用df -i看到100%之后:小文件是怎么把inode吃光的
3.1 inode是怎么被“小文件海”耗尽的
一旦确认是inode耗尽,接下来的核心问题就变成:到底是哪个目录冒出了海量小文件。生产环境里最常见的几类温床:
- 容器运行时目录:
/var/lib/docker/overlay2下每个镜像层、容器层都是一堆目录和文件。镜像更新的层数越多,容器销毁重建越频繁,小文件就越堆积如山。Docker长时间不清理,这里能轻松囤出几十万甚至上百万个文件。 - 应用session文件:PHP的session默认落盘到
/var/lib/php/sessions,session过期回收配置不当,一天能产生几十万个文件。 - 日志切分碎片:有些应用不按天滚动日志,而是把日志按请求切成大量小文件;还有
systemd-journald的journal文件,长时间不轮转也会产生大量文件。 - 邮件队列:
/var/spool/mqueue或Postfix队列里有大量未发送的小邮件,每封就是一个文件。 - 临时目录:
/tmp、/var/tmp被乱七八糟的程序当缓存目录使用,文件只写不删。
这些场景有个共同特点:单个体积很小,但数量极其庞大。所以你拿du -sh去查,每个目录看起来都只有几百M,完全不起眼,但它们加在一起却能把inode吃干净。这就是为什么inode问题容易让你“查了个寂寞”——大文件排查法在这里失效了。
3.2 用两个命令快速盘出“文件大户”
inode耗尽的场景下,du看体积没用,得按“文件数量”来统计。我常用的命令是这俩:
# 从根目录开始,按层统计各目录的文件数量 for d in /*; do echo -n "$d: "; find "$d" -xdev | wc -l; done # 深入某个指定目录,找到最底层的一级 for d in /var/lib/docker/*; do echo -n "$d: "; find "$d" -xdev | wc -l; donefind | wc -l思路很朴素:数一遍目录树里到底有多少个条目。加上-xdev是为了不跨文件系统,避免把其他挂载点里的文件也算进来。如果你嫌一层层手工点太慢,也可以用du --inodes一步到位:
# 按inode数量排序,找出当前目录下哪个子目录文件最多 du --inodes -d 1 / | sort -rn | head -20du --inodes在GNU coreutils里是原生支持的,直接按inode使用数做汇总,不用自己写循环,省事得多。跑完基本就能看到/var/lib/docker、/var/lib/php/sessions这种目录突兀地排在前面。
3.3 inode耗尽的临时缓解与根治思路
找到“文件大户”之后,操作上要分两步走。
临时缓解是先把inode使用率降下来。如果是session文件或临时缓存文件,可以直接清掉一部分老文件;如果是overlay2目录,就要用docker system prune或手动清理悬空镜像(后续展开)。这一步的目标是让系统先恢复写入能力,别让业务继续挂着。
根治思路则要看文件产生的源头。比如session文件,你得去排查session.gc_probability和session.gc_maxlifetime配置,调大清理概率;比如容器日志碎片化,你得在应用侧或docker日志驱动上做轮转配置。只清一次不解决上游,过一周又满了——这种“清了又满、满了又清”的循环我见得太多。
注意:如果你确认是
/var/lib/docker下的文件数量爆炸,并且历史镜像很多,直接手动删某个overlay2子目录容易把镜像搞坏。宁可花时间跑docker image prune、docker system prune,也别去rm -rf底层目录结构。
4. 精准定位:从大目录一路查到被占用空间的完整链路
4.1 块空间满的场景:du逐层下钻的笨办法
如果df -i正常、df -h显示某个分区已经100%,那就要老老实实拿du逐层下钻。我习惯的路径是从挂载点根目录开始:
du -h -d 1 / | sort -hr | head -20-d 1表示只看一层子目录,sort -hr按人类可读的容量倒序排列。看到/var最大,就再往下一层:
du -h -d 1 /var | sort -hr | head -20 du -h -d 1 /var/lib | sort -hr | head -20一层层追下去,最终会定位到某个具体的目录,比如/var/lib/docker、/var/log、或者某些应用的data目录。这个办法虽然朴素,但最可靠——它不会因为文件类型不同而漏判,所有占空间的东西都会暴露出来。
实际排查中,有几个目录特别值得优先怀疑:
| 目录 | 常见膨胀原因 |
|---|---|
| /var/log | 各类应用日志、nginx访问日志不轮转 |
| /var/lib/docker | 镜像层累积、悬空镜像、容器可写层扩大 |
| /var/lib/mysql | binlog日志、慢查询日志、临时表空间 |
| /tmp | 程序写临时文件不清理、core dump文件 |
| /home | 用户目录里的大文件、构建产物 |
4.2 deleted files:删除后空间却一直不释放的“幽灵占用”
这是我个人认为Linux磁盘排查里最玄学、也最容易被忽略的一种情况。现象是:df -h显示某个分区还是满的,但你明明已经把大文件rm掉了,空间却一点没降下来。
原因在于——进程仍然持有已经被删除文件的文件句柄。Linux下,文件被删除之后,如果某个进程还在通过句柄读写它,那么这个文件的数据块并不会真正释放,得等进程关闭句柄或退出才行。常见场景有:
- 运行中的Java/Python进程,日志文件被
rm了但进程没有重新打开文件描述符; - MySQL等数据库的临时文件或undo log被误删;
- 某些服务的buffer/cache文件,被keep打开着。
排查方法就是靠lsof:
# 查找所有处于deleted状态但仍被打开的文件 lsof -nP | grep deleted正常情况下列表可能就几条,但空间满的时候,你往往会在这里看到几个“幽灵文件”,占用好几十G,位置显示/var/log/xxx.log (deleted)。找到占用进程的PID之后,有两种处理方式:
- 如果你能停服务——直接重启或者让进程重新加载配置,句柄释放,空间自动回来;
- 如果服务不能停——可以用
cat /dev/null > /proc/PID/fd/文件描述符路径的方式把文件截断,注意不是删掉,而是清空内容。这个操作要小心,确认好fd路径再执行。
提示:
rm掉日志文件之后,如果服务还能正常写日志,可能会冒出“No such file or directory”的错误。正确做法是先kill -USR1或kill -HUP让进程重新打开日志文件,而不是简单粗暴直接删。
4.3 用ncdu把排查效率提上去
如果是第一次排查这种问题,或者机器上的目录结构比较深,手工一层层du确实累。我的经验是可以直接装一个ncdu(NCurses Disk Usage),交互式排查效率翻倍:
# CentOS / Ubuntu yum install -y ncdu # 或 apt install -y ncdu # 在根目录下扫描,进入交互界面后可以用方向键逐层浏览 ncdu -x /ncdu把整个目录树按占用大小排好,上下键切目录,d键还能直接在界面里删文件。它和du的本质是一样的,只是把“逐层命令”变成了“视觉浏览”,更大程度上减少了漏看、错看的情况。如果你经常要处理这类问题,强烈建议装一个。
5. docker场景专项:overlay2是怎么把空间和inode一起耗光的
5.1 docker的存储结构为什么这么容易出问题
Docker之所以经常成为“no space left on device”的重灾区,和它的存储驱动机制有关。默认的overlay2驱动,会把镜像分成一层一层的只读层,容器运行时再叠一个可写层。每次docker build产生新的镜像层,每次docker pull拉取新镜像,都会在/var/lib/docker/overlay2下铺设大量目录和文件。
时间一长,这个目录会有两个趋势:
- 体积变大:镜像层和容器可写层累积,加上历史镜像不清理,几十G甚至上百G都有可能;
- 文件数量爆炸:每个镜像层都有独立的目录、
diff目录、link文件等,镜像越多文件数越多。
这就导致一个问题:有时候df -h看/var/lib/docker挂载点还有空间,但df -i已经告急;有时候反过来,文件数不多,但块空间被一堆悬空镜像吃光了。两种“满”都可能由Docker引发,排查时一定把两个指标都看了。
判断Docker具体占了多少空间,用这个命令最直接:
docker system df它会列出镜像、容器、数据卷、构建缓存这几类分别占用了多少空间,以及有多少是“可回收”的。如果IMAGE列显示一堆悬空镜像(<none>),容器列有大量已退出但没删除的容器,BUILD缓存也囤了一堆,那清理空间的空间就很大。
5.2 容器日志无限膨胀是隐形杀手
很多人排查Docker磁盘占用,注意力全放在镜像上,忽略了容器日志。Docker默认的日志驱动是json-file,每个容器的标准输出都会落成宿主机上的日志文件,路径在/var/lib/docker/containers/<容器ID>/<容器ID>-json.log。
如果应用本身打日志没有节制、而且没有配置日志轮转,这个json文件能涨到好几个G。更麻烦的是,这个文件被容器进程持续打开,就算你手工rm了它,空间也不会释放——这就是前面说的“幽灵占用”在Docker场景下的典型表现:du看不到大文件,df却依然满着。
我处理过最极端的一次,一个跑Java服务的容器,json日志文件涨到了30多G,节点差点挂掉。后来在/etc/docker/daemon.json里加了日志轮转配置,才彻底解决:
{ "log-driver": "json-file", "log-opts": { "max-size": "50m", "max-file": "5" } }max-size表示单个日志文件最多50M,max-file表示最多保留5个文件。改完配置后重启docker生效。要注意:这个配置只对之后创建的容器生效,已经存在的容器还是老配置,最稳妥的办法是重建容器,或者至少在部署新容器时把参数传上:
docker run \ --log-driver json-file \ --log-opt max-size=50m \ --log-opt max-file=5 \ your-image5.3 三条docker清理命令的边界和代价
Docker自带的清理命令用起来有讲究,不建议一把梭。下面这张表是我自己总结的边界:
| 命令 | 清理内容 | 注意点 |
|---|---|---|
docker container prune | 已停止的容器 | 如果容器里还有未提交的变更,删了就没了 |
docker image prune | 悬空镜像(<none>标签) | 不加-a只清悬空镜像,比较安全 |
docker image prune -a | 所有未被容器使用的镜像 | 会把还在用但没跑容器的镜像也删掉,再次拉取会花时间 |
docker system prune -a --volumes | 镜像+容器+网络+数据卷+构建缓存 | 最激进,会把所有闲置数据卷一并删除,务必先确认 |
生产环境的建议是:日常清理用docker image prune加docker container prune就够了,-a和--volumes只在空间告急且确认无重要数据时使用。数据卷(volume)里经常藏着数据库文件,一旦被--volumes删掉,想恢复基本没戏。
6. 治标更要治本:这台机器以后怎么才能不再警报
6.1 日志轮转和核心配置:一劳永逸的基础项
排查完问题不代表结束。真正要紧的是让这类问题“少来烦你”,所以我在每次处理完no space问题的几天内,都会顺手把下面几件事补上:
第一个是系统日志轮转。/etc/logrotate.conf和/etc/logrotate.d/目录下配置了各系统日志的轮转策略。确认一下有没有把关键日志目录加进去,比如:
/var/log/nginx/*.log { daily rotate 14 compress delaycompress missingok notifempty dateext }这段配置的意思是nginx日志每天切一次、保留14份、切完压缩。类似地,Java应用如果通过stdout写日志,就要靠Docker的log-opts限制;如果是文件日志,就得靠logrotate。
第二个是systemd-journald的容量上限。journald默认不会无限占用磁盘,但上限在某些发行版上偏高,需要主动收紧。修改/etc/systemd/journald.conf:
SystemMaxUse=500M MaxRetentionSec=2week改完执行systemctl restart systemd-journald。这样journal日志最多占500M的黑洞空间不会侵蚀业务分区。
第三个是监控告警要同时覆盖df -h和df -i。很多监控工具默认只看磁盘使用率,inode告警没人管,所以有时候inode满到100%还得等业务报障才发现。
6.2 一条命令级别的“脚本体检”
我习惯在服务器上保留一个快速体检脚本,排查时省得敲一串命令。下面这个是精简版:
#!/bin/bash echo "===== 块空间使用 =====" df -h | grep -v tmpfs echo "" echo "===== inode使用 =====" df -i | grep -v tmpfs echo "" echo "===== 最占空间的10个目录 =====" du -h -d 1 / 2>/dev/null | sort -hr | head -10 echo "" echo "===== deleted但未释放的文件 =====" lsof -nP 2>/dev/null | grep deleted | head -20把这段存成disk-health.sh,需要的时候直接执行,10秒出全貌。也可以挂到crontab里凌晨跑一次,输出重定向到日志文件,早上扫一眼就够了。
6.3 我踩过几次坑之后的最终检查清单
这套问题前前后后处理过七八回之后,我给自己沉淀了一份清单,每次遇到no space相关告警就走一遍:
- 先看
df -h和df -i,把“块满”和“inode满”分开。 - 确认挂载点边界:报错的目录到底落在哪个分区上,别在A分区查了半天,实际写的是B分区。
- 块空间满:用
du或ncdu逐层定位,优先查/var/log、/var/lib/docker、/tmp。 - inode满:用
du --inodes或find | wc -l找小文件温床,重点看session目录和overlay2。 - df显示满但du找不到大文件:立刻跑
lsof | grep deleted,找幽灵句柄。 - Docker场景:先
docker system df看镜像和构建缓存,再查/var/lib/docker/containers/*/*-json.log的日志大小。 - 清理后:用
df -h、df -i复验,确认使用率降下来,并记录占空间最大的TOP目录,便于下次快速定位。 - 事后补配置:日志轮转、journald上限、docker日志驱动、监控告警,四项缺一不可。
这套清单帮我在后来的几次事件里,基本都能在几分钟内找到根因。排查的主动权,其实就来自“你知道空间可能藏在哪几个角落”的预判——这比临时翻文档有效得多。
最后再说个实际体会。清理磁盘永远只是“灭火”,真正省心的是把“防火”做在前面。日志轮转、镜像清理频率、inode监控这三样只要舍得花半小时配置好,后面一年可能都不会再碰这个报错。别等到凌晨手机响才想起来补课,那种感觉我太熟了。