ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Linux磁盘空间告急?du与df文件大小统计实战指南

Linux磁盘空间告急?du与df文件大小统计实战指南

1. 磁盘空间告急时,我第一时间会敲哪条命令

凌晨两点,监控告警说某台服务器的根分区使用率冲到了92%。这种时候没时间慢慢翻文档,我脑子里第一个蹦出来的就是du和df这对老搭档。df看整体水位,du揪出具体是谁在吃空间,这套组合拳我打了十多年,几乎没失手过。

文件大小统计这件事,听起来简单,但真到生产环境里,坑比想象中多得多。比如du默认按块统计而不是按字节,硬链接会被重复计算,稀疏文件的实际占用和表面大小能差出几个数量级,还有ls -l显示的大小和du报出来的数字对不上——这些细节如果没搞清楚,排查方向可能从一开始就是错的。

这篇文章面向的是日常需要和 Linux 服务器打交道的运维、后端开发,以及任何需要管理磁盘空间的人。不管你是刚接触命令行不久,还是已经能熟练敲find和awk组合拳,我都会把文件大小统计这件事从原理到实操拆开讲清楚。核心会围绕du、df、ls、find、stat这几个命令展开,配合管道和排序,给出可以直接复制粘贴的方案。更重要的是,我会解释每个参数背后的逻辑,让你在遇到新场景时能自己推导出该用什么命令,而不是死记硬背。

2. 先搞清楚“大小”到底指什么,不然统计全是白费

2.1 表面大小、磁盘占用、块大小,三个概念别混

很多人第一次被文件大小搞懵,是因为发现ls -l显示一个文件是 100 字节,但du报出来是 4K。这不是命令有 bug,而是它们统计的根本不是同一个东西。

表面大小(apparent size)是文件内容的实际字节数,ls -l和stat的%s显示的就是这个。磁盘占用(disk usage)是文件在文件系统上实际占了多少块,du默认统计的是这个。文件系统分配空间的最小单位是块(block),常见的是 4K。哪怕你写一个字节进去,它也得占一个块,所以 1 字节的文件磁盘占用就是 4K。

这里有个容易踩的坑:du默认会按块大小向上取整,但不同文件系统的块大小可能不一样。ext4 默认 4K,xfs 可能更大。你可以用stat -f看文件系统的块大小,用tune2fs -l看 ext 系列的块配置。如果做精确的容量规划,这个差异必须考虑进去。

提示:du -b可以让du按字节统计表面大小,和ls -l对齐。但注意-b会隐含--apparent-size,统计的是表面大小而非磁盘占用。

2.2 硬链接和稀疏文件,两个让统计失真的元凶

硬链接的本质是多个目录项指向同一个 inode。如果你用du统计一个包含硬链接的目录,默认情况下同一个 inode 只会被计算一次——这通常是好事,因为磁盘上确实只存了一份数据。但如果你用find配合-size去累加,就可能把同一个文件算多次,得出一个虚高的数字。

稀疏文件更隐蔽。比如数据库预分配的大文件、虚拟机磁盘镜像,表面大小可能是 100G,但实际只占了几百兆。ls -l看到的是 100G,du看到的是几百兆。如果你用ls的结果去做容量评估,会严重高估。反过来,如果你用du去判断一个文件是否“太大”,又可能漏掉那些表面巨大但实际占用很小的文件。

判断稀疏文件可以用du和ls的差值来估算,或者用stat的%b(占用的块数)乘以块大小,再和%s对比。差值越大,稀疏程度越高。

2.3 为什么 du 和 df 的数字经常对不上

df统计的是文件系统层面的已用空间,du统计的是目录树里文件占用的总和。两者对不上是常态,原因通常有这几个:被删除但仍有进程持有文件描述符的文件(df算已用,du看不到),文件系统保留块(ext 系列默认给 root 保留 5%),以及挂载点嵌套导致的重复计算。

排查这种差异时,我一般先用df -h确认哪个分区异常,再用du -x限制不跨文件系统,逐层往下钻。如果du加起来远小于df的已用,那大概率是有进程占着已删除的文件,用lsof +L1能揪出来。

3. 日常统计最常用的几条命令,以及它们背后的逻辑

3.1 du 的常用姿势:从总览到钻取

du是文件大小统计的主力。最基础的用法是du -sh /path,-s汇总,-h人类可读。但实际排查时,我更多用的是du -h --max-depth=1 /path | sort -rh | head -20。这条命令列出指定目录下每个子项的大小,按从大到小排序,取前 20 个。--max-depth=1控制递归深度,避免输出爆炸。

这里有个细节:sort -rh里的-h是 human-numeric sort,能正确识别 K、M、G 单位。如果你用普通的sort -n,4K 会排在 1M 后面,因为它是按字符串比较的。这个坑我见过太多人踩。

另一个高频场景是统计当前目录下所有文件的总大小,但不包括子目录:du -sh *会包含目录,find . -maxdepth 1 -type f -print0 | du -ch --files0-from=-则只算文件。后者用-print0和--files0-from=-是为了处理文件名里有空格或特殊字符的情况,这是生产环境里必须养成的习惯。

3.2 df 的正确打开方式:看水位也要看 inode

df -h看的是空间水位,但很多人忽略了df -i。inode 耗尽时,磁盘还有空间,但就是写不进去。这种情况在大量小文件的场景下特别常见,比如邮件队列、缓存目录、session 文件。我遇到过好几次磁盘使用率才 60%,但服务就是报“No space left on device”,最后发现是 inode 用完了。

df的-x参数可以排除特定文件系统类型,比如df -h -x tmpfs -x devtmpfs能过滤掉内存文件系统,让输出更干净。--output可以自定义列,比如df -h --output=source,size,used,avail,pcent,target,方便脚本解析。

3.3 ls 和 stat:看单个文件的精确信息

ls -l显示的是表面大小,ls -s显示的是块数(注意是块数不是字节数,要乘以块大小才是字节)。ls -lh加上人类可读。但ls的输出格式在不同系统上可能有差异,做脚本解析时不如stat可靠。

stat能给出最完整的信息。stat -c '%s' file拿表面大小,stat -c '%b' file拿占用的块数,stat -c '%B' file拿块大小。组合起来stat -c '%s %b %B' file就能同时看到表面大小、块数和块大小,自己乘一下就知道磁盘占用。stat的-c格式串还支持%n(文件名)、%F(文件类型)、%y(修改时间)等,做批量统计时非常灵活。

3.4 find 按大小筛选:精确到字节的过滤

find的-size参数支持按大小筛选,单位有c(字节)、k(KB)、M、G。注意-size +100M表示大于 100M,-size -100M表示小于 100M,-size 100M表示正好 100M(但因为是按块向上取整,精确匹配很少用)。

一个实用组合是找出当前目录下大于 1G 的文件并按大小排序:find . -type f -size +1G -exec du -h {} + | sort -rh。这里用-exec ... +而不是-exec ... \;,因为+会把多个文件批量传给du,减少进程创建开销,大目录下性能差异明显。

如果要按表面大小筛选而不是磁盘占用,find的-size默认就是按表面大小判断的,这点和du不同。所以find . -size +1G找的是表面大小超过 1G 的文件,哪怕它是稀疏文件实际只占几兆。

4. 把命令组合起来,解决真实场景里的统计需求

4.1 找出根分区下最占空间的 10 个目录

这是磁盘告急时的标准动作。我通常分两步:先du -h --max-depth=1 / 2>/dev/null | sort -rh | head -10看顶层,找到大头后再往下钻。2>/dev/null是为了屏蔽权限不足的报错,否则输出里会混入大量Permission denied。

如果顶层看不出明显的大头,可能是空间分散在深层目录里。这时候可以用du -ah / 2>/dev/null | sort -rh | head -30,-a会把所有文件也列出来。但这条命令在大型文件系统上会很慢,因为要遍历所有文件。更高效的做法是用du -h --max-depth=2先看两层,逐步缩小范围。

注意:在根目录跑du时一定要加-x,否则会跨到其他挂载点,把网络存储、外部磁盘都算进来,既慢又不准。

4.2 统计某个目录下各文件类型的大小分布

想知道是日志文件占空间还是图片占空间,可以按扩展名聚合。思路是用find列出文件,用awk提取扩展名,再按扩展名累加大小。一个可用的命令是:

find /var/log -type f -printf '%s %p\n' | awk '{n=split($2,a,"."); ext=(n>1?a[n]:"noext"); size[ext]+=$1} END {for (e in size) printf "%10d %s\n", size[e], e}' | sort -rn

这里用find -printf '%s %p\n'直接输出字节大小和路径,比-exec stat快很多。awk里用split按点号切分路径取最后一段作为扩展名,没有点的归为noext。最后按大小降序输出。这个脚本我在清理日志目录时经常用,一眼就能看出是.log还是.gz在吃空间。

4.3 找出大文件并生成可读的清理清单

清理磁盘时,我需要一份按大小排序的文件清单,包含路径、大小、修改时间。find配合-printf可以一次性输出:

find /data -type f -size +100M -printf '%s\t%TY-%Tm-%Td %TH:%TM\t%p\n' | sort -rn | head -50 | awk -F'\t' '{printf "%8.1f MB %s %s\n", $1/1024/1024, $2, $3}'

%s是字节大小,%TY-%Tm-%Td %TH:%TM是修改时间,%p是路径。sort -rn按字节数降序,head -50取前 50。最后的awk把字节转成 MB 并格式化输出。这份清单可以直接拿去和业务方确认哪些能删。

4.4 监控目录大小变化,定位增长源头

有时候磁盘不是一下子满的,而是某个目录在持续增长。这时候需要定时采样对比。我一般用du -sb拿字节数,存到文件里,隔一段时间再采一次,用diff或awk算差值。

du -sb /data/* 2>/dev/null | sort -k2 > /tmp/du_snapshot_$(date +%s).txt

过一小时再跑一次,然后用join或awk对比两个快照,找出增长最快的目录。-b保证是按字节统计,避免块大小带来的误差。这个思路也可以做成定时任务,每天采一次,观察长期趋势。

5. 那些年我踩过的坑和对应的排查技巧

5.1 du 报的数字比预期小很多,怎么回事

最常见的原因是权限不足。du遇到没权限的目录会跳过并报错,如果你把2>/dev/null一加,这些目录就被静默忽略了,统计结果自然偏小。排查时先不加2>/dev/null,看看有没有Permission denied,有的话用sudo重跑。

另一个原因是硬链接。du默认对同一个 inode 只算一次,如果你统计的目录里有大量硬链接,结果会比ls累加小。这是正确行为,但如果你不知道,就会觉得数字不对。用du -l可以让du重复计算硬链接,但一般不建议,除非你确实需要表面上的总和。

5.2 df 显示有空间但写不进去

前面提过 inode 耗尽,用df -i确认。还有一种情况是文件系统保留了块给 root,普通用户看到df有空间但写不了。ext 系列默认保留 5%,可以用tune2fs -m调整,但生产环境不建议随便改,保留块是为了防止碎片化和保证 root 能应急。

还有一种隐蔽情况是挂载点被覆盖。比如/data原本挂了一个大盘,后来因为某种原因没挂上,但/data目录还在,往里写其实是写到根分区了。用df /data和mount | grep data对比一下就能发现。

5.3 统计速度太慢,怎么加速

du在大型文件系统上慢是正常的,因为它要遍历每个目录和文件。加速的思路有几个:用-x限制不跨文件系统,减少遍历范围;用--max-depth限制递归深度,先看大头;用find -printf替代-exec stat,减少进程创建;如果文件系统支持,用ncdu这类工具做交互式浏览,它内部也是调du但做了缓存和增量展示。

还有一个技巧是用du的--time参数看每个目录的修改时间,结合--exclude排除掉明显不需要统计的目录,比如.git、node_modules、缓存目录。这些目录文件数量巨大但通常不是空间大头,排除掉能显著提速。

5.4 常见问题速查表

现象可能原因排查命令
du 和 ls 大小不一致块大小取整、稀疏文件stat -c '%s %b %B' file
du 总和远小于 df 已用已删除但被进程持有的文件lsof +L1
df 有空间但写不进去inode 耗尽、保留块df -i、tune2fs -l
du 统计结果偏小权限不足、硬链接去重去掉2>/dev/null看报错
du 跑得特别慢遍历文件太多、跨文件系统加-x、--max-depth
find -size 和 du 对不上find 按表面大小,du 按磁盘占用find -size加-b对比

6. 几个进阶技巧,让统计更精准更高效

6.1 用 ncdu 做交互式排查

命令行里翻du的输出毕竟不直观,ncdu是个很好的补充。它把du的结果做成可交互的界面,能按大小排序、进入子目录、直接删除文件。安装也简单,大部分发行版的仓库里都有。我一般在初步定位后用ncdu做精细清理,比反复敲du效率高得多。

ncdu的-x参数同样是不跨文件系统,--exclude可以排除指定模式。它第一次扫描会慢,但之后有缓存,再次进入很快。对于需要反复查看的场景,体验比纯命令行好很多。

6.2 用 awk 做自定义聚合

du的输出是“大小 路径”的格式,用awk可以按任意维度聚合。比如按一级目录聚合:

du -sb /data/* 2>/dev/null | awk -F'\t' '{split($2,a,"/"); size[a[3]]+=$1} END {for (d in size) printf "%12d %s\n", size[d], d}' | sort -rn

这里假设/data下是一级目录,a[3]取的是/data/xxx里的xxx。实际使用时根据路径深度调整数组下标。这种聚合方式比du --max-depth更灵活,因为你可以按任意规则分组,比如按日期、按项目名、按用户。

6.3 把统计结果接入监控

如果只是偶尔排查,手动敲命令就够了。但如果想持续观察,可以把du的结果推到时序数据库里。思路是写个脚本,定时跑du -sb拿关键目录的大小,通过 API 推送到监控系统,设置阈值告警。这样磁盘还没满就能提前介入,而不是等到告警了才去救火。

脚本里要注意的是,du本身有性能开销,不要跑得太频繁。对于变化不快的目录,一小时一次足够了。对于日志这类增长快的目录,可以缩短到五分钟,但要用--max-depth限制范围,避免全盘扫描。

6.4 处理文件名里的特殊字符

生产环境里文件名带空格、换行、引号的情况不少见。用find -print0配合xargs -0或者du --files0-from=-能安全处理。如果直接用for f in $(find ...),遇到空格就会断成多个文件名,统计结果就乱了。这个坑我在早期脚本里踩过,后来一律用-print0。

du的--files0-from=-是从标准输入读以 null 分隔的文件名列表,配合find -print0是绝配。比如find /data -type f -size +1G -print0 | du -ch --files0-from=-能安全地统计所有大于 1G 的文件总大小,哪怕文件名里有空格或换行。

7. 关于文件大小统计,我个人的几条经验

干了这么多年运维,文件大小统计这件事我最大的体会是:永远不要相信单一命令的输出。du、df、ls、stat各有各的视角,交叉验证才能得出可靠结论。遇到数字对不上,先别急着怀疑命令有问题,多半是统计口径不同。

另一个经验是,养成加-x的习惯。尤其是在根目录或者挂载点复杂的机器上,不加-x的du可能跑到网络存储或者外部磁盘上,既慢又容易得出误导性的结果。这个参数我几乎是条件反射地加上。

最后,清理磁盘前一定要确认文件是否还被进程持有。直接rm一个被进程打开的大日志文件,空间不会立即释放,df还是满的,容易让人以为删错了。用lsof +L1确认,或者用truncate -s 0清空而不是删除,能避免这个问题。这些都是实际踩过坑之后才记住的细节,希望对你有所帮助。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进