☰
Linux服务器磁盘爆满排查与清理:从df到inode、lsof实战指南
2026/9/30 11:04:50 网站建设 项目流程

上周五下午,我这边一台跑着应用服务的 Linux 服务器磁盘使用率冲到了 98%,随后监控告警、业务方投诉一起涌过来。我登录上去先执行df -h,发现/data分区已经红了,再用df -i看了一眼 inode 使用率,也已经到临界值。这种时候最忌讳的是看到某个大文件就随手rm -f,删错了可能比磁盘满更麻烦。这篇文章就把我这些年处理 Linux 服务器磁盘爆满的排查链路、常用命令和清理动作完整写下来,给遇到同样问题的运维和开发朋友做参考。

文章面向的是需要自己处理服务器问题的后端开发、运维工程师,也包括刚开始学 Linux 的新人。我会按照“先确认满的性质 → 再逐层定位大文件 → 再排查隐藏空间黑洞 → 最后做清理和长期预防”的顺序来写,每一步都给出可以直接复制执行的命令,并解释为什么这样做。

1. 确认“满”的具体含义:空间占用还是 inode 耗尽

1.1 df -h 和 df -i 必须同时看

很多人拿到磁盘爆满的告警后,第一反应就是跑df -h,看到某个分区 Use% 到了 100%,就开始在目录里翻大文件。但这里有一个很容易踩的坑:df -h看的是磁盘块空间占用,而 Linux 上还有一个同样会导致“磁盘满”的资源叫 inode,也就是文件索引节点。

你可以把磁盘空间理解为仓库的货架面积,inode 理解为货架上的编号标签。货架面积再大,如果编号标签用光了,新到的货物也摆不进去。对于 ext4、xfs 这类文件系统,每个文件或目录都要占用一个 inode,如果服务器上的小文件特别多,比如程序生成的临时文件、缓存碎片、消息队列的持久化消息,就非常容易出现“块空间还有不少,但 inode 已满,导致读写报错”的情况。

排查时我会把两条命令同时执行:

df -h df -i

我的习惯是看输出里的 Mounted on,也就是挂载点。比如/data分区/dev/sdb1的df -h显示已经 100%,同时df -i显示 IUse% 只有 30%,那基本可以确定是块空间满。反过来,如果df -h显示 60%,df -i显示 100%,那就是 inode 耗尽,需要找的是“大量小文件所在目录”,而不是单一的大文件。

这里还要提一个容易混淆的细节:df显示的 Use% 可能还会出现大于 100% 的情况,比如 102%,这通常发生在 ext4 文件系统的预留块被抵扣、或者异常删除后统计延迟导致的。此时不要慌,先确认业务是否真的在报磁盘写入错误,再继续排查。

1.2 找到真正影响业务的挂载点

df -h输出里通常会有多个挂载点,比如/、/boot、/data、/var等。有时候某个分区满了,但业务根目录在另一个分区,互相之间未必有直接关联。所以关键是先弄清楚:“哪个挂载点是影响当前业务的那个”。

举例来说,如果业务数据写在/data下,但 MySQL 的 binlog 写在/var/lib/mysql下,而/var是独立分区,那你说“磁盘满了”到底是/data满还是/var满,处理方向完全不同。我在排查时习惯先问自己三句话:

  • 告警说的是哪个挂载点?
  • 业务进程的工作目录、数据目录、日志目录分别在哪个挂载点?
  • 这些挂载点之间的容量关系是什么样的?

如果业务没有明确指向,那就直接看df -h输出里哪个分区 Use% 最高,同时看/根分区是否还有剩余。很多时候应用安装在根分区,数据在数据分区,日志又可能被 logrotate 转到/var/log,这三者的占用都要心里有数。

还有一个容易忽视的挂载点:/dev/shm。它默认是 tmpfs,大小约为物理内存的一半,如果某些程序把临时文件写到/dev/shm,同样会体现在df -h中。它不是磁盘,而是内存,但清理方式跟普通文件目录不同,后面我会单独说明。

1.3 先记录现状,再看是突增还是缓慢上涨

不要一上来就删除。删除是不可逆操作,至少先花一分钟记录现场。我会按下面的顺序做快照:

date df -h df -i du -sh /data/* 2>/dev/null | sort -rh | head -20

这样做的目的有两个:一是确认到底哪些目录占用靠前,二是给后续“清理之后是否有效”留一个对比基线。如果磁盘是从几天前就开始缓慢上涨,还是今天突然跳满,这决定了排查的方向。

如果是突然跳满,优先怀疑日志文件、临时目录、core dump、被进程持续写入的大文件;如果是缓慢上涨,则优先怀疑业务数据增长、数据库 binlog、备份文件未清理、Docker 镜像堆积。看文件修改时间可以辅助判断:

find /data -xdev -type f -mtime -1 -size +1G -exec ls -lh {} \;

这条命令列出/data下最近一天内修改过、且大小超过 1GB 的文件。如果一个文件同时满足“最近在写入”“体积巨大”,它大概率就是当前磁盘还在继续涨的直接原因。

2. 把大文件找出来的三条路径:du、find、ncdu

2.1 用 du 逐层下钻,比 ls 更准确

找到占用最高的分区后,下一步是在这个分区内定位大目录。很多新人会用ls -lh看文件大小,这其实不够准确。ls看到的是文件逻辑大小,而du统计的是文件实际占用的磁盘块大小,两者对于稀疏文件、压缩文件、有空洞的文件来说可能差异很大。更重要的是,du可以递归统计目录的总占用,直接帮你看清哪个目录最“重”。

我通常用的第一层命令是:

du -h --max-depth=1 /data 2>/dev/null | sort -rh | head -20

--max-depth=1表示只统计/data下一级目录的总大小,然后按大小倒序排。查看结果后,进入最大的子目录,继续执行同样的命令。这样逐层下钻,直到找到真正占用空间的文件或子目录。

这里有几个细节需要注意:

  • 加上2>/dev/null可以过滤掉因权限不足产生的错误输出,但要注意,如果某些子目录连读权限都没有,du统计结果会不完整,此时需要切换到 root 或调整权限后再查。
  • 加上-x参数可以避免跨越文件系统边界。比如/data下挂载了一个独立磁盘,不带-x时du会把所有挂载点的占用都算进来,容易误导排查方向。
  • du对非常庞大的目录树递归耗时较长,如果磁盘上数百万文件,建议用--max-depth=2先看粗粒度,再逐步深入,不要直接对根分区做全量递归。

2.2 用 find 按大小和时间筛出可疑文件

du告诉我们目录大小,但还不够精确到具体是哪个文件在占空间。这时用find按大小条件筛选大文件更高效。我的常用组合:

find /data -xdev -type f -size +2G -exec ls -lh {} \;

这条命令找出/data下所有大于 2GB 的普通文件,并显示详细信息。如果你怀疑是缓存或日志文件,可以把时间条件也加进来:

find /data -xdev -type f -mtime -3 -size +500M -exec ls -lh {} \;

表示最近 3 天内修改过、且超过 500MB 的文件。执行结果里,如果出现.log、.out、core.*、*.pid这类名字,基本就能锁定目标了。

另外提醒一句:find的-size +2G是识别文件逻辑大小,对于稀疏文件(比如某些数据库的预分配文件)可能会误判,但这类文件极少出现在真的需要清理的场景。更常见的是find会扫描出大量.trash、.cache目录下的历史文件,这些通常是安全删除对象。

2.3 用 ncdu 交互式浏览,适合大目录排查

命令行逐层du比较繁琐,如果你和我一样更习惯看可视化界面,可以装一个ncdu。它在很多发行版仓库里都有:

# Debian / Ubuntu apt install ncdu -y # CentOS / RHEL yum install ncdu -y

安装后直接运行:

ncdu /data

它会先递归统计目录大小,然后进入交互界面,按大小排列目录,方向键可以逐层进入。这个工具在 SSH 终端里用起来非常顺手,比反复执行du | sort要直观得多。

不过要注意,ncdu在目录特别大时初始扫描也要等一会儿,而且它统计的也是磁盘占用,不是文件大小。还有一点:如果你的服务器是内网离线环境,装不了这个工具,那还是老老实实用du和find,完全够用。

3. 最隐蔽的空间黑洞:句柄未释放、日志与容器

3.1 删了文件但空间没回来,先查被进程占用的文件句柄

有一种特别让人头大的场景:你通过du和find找到了一个大日志文件,确定它是罪魁祸首,于是执行了rm -f,但df -h一看,可用空间一点没变。这种时候不用怀疑,通常是有进程仍然持有这个已经被删除文件的文件句柄。

Linux 下,文件被删除后,只要还有进程打开着它,占用的磁盘块就不会被释放,直到进程关闭该句柄或进程退出。这类文件不会出现在du或者find的结果里,因为它们已经不在目录树中了,但会被空间占用计算在内。

排查方法是用lsof查看已删除但仍被打开的文件:

lsof +L1

+L1表示列出 link count 小于 1 的文件,也就是删除后仍被打开的文件。输出里会有进程名、PID、文件路径,路径后面通常会带(deleted)标记。也可以针对某个分区精确查看:

lsof +L1 /data

确认是哪个进程后,处理方式有两种:优先是让业务方平滑重启该服务,进程重启后句柄自然释放;如果该进程比较关键,不能重启,可以通过/proc/PID/fd/逐个确认对应句柄内容,但一般最稳妥的还是协商重启时间后操作。

这里有一个实践中的经验:很多长期运行的服务,比如 Java 应用、Nginx、Python 常驻进程,如果配置了按天切分的日志但旧日志被外部脚本删掉,服务进程仍然持有旧句柄,就会造成磁盘“删了白删”的假象。所以在清理日志前,最好先确认日志是否被进程打开、日志文件是否配置了copytruncate机制。

3.2 systemd-journald 日志在 /var/log/journal 里悄悄膨胀

如果/var/log分区或/分区经常被写满,一个常见元凶是 systemd 的 journald 日志。journald 会把系统日志和内核日志以二进制形式写到/var/log/journal/,默认情况下会持续累积。不像传统的 text 日志跨行处理,journal 文件按时间增长,到后期一个几百兆甚至几个 G 都不奇怪。

先看它占了多少:

journalctl --disk-usage

输出会显示当前日志占用大小和日志文件数量。临时清理可以按容量来:

journalctl --vacuum-size=500M

这条命令会把日志总量压缩到 500MB 以内,只删除旧日志。如果系统日志不重要,也可以直接清空:

journalctl --vacuum-time=7d

表示只保留最近 7 天的日志。但要注意,--vacuum-size是立即生效的临时操作,永久限制 journal 大小需要在配置文件里设置:

vim /etc/systemd/journald.conf

找到SystemMaxUse=,把它设置成你想要的上限,比如 500M,去掉注释。修改后重启 journald 服务:

systemctl restart systemd-journald

这种做法对根分区长期稳定非常有帮助。另外,如果你用的是 rsyslog 或 syslog-ng 收集远程日志,这类旧式日志通常在/var/log/messages、/var/log/secure下,配合 logrotate 做轮转即可,后面会讲。

3.3 Docker overlay2 与容器日志的容量管理

现在的服务器上跑 Docker 非常常见,Docker 所在的数据目录/var/lib/docker往往是磁盘占用大户。如果你不限制容器日志大小,时间久了/var/lib/docker/containers/*/*-json.log会单个达到数十 GB,而且这个问题在普通du中一眼就能看到。

先整体看一眼 Docker 占了多少:

docker system df

它会显示镜像、容器、卷、构建缓存各自的占用。然后是这几个清理动作:

  • 容器日志限制:最推荐的方式是修改 Docker daemon 配置/etc/docker/daemon.json,加入:
{ "log-driver": "json-file", "log-opts": { "max-size": "100m", "max-file": "3" } }

这样每个容器日志上限 100MB,保留 3 个文件。修改后systemctl restart docker。这个方案对已经存在的容器需要重建才生效,但对新容器立竿见影。

  • 悬空镜像清理:docker image prune -a可以删除没有被容器引用的镜像。执行前确认业务不需要回滚到旧镜像。

  • 构建缓存清理:docker builder prune可以清理 BuildKit 缓存。

  • 对于已经写出来很大的容器日志文件,可以直接找到后暂时用truncate -s 0清空文件内容,而不是rm。因为容器进程可能还持有句柄,删除后空间依然不会释放,清空内容可以立刻腾出空间。

Docker 的 overlay2 目录里还会有大量临时层目录,除非你非常确定某个目录没有容器使用,否则不建议手动删除,用上面的docker system prune更安全。

4. 清完之后如何不再次爆满:轮转、监控与回归验证

4.1 清理后必须做的验证动作

清理完成后,不要急着说“搞定了”。我的习惯是至少做三件事:

第一,重新确认磁盘空间和 inode:

df -h df -i

第二,确认业务进程还能正常写入。对应用发一个小请求,或者看应用日志是否在持续更新。比如:

tail -f /data/log/application.log

第三,观察一段时间,确认没有新的异常增长。通常在清理完大日志后,我会隔 10 分钟再看一次df -h,如果空间使用率又快速上升,说明写入源还没找对,得回到第 2 章继续排查。

这里还要提醒一个细节:如果清理时选择了rm而不是truncate,要特别注意日志文件所在的服务进程是否持有句柄。正确做法是先确认进程,再决定是删除还是清空。对于应用自己正在写的日志,最安全的方式往往是truncate -s 0,或者直接配置 logrotate 的copytruncate。

4.2 用 logrotate 和 systemd 定时器把清理自动化

手动清理只能救一时,长期稳定还要靠自动化。Linux 自带的 logrotate 是日志轮转的标配,你可以在/etc/logrotate.d/下新建一个配置文件,针对业务日志设置轮转策略。

比如我经常给业务方写的配置:

/data/log/application.log { daily rotate 7 compress delaycompress missingok notifempty copytruncate }

含义是:日志每天轮转一次,保留 7 份,旧日志压缩,轮转时复制当前日志并清空原文件。copytruncate适合那些不支持重新打开日志文件的应用,它通过复制和清空的方式避免应用进程持有旧句柄导致空间不释放。如果应用支持 signal 通知重新打开日志,比如 Nginx,可以改用create配合postrotate发送信号,这样日志切割更精确。

对于 journald,前面已经说过设置SystemMaxUse。对于临时目录和缓存,我还会用 crontab 定期执行清理脚本:

0 3 * * * find /tmp -type f -mtime +3 -delete >/dev/null 2>&1

但这里的定时清理一定要慎重,/tmp下某些文件可能是正在运行的进程需要的。更好的方式是引导业务方把临时文件写到单独目录,并制定明确的保留周期。

4.3 监控告警设置在爆满之前,而不是之后

说实话,我处理过的磁盘爆满故障,绝大多数都可以靠一个好的监控系统避免。与其等磁盘 100% 再救火,不如在 85% 的时候就收到通知,提前处理。

监控维度至少包含两个指标:

  • 块设备使用率:按挂载点区分,比如/data85% 告警、95% 严重告警。
  • inode 使用率:有时块设备空间还没满,inode 先爆,所以也要单独监控。

如果公司没有商业监控平台,自己用 node_exporter + Prometheus 或者简单的 cron 脚本也可以做到。我写过最简单的告警脚本逻辑:

THRESHOLD=85 USE=$(df -h /data | awk 'NR==2{print $5}' | sed 's/%//') if [ "$USE" -ge "$THRESHOLD" ]; then echo "disk usage high: $USE%" | mail -s "disk alarm" admin@example.com fi

更完善的还可以把 Top10 大目录一起放在告警内容里,让处理人一进 SSH 就知道该往哪查。

另外,基于我个人的实战体会:每次磁盘故障处理完,我都会花 5 分钟想一下“这个容量是被什么业务消耗的,预期增长周期是多少”。如果增长周期很短,比如日志每天 10GB,那光清理没有意义,尽快做轮转和容量扩容才是正路。

最后再分享一个我自己的习惯:拿到磁盘告警后,第一件事先执行df -i和lsof +L1这两条命令。前者排除 inode 满,后者提前发现是否有已经删除但未释放的句柄。这两条看似简单的检查,能省下后面至少半小时的绕路排查时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询