Linux磁盘空间占用排查实战:理解df与du,善用lsof与inode
2026/9/10 5:49:21 网站建设 项目流程

1. 先搞清楚磁盘占用分析到底在解决什么问题

日常运维和开发中,最让人心头一紧的告警之一就是“磁盘空间不足”。我处理过很多次这种问题,表面看只是df -h输出红了,但背后原因五花八门:可能是某个服务把日志写得停不下来,可能是一个临时文件被进程持续写入但已经删除,也可能是某个数据目录被备份脚本塞爆了。真正头疼的不是“磁盘满了”这个结果,而是“空间去哪了”这个谜题。

这篇内容适合所有跟Linux打交道的人看,不管是刚入门的新手,还是已经写了几年脚本的老手。新手可以通过这篇文章建立一个完整的排查思路,不再拿到服务器就只会按方向键翻历史记录;老手也可以对照一下自己的排查流程,看看有没有遗漏的盲区。我尽可能把实际操作中的细节、容易踩的坑、以及那些文档里不会明说的经验都写出来。

1.1 先建立一个基本认知:df和du的差异

很多人刚开始做磁盘占用分析时,习惯一上来就执行du -sh *,然后发现统计出来的总大小跟df -h显示的使用量对不上,就开始怀疑是不是命令用错了。其实df和du的统计机制本身就不同,理解它们的差异比会敲命令重要得多。

df是从文件系统层面获取块设备的使用情况,它统计的是整个文件系统的总块数、已用块数和可用块数,这个数据来自文件系统自身的元数据。du则是通过遍历目录树,逐个文件累加占用的块数来得到结果。两者统计维度不同,天然会出现差异。差异来源主要有几个:文件被删除但进程仍持有文件描述符时,du已经看不到了,但df会继续把空间算作已用;文件系统预留的块(比如ext系列默认预留5%)也会造成两边数据不一致;还有du默认不统计其他挂载点的内容,而df是按每个挂载点单独显示的。

我自己的习惯是先df看整体水位,再用du定位大头。df告诉你是哪个分区出了问题,du告诉你是哪个目录在搞事情。两条命令配合使用,基本能解决90%的“磁盘满了”问题。

1.2 分析前先摸清现场:这些信息必须第一时间记录

在动手清理之前,先花两分钟把现场信息记录下来。这一步经常被忽略,但在事后复盘和定位根因时非常有用。至少需要记录:df -h的输出、df -i的输出(inode使用情况)、当前时间点、最近是否有部署或配置变更、是否有批量任务在跑。

尤其是df -i的输出,我见过不少“磁盘还有好几十G,但就是写不进文件”的情况,最后查下来是inode耗尽了。inode是文件系统用来保存文件元数据的数据结构,每个文件或目录都要占用一个inode。当分区里文件数量极其庞大时,即使还有剩余空间,也会因为inode用完而无法创建新文件。这个问题在消息队列积压、邮件队列、小文件缓存这类场景里特别常见。

我常用的快速记录方式是把这些信息直接追加到一个日志文件里,带上时间戳,方便后续回溯。命令大概是这样的:

{ echo "===== $(date '+%F %T') =====" df -h df -i echo "----- 最近24小时新增的大文件 -----" find / -xdev -type f -mtime -1 -size +100M -exec ls -lh {} \; 2>/dev/null | head -50 } >> /var/log/disk_audit.log

这段命令把df的快照和最近一天内新增的大文件都记录下来了,排查时非常有用。注意-xdev参数,告诉find不要跨文件系统,这样能避免扫到/proc、/sys这些虚拟目录时产生大量无意义的输出。

2. 从根目录开始的逐层下钻:定位大目录和文件的实操方法

拿到服务器后,我一般不会直接du -sh /*,因为有些挂载点可能很慢,甚至会有网络文件系统挂在上面,扫起来会卡住。更稳妥的做法是先看挂载情况,再针对可疑的挂载点逐个排查。

df -h mount | grep -v -E 'proc|sysfs|cgroup|devpts|tmpfs|overlay'

先看清楚哪些是真实存在的磁盘分区,哪些是虚拟文件系统。虚拟文件系统不占实际磁盘空间,排查时可以跳过。然后针对使用率比较高的分区,从挂载点开始往下逐层定位。

2.1 用du逐层定位大目录的操作细节

假设/分区的使用率已经到95%,我会执行:

du -h -x -d 1 / 2>/dev/null | sort -rh | head -20

这里几个参数都值得说一下。-h是human-readable,以K、M、G为单位显示;-x是one-file-system,不跨文件系统,这一点很关键,避免把其他挂载点的空间也统计进来,导致结果虚高;-d 1是目录深度为1,只看根目录下一级子目录的大小。-d参数比--max-depth写起来短,效果一样。最后用sort按大小从大到小排,取前20个。

看到哪个目录是主要占用来源后,再对那个目录执行同样的操作,一层层往下钻。比如发现/var很大,就执行du -h -x -d 1 /var 2>/dev/null | sort -rh | head -20,这样很快就能定位到具体是/var/log还是/var/lib还是/var/tmp在膨胀。

这里有一个打包好的逐层下钻思路:与其一次次手动敲命令,不如写一个小循环,每层自动找出最大的子目录并打印出来。我以前是直接在shell里套循环的,后来发现写个递归的脚本更顺手,在文章后面的章节我会贴一个完整版本,直接把整条链路从上到下自动找出来。

2.2 找大文件时的排序与过滤技巧

目录定位到了,但有时候一个目录里有海量文件,直接用ls -lS排序可能因为文件太多而卡顿。这种情况下我用一条组合命令精准锁定:

find /var/log -xdev -type f -size +500M -exec ls -lh {} \; 2>/dev/null | awk '{print $5, $9}' | sort -rh | head -30

这条命令做了三件事:找出/var/log下所有大于500M的普通文件;格式化输出大小和完整路径;按大小倒序排并取前30条。-size +500M这个阈值可以根据实际情况调整,如果磁盘特别紧张,可以降到100M甚至50M。

另外,日志目录里经常有带时间戳的轮转文件,比如app.log.20250101app.log.20250102这种。要快速统计这类文件占了多少空间,可以按文件名模式聚合:

find /var/log -xdev -type f -name "app.log.*" -exec du -ch {} + 2>/dev/null | tail -1

du -ch组合参数里的-c是总计,-h是人类可读。tail -1取出最后一行总计值。这样能快速判断某个服务的日志轮转文件整体占了多少空间,比一个个文件加起来方便得多。

用户目录也是重灾区。/home下的每个用户目录我都习惯单独看一下:

find /home -xdev -mindepth 1 -maxdepth 1 -type d -exec du -sh {} \; 2>/dev/null | sort -rh

这样能一眼看出哪个用户是“空间吞噬者”。如果服务器上跑着多个Java应用或数据库实例,数据目录、堆转储文件、错误日志都是大文件出现的常见位置,排查时不要遗漏。

3. 空间被占用但文件已删除:lsof和日志文件的特殊处理

这类问题是我在故障排查中遇到最多的一种“灵异事件”之一。表象很典型:df -h显示某分区使用率高达99%,但du -sh /*把所有目录大小加起来,怎么算都只有50%左右,空间莫名其妙“消失”了。

如果你也遇到这种情况,最可能的原因是:有进程正在写入一个已经被删除的文件。Linux的文件系统机制是,只要进程持有文件描述符,即使文件已经通过rm从目录结构中移除,磁盘空间依然被占用,直到进程关闭该文件或进程结束。

3.1 用lsof精准定位无法释放的空间

定位这类问题,lsof是绕不开的工具。以下是我常用的命令:

lsof +L1 2>/dev/null | sort -k7 -rh | head -30

+L1表示只显示link count为0的文件,也就是已经被删除但仍有进程打开的文件。这里稍微解释一下,link count是文件硬链接的数量,当文件从目录中删除后,硬链接数为0,但如果有进程打开了它,文件的数据块还没有真正释放。列出结果后,关注SIZE列和COMMAND列,能看出是哪个进程占用了多少空间。

输出里还可能包含类似/path/to/file (deleted)的路径,注意看(deleted)标记,这就是阻塞空间的“元凶”。

如果需要更精确地按目录筛选,可以加上路径过滤。比如怀疑/var/log下的文件,执行:

lsof +L1 2>/dev/null | grep '/var/log'

或者按进程名过滤:

lsof +L1 2>/dev/null | grep -E 'java|nginx'

找到阻塞空间的进程和文件后,处理方式有两种。如果服务允许重启,直接重启服务,文件描述符释放,空间立即回收。如果不能重启服务,可以对/proc/<pid>/fd/<fd>路径下的文件做空操作来释放空间。比如进程PID是1234,文件描述符是5,可以执行:

: > /proc/1234/fd/5

把该文件截断为空,但保持文件描述符仍指向有效位置,进程可以继续写入,同时空间立即释放。这个操作比rm更优,因为rm后进程仍然写一个不可见文件,空间不会释放。这里特别提醒:执行ls -l /proc/1234/fd/5可以查到实际文件路径和大小,确认没问题再操作。

3.2 日志截断的正确姿势:别一删了之

很多日志文件是长期被进程持有的,比如Java服务用logback写的日志,Nginx的access log和error log,Supervisor管理的进程输出日志。这些日志文件即使被rm删掉,进程还是会继续写入旧的文件描述符,空间不会释放,反而因为找不到原路径,排查难度更大。

正确的做法是截断而不是删除:

truncate -s 0 /var/log/nginx/access.log

或者用重定向清空:

> /var/log/nginx/access.log

清空后空间立即释放,进程继续写入也是正常的。需要注意,如果日志文件被Nginx自己重开过(比如配置了定时轮转并reopen),路径可能已经指向新文件,这时候清空旧文件反而不必要。先lsof确认一下哪些进程持有它,再决定怎么处理。

另外,即使进程持有文件,截断后写日志的位置会从offset 0开始,有些程序会因此出现日志空洞,但概率很低,绝大多数场景下无影响。如果日志中突然缺了一段时间,不要慌,检查是否有人对日志文件做过截断操作。

我自己的习惯是:发现日志文件过大时,先看看有没有日志轮转配置,没有的话立即配置logrotate,然后再处理当前积累的大文件。一删了之只解决眼前问题,没有轮转配置,过几天还会再爆一次。

4. 容易忽略的inode耗尽和挂载边界问题

在磁盘占用分析中,inode问题是一个容易被忽略但非常致命的方向。很多人在排查时只看磁盘空间,等到确认空间还有几十个G、但创建文件失败时,才意识到inode已经耗尽。

4.1 inode耗尽的判断和处理流程

先用命令确认问题:

df -i

IUse%这一列,如果接近100%,就是inode耗尽。导致inode耗尽的原因是文件数量过多,比如大量的小文件、邮件队列积压、消息队列的临时文件没清理、ext文件系统的journal节点异常增长等。这些小文件可能只占很少的磁盘空间,但每个都要占用一个inode。

排查手段和定位大目录一样,用find统计目录下文件数量:

find /var/spool -xdev -type f | wc -l

如果文件数量特别巨大,直接ls可能会卡死,可以用find配合wc -l先统计数量级,再决定从哪里开始清理。对目录逐层下钻找出文件数量最多的子目录,可以用:

find /var -xdev -type d -exec sh -c 'echo "$(find "$1" -maxdepth 1 -type f | wc -l) $1"' _ {} \; 2>/dev/null | sort -rn | head -20

这条命令会找出/var下文件数最多的20个目录,帮助快速聚焦。

清理时注意,inode问题往往是数量问题而非大小问题,所以删除策略要面向“减少文件数量”而不是“释放空间”。我遇到过一种情况:一个队列目录下有60多万个JSON小文件,每个只有几KB,但硬生生把inode吃满了。清理时用find ... -deletexargs rm -f更安全,前者不会因为参数列表过长而失败:

find /var/spool/queue -xdev -type f -mtime +3 -delete

4.2 文件系统边界:挂载点下的真实占用

排查磁盘占用时,很多人没有意识到dudf对挂载点的处理差异。df按文件系统维度显示,du按目录树遍历。如果在某个挂载点下面有了子目录的挂载,那么父分区看到的子挂载目录大小其实是“空”的,它的空间算在子分区的df报告里。

我举个例子。/data是一个独立分区,/data/mysql下又挂了一个单独的卷。在/分区里看/data目录,du默认不会跨挂载点去统计/data/mysql的实际数据,所以如果只看父目录的du结果,会觉得空间“少”了。

排查时要留意:

findmnt -rA | grep -v -E 'proc|sysfs|cgroup|devpts|tmpfs|overlay'

或者就是mount看一遍,确认哪些路径下有独立挂载。如果分区的使用率异常高但du到挂载点时看不到大文件,先看看这个挂载点下是不是还有子挂载。子挂载本身会把父目录的原内容“隐藏”掉,父目录下实际的数据可能已经被覆盖,这种场景需要特别小心。

磁盘占用分析里的“边界”问题不只是挂载点。还有一个常见坑是在根文件系统下执行du -sh /时把/proc/sys这类虚拟文件系统也扫了进去,虽然它们不占实际磁盘,但遍历它们会花费大量时间,还可能输出各种无意义的错误。因此我习惯在du和find命令中始终带上-x-xdev参数,效果是告诉命令不要跨文件系统边界,避免了把虚拟文件系统也纳入统计。

5. 日志、缓存和临时文件的清理实践

排查完大目录、处理完被占用的文件、修完inode问题,接下来要考虑的是建立长效机制,尤其是日志、缓存和临时文件这三类最常见的“空间杀手”。

5.1 journald日志的快速增长和控制方法

使用systemd的Linux发行版,systemd-journald会持续记录系统日志。这个日志的默认大小配置在很多系统上偏宽松,日积月累可能占掉好几个G。我记得遇到过一台Ubuntu服务器,/var/log/journal直接吃掉了5G多的空间,查了半天才发现是journal没限制。

查看当前journal占用:

journalctl --disk-usage

立即清理:

journalctl --vacuum-size=200M journalctl --vacuum-time=7d

前者把journal压缩到200M以内,后者清理7天之前的日志。如果想永久限制journal大小,编辑/etc/systemd/journald.conf,找到SystemMaxUse这一项,取消注释并设置为合理值,比如:

SystemMaxUse=300M

改完后重启服务使配置生效:

systemctl restart systemd-journald

实际测试中,SystemMaxUse生效后journal会自动轮转和清理历史日志,不会再无限增长。这个配置对长期运行的服务器非常有用。

5.2 logrotate的正确配置和排障

日志轮转是Linux运维的基础技能,但配置不当也会出各种问题。最典型的情况是配置了logrotate,但copytruncate参数用得不对,导致日志文件被截断后,进程写入的位置错乱,或者轮转不生效。

一个相对稳妥的配置示例,比如Nginx日志轮转:

/var/log/nginx/*.log { daily rotate 14 compress delaycompress missingok notifempty create 0640 nginx adm sharedscripts postrotate [ -f /var/run/nginx.pid ] && kill -USR1 `cat /var/run/nginx.pid` endscript }

这里解释几个关键参数。daily是每天轮转一次;rotate 14保留14个归档文件;compress对归档文件做gzip压缩;delaycompress延迟一天再压缩,这样昨天日志仍然可以方便地查看;postrotate会在轮转后给Nginx发送USR1信号,让它重新打开日志文件,确保不中断日志写入。

如果服务本身不支持重开日志,只能靠copytruncate参数,它的原理是先复制日志文件再清空原文件。这个方案虽然兼容性好,但会丢失少量日志内容(在复制和截断之间写入的数据),而且如果日志量很大并且频繁轮转,copytruncate对性能有一定损耗。

配置完可以手动触发一次做验证:

logrotate -v /etc/logrotate.conf

-v参数输出详细过程,能看出有没有报错。如果日志没有按预期轮转,检查以下几个方面:配置文件是否有语法错误、dateext是否与rotate天数配合得当、定时任务是否生效(/etc/cron.daily/logrotate是否存在且可执行)、日志文件属主权限是否允许轮转。

5.3 包管理器和临时目录的定期清理

apt、yum、dnf这些包管理器在安装和升级时会下载大量缓存。Debian/Ubuntu系统执行:

apt-get clean apt-get autoremove --purge

RHEL/CentOS系统执行:

yum clean all dnf clean all

这些缓存看似不起眼,积攒久了也能占用数G空间。对于长期运行的服务器,我建议把清理加入定时任务,每月执行一次,避免缓存无限膨胀。

临时目录/tmp/var/tmp也需要关注。/tmp通常有系统自动清理机制,但/var/tmp保留的是长时间需要存在的临时文件,很多应用会往里写入临时数据。如果担心误删,可以按mtime时间戳来清理,比如删除7天前的文件:

find /var/tmp -xdev -type f -mtime +7 -delete find /var/tmp -xdev -type d -empty -delete

第二条命令把空的子目录也清除掉,避免目录越积越多。

6. 一键诊断:把整个排查流程固化成脚本

排查思路讲了不少,实际应用中把这些经验固化成脚本能省很多事。我自己写了一个简易的磁盘占用诊断脚本,思路很简单:先看df和inode水位,再列出最大的目录,然后定位大文件和已删除但仍被占用的文件。这里贴一个精简版本:

#!/bin/bash # 磁盘占用快速诊断脚本 # 用法: ./disk_audit.sh [挂载点] MOUNT_POINT="${1:-/}" DATE_TAG=$(date '+%F %T') echo "===== 磁盘空间概览 ($DATE_TAG) =====" df -h "$MOUNT_POINT" echo echo "===== inode 使用情况 =====" df -i "$MOUNT_POINT" echo echo "===== 一级子目录空间占用 Top 10 =====" du -h -x -d 1 "$MOUNT_POINT" 2>/dev/null | sort -rh | head -10 echo echo "===== 大于 500M 的文件 Top 20 =====" find "$MOUNT_POINT" -xdev -type f -size +500M -exec ls -lh {} \; 2>/dev/null | awk '{print $5, $9}' | sort -rh | head -20 echo echo "===== 已删除但仍被进程占用的文件 =====" lsof +L1 2>/dev/null | awk 'NR==1 || $7 > 1048576' | sort -k7 -rhop

使用方法也很简单,比如要排查根分区,直接执行./disk_audit.sh /;排查/var分区,执行./disk_audit.sh /var。脚本会自动输出五个维度的信息,覆盖磁盘水位、inode水位、目录大头、大文件和空间泄漏五类问题。

这里解释一下最后一条lsof的awk过滤条件:$7 > 1048576表示只显示SIZE列大于1MB的被删文件,避免太小不值得关注的记录刷屏。如果你用-o参数指定了输出字段,列位置可能不同,需要根据实际输出调整$编号。

6.1 告警阈值建议与定时巡检

脚本定位问题本身已经完成了大半,但如果每次都要等到磁盘满了才跑脚本,那运维体验还是太被动。我建议把这个脚本接入定时任务,比如每天凌晨跑一次,并把结果追加到日志文件,供后续回溯。

0 2 * * * /usr/local/bin/disk_audit.sh / >> /var/log/disk_audit.log 2>&1

同时做一个简单的告警判断,比如空间使用率超过85%或者inode使用率超过90%时触发提醒。要注意df -h输出中最后一行是整体使用情况,处理起来并不方便,我通常用df -Pawk来提取数值列做判断。提取时注意多块磁盘的情况,可以去掉-h参数用-P固定输出格式,数值也是以KB为单位的纯数字,方便计算:

df -P "$MOUNT_POINT" | awk 'NR==2 {if ($5+0 >= 85) print "磁盘空间告警: " $5}'

inode的告警类似,用df -Pi输出inode情况,同样用awk判断。

6.2 关于自动清理脚本的边界思考

看到这里,可能有人会问:既然诊断脚本能跑,能不能再写一个自动清理脚本,把日志、临时文件、缓存一键清掉?我的建议是谨慎。

自动清理的风险在于“误删”。比如脚本里如果包含find /var -type f -mtime +7 -delete这样一刀切的逻辑,很可能把正在使用的PID文件、socket文件或者某个应用精心生成的配置删掉。一旦服务起不来,损失远超磁盘空间本身。

我的经验是:诊断自动化、清理半自动化。自动化负责发现问题并通知人,人确认后执行清理操作。如果服务器规模大、出问题的频率高,可以先把“清理”动作限定在明确安全的范围内,比如journald日志(它自带系统级保障)、包管理器缓存(最多重新下载)、明确的归档目录等。对待有业务含义的数据文件,始终保留人工确认环节。

7. 常见问题速查表与排查口诀

把前面讲的排查经验整理成一张速查表,方便遇到问题时快速定位。以下是我自己在实际支持中经常对照的表格:

现象可能原因首选排查命令推荐处理方式
df显示使用率超高,du统计相差很大文件被删除但进程未释放lsof +L1重启进程或截断/proc/<pid>/fd/<fd>
磁盘还有空间,但创建文件报No space leftinode耗尽df -i清理小文件、调整inode策略
journal相关目录持续增长journald未限制大小journalctl --disk-usage配置SystemMaxUse
日志目录持续膨胀缺少logrotate配置ls -lh /var/log为对应服务配置logrotate
某个目录空间突然暴涨服务异常或临时文件堆积du -h -x -d 1 <目录>定位子目录后清理
磁盘使用率忽高忽低可能是缓存或临时文件被定时清理观察df时间序列结合cron任务和脚本审计

排查时我习惯记一句口诀:先看水位,再找界碑,确认文件锁,修完记得配。“水位”就是df和df -i的整体使用情况,“界碑”指挂载点边界,“文件锁”指被进程占用的已删除文件,“修完配”是提醒自己处理完眼前问题后把日志轮转、告警阈值配好。这十六个字基本覆盖了90%场景的排查路径。

8. 最后分享两个小技巧

唠叨了这么多,最后把我实际踩过坑、也最想推荐的两个小技巧写出来。

第一个是关于du的进度问题。当服务目录特别大时,执行du会扫描很久,看起来像卡死了。其实du是在正常工作,只是没有进度提示。如果等得着急,可以在另一个终端用lsof | grep du看它在读哪些目录,或者直接开-v参数看输出。不过更实用的方式是对有问题的目录做并行化扫描。du本身是单进程的,但可以:

find /var -xdev -mindepth 1 -maxdepth 1 -type d -print0 | xargs -0 -P 4 -I {} du -sh {}

-P 4表示4个进程并行统计,速度快不少。但要注意并行du对IO压力的影响,如果服务器本身负载已经很高,并行会加剧磁盘争抢。

第二个是关于大文件搜索的-size单位。find的-size参数中,c表示字节,k表示KB,M表示MB,G表示GB,默认单位是512字节的块。新手最容易犯的错是写-size 500M发现没匹配到任何文件,其实不是没大文件,而是正在搜索的文件都不到500M。可以先放宽阈值比如-size +100M试一遍,再逐步收紧。

工具和方法本身都不复杂,复杂的是在出问题时快速判断该用哪一把钥匙。多测多积累,遇到类似问题就有感觉了。

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

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

立即咨询