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 默认 4Kxfs 可能更大。你可以用stat -f看文件系统的块大小用tune2fs -l看 ext 系列的块配置。如果做精确的容量规划这个差异必须考虑进去。提示du -b可以让du按字节统计表面大小和ls -l对齐。但注意-b会隐含--apparent-size统计的是表面大小而非磁盘占用。2.2 硬链接和稀疏文件两个让统计失真的元凶硬链接的本质是多个目录项指向同一个 inode。如果你用du统计一个包含硬链接的目录默认情况下同一个 inode 只会被计算一次——这通常是好事因为磁盘上确实只存了一份数据。但如果你用find配合-size去累加就可能把同一个文件算多次得出一个虚高的数字。稀疏文件更隐蔽。比如数据库预分配的大文件、虚拟机磁盘镜像表面大小可能是 100G但实际只占了几百兆。ls -l看到的是 100Gdu看到的是几百兆。如果你用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-depth1 /path | sort -rh | head -20。这条命令列出指定目录下每个子项的大小按从大到小排序取前 20 个。--max-depth1控制递归深度避免输出爆炸。这里有个细节sort -rh里的-h是 human-numeric sort能正确识别 K、M、G 单位。如果你用普通的sort -n4K 会排在 1M 后面因为它是按字符串比较的。这个坑我见过太多人踩。另一个高频场景是统计当前目录下所有文件的总大小但不包括子目录du -sh *会包含目录find . -maxdepth 1 -type f -print0 | du -ch --files0-from-则只算文件。后者用-print0和--files0-from-是为了处理文件名里有空格或特殊字符的情况这是生产环境里必须养成的习惯。3.2 df 的正确打开方式看水位也要看 inodedf -h看的是空间水位但很多人忽略了df -i。inode 耗尽时磁盘还有空间但就是写不进去。这种情况在大量小文件的场景下特别常见比如邮件队列、缓存目录、session 文件。我遇到过好几次磁盘使用率才 60%但服务就是报“No space left on device”最后发现是 inode 用完了。df的-x参数可以排除特定文件系统类型比如df -h -x tmpfs -x devtmpfs能过滤掉内存文件系统让输出更干净。--output可以自定义列比如df -h --outputsource,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字节、kKB、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-depth1 / 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-depth2先看两层逐步缩小范围。注意在根目录跑du时一定要加-x否则会跨到其他挂载点把网络存储、外部磁盘都算进来既慢又不准。4.2 统计某个目录下各文件类型的大小分布想知道是日志文件占空间还是图片占空间可以按扩展名聚合。思路是用find列出文件用awk提取扩展名再按扩展名累加大小。一个可用的命令是find /var/log -type f -printf %s %p\n | awk {nsplit($2,a,.); ext(n1?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 filedu 总和远小于 df 已用已删除但被进程持有的文件lsof L1df 有空间但写不进去inode 耗尽、保留块df -i、tune2fs -ldu 统计结果偏小权限不足、硬链接去重去掉2/dev/null看报错du 跑得特别慢遍历文件太多、跨文件系统加-x、--max-depthfind -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清空而不是删除能避免这个问题。这些都是实际踩过坑之后才记住的细节希望对你有所帮助。