生产服务器告警,磁盘占用率95%,作为运维或者后端开发,第一反应肯定是查清楚哪个目录吃掉了空间。这时候Linux的du命令是绕不开的基础工具。du全称disk usage,用来统计文件和目录在磁盘上的实际占用情况。别看命令本身很简单,真正能用顺手、用得明白,其实有不少门道——单位换算、硬链接、稀疏文件、挂载点、权限边界,这些细节都可能导致统计结果和你的直觉不一致。
这篇文章我从统计原理讲起,再整理日常排查方法、最容易踩的坑、以及与周边命令的组合用法,最后聊一下大目录扫描的性能控制。适合刚接触Linux的新手建立正确认知,也适合已经常用du但没细究过这些行为的运维同学参考。我尽量用实际输出和例子说话,你看完可以直接拿去用。
1. du统计的到底是什么:磁盘块、递归与“假大小”
1.1 为什么ls -l和du显示的大小总对不上
先看一个很多人问过的问题:一个文件用ls -l看是1字节,用du -sh看却是4.0K,到底谁是对的?其实两个都对,只是统计维度不同。ls -l显示的是文件逻辑上的字节长度,而du统计的是文件在磁盘上实际占用的存储块数量。
操作系统的文件系统一般以固定大小的块为单位分配空间,Ext4、XFS这类常见文件系统,数据块大小通常是4096字节。一个只有1字节的文件,逻辑上只有一个字节,但落盘时照样会占一个完整的数据块,也就是4KB。所以你会看到du输出4.0K。文件大小从1字节变成3KB,还是占4KB;到了5KB,就要分配两个块,即8KB。
这里的关键概念叫“磁盘占用”。磁盘上不存在按字节精确回收的空间,文件系统按块记账。du读取文件元数据中的“已分配块数”,再乘以每块字节数,最终得到的就是真实占用。
1.2 目录本身也是要占空间的
很多新手不知道,目录在Linux里也是一种特殊文件,它同样会消耗磁盘块。新建一个空目录,du -sh看这个目录,通常就是4.0K。这个大小用来保存目录内部的文件名、inode号等条目信息。目录里的文件越多、文件名越长,这个目录本身的体积也会增大,但增速不像普通文件那么夸张。
更关键的是,du在统计目录时会把该目录下所有子目录、所有文件累加起来一起算。也就是说,du -sh /var/log输出的是整个log目录树的总占用,不是log目录自身那几个字节。这种递归统计是du最核心的行为,也是后面排查磁盘空间时真正有价值的地方。
如果你想看的是“整个目录树一共多大”,用du -sh或du -s;如果想看目录里每个子目录各自多大,用du -h --max-depth=1。如果只把du后面直接跟一个目录路径而不加任何参数,它会把下面每一层子目录都列一遍,输出非常长,日常很少这样用。
1.3 du的默认单位、显示格式和几个基础参数
GNU版本的du默认输出单位是1024字节的块数,也就是数字后面不带单位,全是一堆以K结尾的块计数。实际上这些数字的单位是K,即“多少个1K块”。加上-h参数后,du才会自动把结果换算成K、M、G这种人类友好格式,换算关系是1024进位,不是1000进位。
要精确看字节数,可以用-B 1指定块大小为1字节,例如du -B 1一个文件会直接输出4096这种原始数值。不过日常排查用-h就够了。还有一层容易混淆的是--apparent-size,这个参数可以让du显示表观大小,也就是ls -l看到的逻辑大小。对于普通文件,它和du默认输出的磁盘占用会有差异,这个差异恰恰能帮助我们理解稀疏文件的场景,后面会专门讲。
基础参数里,-s是汇总,只给最终总和;-a会列出所有文件而不是只列目录;--max-depth=N控制输出层级,比如--max-depth=1只显示当前目录下一级子目录的总占用。这几个参数是日常用得最多的。
2. 实战排查:三层递进定位磁盘占用大户
2.1 第一层:先确认是哪个分区出了问题
拿一台报警服务器举例,第一步永远是df -h,而不是直接du扫盘。df -h会告诉你每个挂载点的容量、已用、可用、使用率。这一步的核心价值是缩小问题范围:如果/分区满了,就排查根文件系统下的目录;如果/data分区满了,就只需要处理/data这块,不相关的地方完全不用碰。
注意df和du的分工差异:df站在文件系统管理的全局视角统计块组、超级块、保留块等整体占用;du站在文件视角,逐个inode累加文件大小。前者像看一个仓库的总库存,后者像把仓库里每件货登记一遍。这种底层差异直接导致两个命令的结果天然会有偏差,方向是df的数字通常会大于du统计的花销总和。
2.2 第二层:从根目录向下找大目录
确认了分区之后,在根目录上用这条命令:
du -x -h --max-depth=1 / 2>/dev/null | sort -h -r | head -20这里有几个参数我逐个解释。-x表示不要跨越到其他文件系统。假如你机器上/data单独挂了一块盘、/home也单独挂载,不加-x的话du会把它们内部的内容也统计进来,看起来像是根分区吃满了,实际却是别的分区占空间,很容易误判。-x配合从小到大、从根到叶的排查,能保证每个数字都只落在当前文件系统内。
--max-depth=1控制只输出根目录下一级子目录的总量,不会把子目录的孙级内容全部打印出来。sort -h -r按人类可读的大小倒序排列,head -20只取最大的二十项。如果/分区下面堆了十几个目录,这条命令能立刻让你看到到底是/var、/usr还是/root在膨胀。
实测一下,输出可能长这样:
15G /var 8.2G /usr 2.1G /root 1.5G /opt2.3 第三层:逐层钻取,锁定具体目录
拿到上一层的榜单之后,下一个动作是进入最大的目录继续钻。比如/var占了15G,那就执行:
du -x -h --max-depth=1 /var 2>/dev/null | sort -h -r | head -20很可能下一步会看到/var/log占掉了大头。再往里走,/var/log/journal或者某个服务日志目录就暴露出来了。这一层一层往下钻的过程,本质上是在不断缩小排查半径,每次只关注当前层级最大的那个分支。
如果钻到某一层发现大文件居多而不是大目录,比如日志文件不小但目录总占用却不大,说明问题主要集中在少数单文件上。这时候find就该登场了,比如查找指定范围内大于500MB的文件:
find /var -xdev -type f -size +500M -exec ls -lh {} \;清理之前务必确认文件是不是还处于写状态,最稳妥的做法是把日志先truncate成空文件,而不是直接rm掉。这类处理后面组合部分我会再展开。
2.4 几个高频参数组合的快查
du -sh <路径>:只看一个目录树总占用,最常用。du -h --max-depth=1 <路径>:显示目录+子目录占用的单层清单。du -a -h <路径>:把目录下的所有文件也逐一带上,用于定位文件级大块头。du -t 2G:只显示占用大于2G的项,小目录直接过滤掉,输出更干净。du --exclude=".cache" --exclude="*.log" <路径>:排除指定目录或模式后再统计。du --block-size=1 <文件>:以字节为单位显示精确占用。
这些参数组合不是孤立的,排查时经常连续用几条命令配合。我的习惯是先把第二层的榜单打出来,再针对最大目录逐层向下,而不是一上来就全盘扫。
3. du最容易被误解的五个边界场景
3.1 硬链接:为什么同一个文件会“统计出两份”
硬链接是du统计里最容易让人迷惑的情况。假设你执行了ln a.txt b.txt,创建了一个硬链接,两个文件名指向同一个inode,数据只有一份。此时你分别用du -sh a.txt和du -sh b.txt,会各自看到一个4K的占用。但如果用一个目录把它们包含进去,再du整个目录,却不会出现8K,而是只统计一份,因为GNU du在递归统计同一棵目录树时,默认把同一个inode的多个硬链接当作一份记账,不会重复计算。
这个设计本身是合理的,它让“目录树总占用”真正反映物理磁盘的消耗。但反过来也会造成一个经典误区:如果你图省事,用du -sh dir1 dir2 dir3手动加起来,准备估算两个目录合并后的占用,而其中存在跨目录的硬链接,手动求和就会虚高。要强制重复计数可以用--count-links参数,但在绝大多数日常场景里,默认的去重行为恰恰是我们要的。
备份目录、邮件存储、Git对象库这类硬链接出现频繁的地方,统计前最好先想清楚自己到底要“逻辑上的多个文件占用”还是“物理磁盘上的实际占用”。默认行为对应后者。
3.2 稀疏文件:50GB的逻辑文件可能只占几KB
稀疏文件是另一个反直觉的例子。文件系统允许你创建一个“空洞”文件,只记录文件长度,不真正给这些空洞分配磁盘块。比如创建一个50GB的稀疏文件:
truncate -s 50G sparse.imgls -lh会显示这个文件有50G,但du -sh大概率显示0或者几KB,因为实际落盘的块极少。这类文件常见于数据库快照、虚拟磁盘镜像、某些下载工具的临时文件、以及部分日志系统的预分配场景。
碰到ls和du数字差异特别大的文件,先不要质疑命令坏了,用file、stat或者du --apparent-size对比确认是不是稀疏文件。复制这类文件时,如果普通cp可能会把它展开成完整50G,反而浪费磁盘,应该用cp --sparse=always保留稀疏特性。
3.3 文件已被删除但磁盘不释放:你du不到却真实存在
这是运维场景中非常经典的一类故障:df -h显示某个分区100%满,但用du从根目录开始把整个文件系统翻遍,统计出来的总和远小于df的输出。明明删掉了一个大文件,空间却没有回来。
根本原因通常是有进程仍然持有这个文件的文件句柄。Linux下删除文件只是把目录项摘除,如果有进程打开了它,inode本身不会被立刻回收,对应的磁盘块继续被占用。du是按文件名遍历目录来统计的,文件名都没了,du自然看不到这部分占用,但df知道块还没释放。
排查命令很直接:
lsof +L1+L1的含义是只列出link count小于1的文件,也就是那些已经被删除但还被进程打开的文件。看到结果后,对应处理方式无非两种:重启持有句柄的服务,让进程关闭句柄;或者在极端紧急情况下考虑kill掉相关进程。空间会立刻回来。
3.4 扫描边界:挂载点、/proc和-x的关系
du默认会递归进入所有子目录,包括那些实际上是其他文件系统的挂载点。这意味着du /的时候,如果/data是独立分区,/data下的内容会被计入根文件系统的“目录树占用”里,造成根分区统计数据虚高。反之,如果只想看某个目录在当前文件系统内的真实消耗,-x这个选项就非常关键。
还有一个更隐蔽的问题是虚拟文件系统。直接du -sh /proc或者du划到/proc内部,root用户可以读一堆动态数字和端口信息,但这些不代表磁盘真实占用,甚至可能让命令挂起或产生无意义的输出。日常扫描根目录时,务必习惯性带上-x,它同时帮你跳过proc、sys、dev这些不需要统计的伪文件系统。
3.5 权限不足:普通用户扫根目录时的隐形缺口
普通用户执行du /会在一堆子目录上收到Permission denied,这些目录没有读取权限时,du既取不到它的大小,也不会继续深入。结果就是统计出来的“总占用”比真实情况小。命令输出里会出现大量cannot access的提示,如果忽略了,很容易误判空间不大。
处理办法很简单:要么对特定目录有理有据地用sudo执行,要么在执行时加上2>/dev/null把错误输出丢掉,只看有效统计。但丢掉错误输出会掩盖细节,我的习惯是先不加过滤跑一次,看清哪些目录没权限,再决定是否用sudo或排除。真正做自动化采集时才会统一2>/dev/null,保持日志干净。
4. du和周边命令组合:从看大小到做清理
4.1 sort -h怎么让du输出变成可读榜单
GNU sort自带-h参数,支持按人类可读的数字后缀排序。这条命令是排查空间的神器:
du -h --max-depth=1 /data 2>/dev/null | sort -h -r | head -30如果直接用sort -n,排序的是纯数字,对4.0K、32M、2.1G这种带单位的文本没意义,结果会乱掉。sort -h会正确识别K、M、G、T后缀,从大到小排得明明白白。这个能力是基于GNU coreutils提供的,Linux发行版基本都有,只要sort --version不低于8.16基本没问题。
拿到榜单后,如果要长期观察几个关键目录的增长,可以把同样的命令写进一个脚本,输出带上时间戳,按天存档。
4.2 du管总账,find管大文件
du擅长算目录树的总量,但它不会替你精确找出某个大文件的完整路径。找大文件是find的专场。典型场景是:du已经定位到/opt/app占用最高,接下来想知道这个大占用是无数小文件堆积还是某个巨型文件导致,用一条find直接锁文件:
find /opt/app -xdev -type f -size +200M -exec ls -lh {} \;这里的size +200M表示大于200MB,-exec ls -lh是打印出人类可读大小,-xdev仍然是不跨文件系统。把“目录总账”和“文件级定位”组合起来,一次磁盘告警通常五六条命令就能彻底定位。
如果既要按目录看、又想要文件粒度,可以退一步用du -a -h然后再sort。但文件数量多的时候输出会非常长,远不如find来得精准。两个命令的分工是du管“哪些目录在膨胀”,find管“具体哪个文件是元凶”。
4.3 df与du结果不一致:完整排查链路
df和du数值对不上,在Linux运维里概率不小,尤其是大分区、高负载、频繁写日志的机器。我建议按下面的顺序排查。
第一步,确认df -h里的使用率,记下具体分区。第二步,用du -x -h --max-depth=1在该分区根目录跑一遍,把这些子目录占用加起来。第三步,如果du总和明显小于df已用空间,立刻执行lsof +L1查已删除但被打开的文件,这是最高频原因。第四步,如果查不到deleted文件,检查文件系统保留块和元数据开销。
Ext4默认会为root保留5%的块,避免碎片和紧急情况,这部分不算可用空间,所以df显示100G的分区,可用可能只有95G。用tune2fs -l /dev/设备名可以看reserved block count,把保留比例调小能释放一部分空间,但不建议生产环境乱调。再加上du本身不统计超级块、日志节点这类文件系统元数据,df比du整体偏大是正常现象,偏差一般不会到几个G。如果差得离谱,料定问题出在deleted文件或者某些隐藏的挂载点上。
4.4 ncdu这种交互式工具值不值得用
ncdu是一个非常聪明的du封装,它复用du的扫描数据,把它变成终端里的交互式界面。装好后跑一句ncdu /data,就能像文件管理器一样在各层目录间移动,实时看到每个目录的占比,还能按大小排序。对一次性排查和探索型清理来说,效率比命令行逐个du高不少。
安装很常规,发行版软件源里基本都有。使用上我只提醒两点:第一,删文件前务必按两下方向键确认选中的目标,这个界面按d会直接发送删除动作,看清路径再动手;第二,它扫描的同样是递归统计,大目录下初次进入会有一段等待时间,和du本身的性能瓶颈一致,别觉得是卡死了。ncdu适用场景是“人肉追踪一个会话内彻底搞清目录结构”,而普通cron脚本采集用du就够了,因为ncdu交互式设计天然不适合无人值守。
5. 大目录下的性能控制与自动化
5.1 为什么百万级小文件的目录会卡住
du本质上是顺序遍历目录树,对每个文件、每个目录做一次stat系统调用。性能瓶颈不在于数据量多少,而在于文件数量和文件系统元数据读取成本。一个包含上百万个小文件的目录树,哪怕总体积只占几个G,扫描时间也可能以分钟甚至小时计。经典的案例是Yarn的缓存、node_modules、邮件存储和某些不清理的日志目录。
这里有个关键认知必须澄清:--max-depth参数只控制输出层级的多少,并不减少扫描工作量。du为了拿到任意一级目录的总占用,必须递归到最深层,把整个子树走完。真正能加速的办法只有缩小扫描范围,或者用--exclude把不需要的目录排除掉。
5.2 给du设置IO和CPU优先级
在业务繁忙的机器上直接跑du,可能把磁盘IO推高,影响正常业务。尽量避开高峰期扫描,如果必须在白天跑,建议通过ionice和nice降低影响:
ionice -c3 nice -n 19 du -xsh /data-c3表示idle调度,只有磁盘空闲时才真正读写,对业务影响最小。nice -n 19把进程的CPU优先级降到最低,适合低频次后台统计。如果扫描量实在太大,还有一个蠢办法但有效:先看df确认分区整体容量,然后只对最可疑的几个大目录分别du,缩小扫描范围比调优先级更实在。
5.3 并行统计大目录的思路和代价
du本身是单线程的,在目录设计比较合理的场景下可以手动并行。思路是先把大目录按一级子目录拆开,用xargs -P并行执行多个du -s,最后汇总。例如:
ls -d /data/* | xargs -P 4 -I{} du -xs {} 2>/dev/null然后再用awk把结果相加。这种做法的收益在子目录之间相互独立时非常明显,能把扫描时间从串行的10分钟压缩到几分钟。但代价是可能破坏硬链接去重逻辑:默认du去重依赖从根开始的单一遍历,按子目录拆开并行统计后,跨目录的硬链接会被重复计算。对没有硬链接的普通数据目录,这个风险基本不存在;对有大量硬链接的备份目录,并行求和数据可能偏大,结论只能用来做排序参考,不能当作精确disk usage。
5.4 用cron定期记录空间占用趋势
磁盘问题不是只有满了才需要关注,最好的处理方式是在膨胀初期就发现它。我习惯给关键目录做定期快照,写进cron:
30 2 * * * /usr/bin/du -x -s /data >> /var/log/diskusage.log 2>&1这样每天凌晨跑一次,不会干扰业务。日志里每行一个数字加时间,空间增长是几周内逐步堆上去的还是一次异常暴涨,翻历史记录立刻就知道。结合du输出配合sort生成Top目录榜单,也能留着事后复盘。这比每次告警了才被动排查轻松太多。
就实际体验来说,du这种命令翻来覆去还是那几十个参数,真正拉开差距的是对统计原理的理解和使用场景的把握。搞懂它统计的是磁盘块而不是逻辑大小,记得排查时用-x避开挂载点,看到df和du对不上先查deleted文件,再掌握几个组合命令和性能控制手段,这个工具就彻底吃透了。