不知道你有没有遇到过这种情况:磁盘快满了,df -h 看容量只剩 5%,你最想做的第一件事一定是找出到底哪些目录在“吃”空间。习惯性地敲一句du -sh /data/* | sort -hr,然后屏幕就像卡住一样,光标闪烁好几分钟,甚至十几分钟都出不来结果。你要是管过几百 GB 甚至几 TB 的存储,一定对 du 这种“慢条斯理”的扫描方式印象深刻。这次我就围绕 Linux 下 du 命令的并行化(parallel)思路展开,聊聊我是怎么把一个 600 万文件的目录树扫描时间从半小时压到几分钟的。
先说结论:du 慢不是它笨,而是它的工作方式本身就是“单线程”地把整个目录树过一遍,对每个文件、每个目录都做一次 stat。这种元数据密集型的任务,在多核机器上完全是浪费资源。而我们要做的,就是让多个 du 进程同时去扫不同的子树,再在最后把结果合并起来。听起来简单,但真正实操时会有分片不均、单位换算、硬链接重复统计、缓存干扰等一系列坑。这篇文章会把每一步的原理、命令、实测结果和踩坑经验都讲透,适合所有在 Linux 服务器上排查磁盘空间、写自动化运维脚本的读者参考。
1. 单线程 du 慢在哪:存储扫描的完整链路
要理解怎么给 du 做并行,先得搞清楚它为什么慢。别急着抄命令,先把底层机制看明白,后面遇到性能问题你才能自己判断到底该加并行度,还是该换思路。
1.1 一条 du 命令背后的系统调用链
du 的核心逻辑其实很朴素:它从你指定的路径开始,用文件系统遍历接口(fts)递归读取目录项,对每一个文件和目录分别执行stat或lstat,拿到st_blocks字段——这个字段表示文件实际占用的磁盘块数,单位是 512 字节。最后把所有块数累加,乘上 512,就是你要的目录大小。
这就意味着,一个包含 N 个文件的目录树,du 至少要发起 N 次readdir(读取目录项)和 N 次stat(获取文件元数据)。用strace -c可以看得很清楚:
bash复制strace -c du -sh /var/cache
输出最后几行会类似这样:
code复制% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
65.32 0.842931 14.321 58912 newfstatat
23.18 0.299112 23.110 12940 getdents64
5.41 0.069822 2.314 30178 close
...
这里newfstatat就是现代 glibc 里 stat 的落地实现,getdents64就是 readdir 在 64 位系统上的系统调用。你看到了吧,几十万次系统调用,全都花在“读目录项”和“取元数据”上了。CPU 计算量几乎可以忽略,真正的成本在系统调用和元数据 I/O。
拿生活场景打个比方:这就好比一个图书管理员要盘点一整座图书馆,但他不推车批量登记,而是每本书都跑到书架前翻一下标签,记下厚度,然后回来写一笔。哪怕书放得再整齐,这种一本一本来的方式也快不到哪去。
1.2 决定扫描速度的关键因素:目录索引、inode 缓存与 I/O 模式
同样是 stat,为什么有的机器扫得很快,有的机器让人抓狂?三个因素在起作用。
第一是目录索引结构。ext4 用 htree(哈希树)索引大目录,xfs 用 B+ 树。优秀的索引结构让 readdir 在目录项极多时依然能保持不错的性能。但这只是让“翻目录”变快了,“取文件元数据”依然要去 inode 表里定位。
第二是 inode 缓存命中率。stat 需要把目标文件的 inode 加载到内存。如果目录树特别大,inode 数量超过了系统内存能缓存的范围,每次 stat 都可能触发实际的磁盘元数据读取。这就是为什么你以为“文件都放在那里”却还是慢——元数据没有常驻内存,每次都是冷读。
第三是存储介质的随机读能力。目录树里的文件在物理磁盘上并不是按遍历顺序线性排列的。单线程 du 的 stat 请求在 HDD 上基本等价于海量随机小 IO,磁头来回摆动,速度自然惨不忍睹。SSD 上没有寻道代价,情况会好很多,但依然受限于元数据读放大和系统调用开销。
还有一个大家常忽略的点:du 统计的是“实际占用块数”,不是文件逻辑大小。你看到某个目录 du 结果比所有文件加起来还大,或者比 ls 看到的文件总大小小很多,都是正常现象。稀疏文件、文件系统块大小、目录自身占用的块,都会影响最终数值。这个底层认知对后面理解并行和合并结果很重要。
明白了 du 慢的本质,就可以进入正题:怎么让它快起来。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 并行化的第一层:du 的多目录参数与 xargs -P 基础组合
并行 du 的思路其实一句话就能讲清楚:把一个大目录树切成多个小目录树,每个小树交给一个独立的 du 进程去扫,最后合并结果。
2.1 一个被忽略的事实:du 本身可以同时接收多个路径
很多人不知道,du 命令后面可以跟多个路径参数,比如du -sh /data/a /data/b /data/c。它能正常工作,把每个路径的大小都列出来。那你可能想问:既然 du 支持多路径,是不是天然就是并行的?
答案是否定的。GNU coreutils 的 du 在遍历多个目录时,虽然没有严格保证顺序,但本质上仍然是同一个进程、同一个遍历控制流在处理。它不会因为你传了 10 个目录就 fork 出 10 个进程。你可以这样验证:
bash复制du -sh /data/* | wc -l
time du -sh /data/*
你会看到它是逐个输出的,总耗时接近所有目录耗时之和。所以想让 du 真正并行,不是靠传参数,而是靠外部手段开多个进程。
2.2 xargs -P 实现目录级并行
最经典、通用性最强的并行方案就是find配合xargs -P。
先给你一个可以直接用的命令:
bash复制find /data -mindepth 1 -maxdepth 1 -type d -print0 | xargs -0 -P 4 -I {} du -sh {}
拆开来看:
find /data -mindepth 1 -maxdepth 1 -type d -print0:列出 /data 下的第一层子目录,-print0让输出用空字符分隔,避免目录名里有空格或换行导致后续解析错误。xargs -0:告诉 xargs 输入是用空字符分隔的。-P 4:开启 4 个并发进程。-I {}:把每个输入项替换到后面的命令里,这里就是du -sh {}。
这条命令的效果是:/data 下有 100 个子目录时,系统同时最多跑 4 个 du 进程,每个进程负责一个子目录的完整递归统计。等所有目录都扫完,你得到 100 行输出,每行是一个子目录的大小。
在目录数量适中(几十到几百个)的情况下,这个方案已经够用了。但有个问题:不同子目录的大小可能天差地别。一个 500GB 的大目录会拖住一个 worker 很长时间,其他 3 个 worker 可能早就扫完了,整体耗时被最慢的那个目录决定,专业点叫“长尾效应”。
2.3 GNU parallel 在超大目录树上的优势
如果你的机器上装了 GNU parallel,处理长尾和任务分配会更舒服。安装方式不说了,CentOS/RHEL 系是dnf install parallel,Ubuntu 是apt install parallel。核心优势有两个:一是它能在任务数量远大于并发数时做动态调度,二是--line-buffer能保证每个任务输出整行完整显示,不会被并发输出切碎。
基础用法:
bash复制find /data -mindepth 1 -maxdepth 1 -type d | parallel -j 8 --line-buffer du -sh
这里不用-I,parallel 默认会把输入项追加到命令末尾,du -sh会自然接收到目录路径。
如果目录数量特别多,比如有几万个,建议先把目录列表写进文件再喂给 parallel,避免命令行参数过长:
bash复制find /data -mindepth 1 -maxdepth 1 -type d > /tmp/dirlist.txt
parallel -j 8 --line-buffer du -sh < /tmp/dirlist.txt
加了--progress还能看到实时进度:当前进度、剩余任务数、平均耗时。这一点在生产环境排查时真的很实用,至少你能知道还要等多久,而不是对着屏幕发呆。
不过说到这里你大概已经意识到:仅仅并行扫第一层子目录,还远没有把并行化的潜力压榨完。真正快起来的关键,是合理的分片策略。
3. 面向实战的分片策略:按目录粒度与按规模分层
并行本身不复杂,复杂的是“怎么切”才能让每个 worker 干的活差不多多。如果切得不均匀,并行度再高也白搭。
3.1 先粗扫后精扫的二层模型
我最推荐的做法是“先粗扫,后精扫”,也叫二层模型。第一步用单线程的du -d1快速拿到所有一层目录的大小排序,第二步只对最大的几个目录做并行深扫。为什么这样做?因为磁盘占用通常高度集中,20% 的目录占了 80% 的空间。把大的目录拆出来单独并行处理,小的目录直接沿用粗扫结果,整体速度提升是最明显的。
先粗扫:
bash复制du -d1 -x --block-size=1M /data 2>/dev/null | sort -hr | head -20
这里有几个参数值得说明。-d1是--max-depth=1的简写,只统计一层子目录大小,速度比完整深扫快得多。-x是--one-file-system,防止万一 /data 下挂着其他文件系统,把外部存储也扫进来。--block-size=1M让输出以 MiB 为单位,方便排序和后续脚本处理。后面的2>/dev/null是为了过滤掉因为权限不足产生的噪音输出。
拿到排序结果后,比如发现 /data/applogs 有 1.2TB,占绝对大头。那就把这个目录单独拎出来,对其内部子目录做并行扫描:
bash复制find /data/applogs -mindepth 1 -maxdepth 1 -type d -print0 | xargs -0 -P 8 -I {} du -s --block-size=1M {}
这里我故意把du -sh换成了du -s --block-size=1M,原因后面在汇总部分会专门讲,现在你先记住:脚本处理时尽量不要用人类可读格式,用固定块大小才能计算。
3.2 目录数过多时的输入分片技巧
有时候顶层目录不多,但某个大目录内部可能有几千上万个子目录,而且大小相对均匀,没有明显的大块头。这时候直接按一层目录并行,每个 worker 处理的量差不多,效果也不错。
另一种情况是目录树层级深、小目录多,你想按“文件数量”分片而不是按“目录粒度”分片。思路是用 find 列出所有文件,然后按固定行数切成多份,每份喂给一个 worker 汇总。比如:
bash复制find /data -type f -print0 > /tmp/allfiles.txt
# 按行数切分
split -l 100000 -d /tmp/allfiles.txt /tmp/files_part_
然后对每个分片用类似du --files0-from=/tmp/files_part_XX --total的方式统计。这个方法对“目录层级极深、单个目录文件数不多”的场景很有效,因为它的并行粒度是文件而不是目录。缺点是du --files0-from输出的结果只有总大小,没法直接看目录级明细,只适合你想知道一个超大路径集合的总大小。
3.3 为什么并行度不是越高越好
并行度提上去之后,收益会出现明显的边际递减,甚至负数。我第一次试的时候直接开了-P 32,以为 32 个进程一定能快到起飞,结果比 -P 8 还慢。原因有三个。
一是元数据锁竞争。文件系统在维护目录项和 inode 状态时需要加锁,大量并发 stat 会在内核里争抢锁。ext4 和 xfs 虽然都做了目录级分桶锁优化,但几十个进程同时扫同一个目录树时,锁竞争依然会吃掉大量 CPU。
二是存储队列饱和。HDD 的 IO 队列深度是有限的,你发再多请求进去也是排队,反而增加调度开销。SSD 虽然并发能力强,但元数据读取本身就有放大,超过一定并发以后收益趋平。
三是系统调用和内存回收开销。每个 du 进程都要维护自己的遍历状态,进程数太多会导致上下文切换变高,page cache 也可能因为同时读大量目录项而被反复淘汰。
根据我的实测,机械硬盘上并行度在 4~8 之间通常就能拿到 80% 的收益;NVMe 固态上 8~16 还能有一点提升,但边际已经很小。所以我的建议是:先-P 4,观察耗时和iostat,再逐步往上加,别一上来就到 16 或 32。
分片策略解决了,你手上已经有一堆 du 的输出。下一个坑马上出现:怎么把几十个并行任务的输出正确汇总成一个总大小和整洁的 top 列表。
4. 结果汇总、单位陷阱与重复统计的三个坑
并行 du 的输出和单线程 du 有一点本质区别:单线程 du 是同一个进程内部累加,天然去重;并行 du 是多个独立进程各自统计,合并时你必须自己操心去重和单位换算。这里坑很多,我一个一个说。
4.1 并行输出合并与 awk 聚合
并行任务结束后,最理想的输出是每一行一个目录的统计结果,格式固定为“大小 路径”。标准 du 输出是大小 + Tab + 路径,Tab 是分隔符。所以你可以用 awk 按 Tab 分割做聚合:
bash复制# 假设所有并行结果都写进了 per_dir.txt
awk -F '\t' '{sum[$2]+=$1} END {for (path in sum) printf "%d\t%s\n", sum[path], path}' per_dir.txt | sort -nr | head -30
-F '\t'是关键。为什么不用默认空格分割?因为路径里极可能有空格,用空格分割会把一个路径拆成多个字段,聚合就全乱了。用 Tab 分割时,路径里的空格无影响,唯一会出问题的是路径本身包含 Tab 字符,这种场景极少,遇到了可以提前用find -print0配合 Perl 脚本处理,这里不展开。
如果你有多个并行批次的结果需要合并,比如“粗扫得到的 top 大目录 + 大目录内部并行子目录结果”,可以把所有结果文件 cat 在一起,再统一走上面的 awk 聚合。因为同一路径可能在粗扫和精扫里都出现过,聚合时它的 size 会被累加。你要注意别把重复的部分算两次,所以通常的做法是:粗扫结果里只保留小目录,大目录用精扫结果替换。
4.2 单位换算:-h 好看但不可算
这是新手最容易踩的坑。du -sh输出的是人类可读格式:
code复制1.2G /data/applogs
856M /data/backup
看起来清爽,但只要你把这些输出喂给 awk 做数值相加,数字部分只会提取到1.2和856,单位完全被忽略,最后加出来是 857.2,还挂着个不存在的“G”,数值完全无法使用。
所以在任何需要脚本汇总的场景里,统一用固定块大小:
bash复制# 以 MiB 为单位
du -s --block-size=1M /data/applogs
# 以 KiB 为单位
du -s -k /data/applogs
--block-size=1M在 GNU coreutils 里表示 MiB(1024×1024 字节),--block-size=1MB才是十进制兆字节。别小看这个区别,500GB 和 465GiB 的差距在生产环境里足以让你做出错误判断。我习惯用 MiB,因为 du 的原始单位 512 字节块正好能整除 2048,不会出小数。输出示例:
bash复制du -s --block-size=1M /data/applogs
123456 /data/applogs
这个 123456 就是 MiB 数,后面可以直接排序、累加,最后除以 1024 变 GiB。
4.3 硬链接、挂载点边界与跨文件系统统计
并行 du 的第三个大坑是重复统计。
硬链接的情况比较隐蔽。GNU du 在单个进程内默认只统计硬链接一次,也就是说同一个 inode 在同一个目录树里出现多次,不会重复计入。但当你并行起多个 du 进程,每个进程只看到自己负责的那一小块目录树,它无法感知其他目录树里还有文件指向同一个 inode。结果就是:同一个 inode 的内容被重复统计了多次。
怎么发现这个问题?du -d1 /data之后,把所有一层目录的大小加起来,再对比du -s /data,如果前者明显大于后者,就说明有跨目录的硬链接存在。想精确处理,得先用find -samefile找出硬链接组,再决定哪个目录归属。不过说实话,排查磁盘占用时我们关心的是“哪些目录看着大”,硬链接重复统计不影响排序,只要你别在报告里写“精确到了字节”就行。
挂载点边界是另一个更常见的坑。假设 /data 下有个目录 /data/mnt,它是一个独立挂载的文件系统。如果粗扫时用了du -d1 /data,这个外部挂载会被算进 /data 的大小;如果你再单独跑一个并行任务去扫 /data/mnt,它的大小又会被单独计一次,最终汇总时被算了两遍。解决方法是所有 du 命令统一加-x,保证只在同一个文件系统内统计。另外,千万别对 /proc、/sys、/dev 这类虚拟文件系统执行 du,轻则输出一堆无意义的 0,重则因为伪文件系统的特殊行为导致进程卡住。扫根目录时建议显式排除:
bash复制du -x --exclude=/proc --exclude=/sys --exclude=/dev -d1 /
汇总和去重的问题处理完,你已经能拿到一份还算靠谱的报告了。但如果你想向同事证明“并行确实快”,或者你发现并行之后效果不明显,那就要考虑缓存和存储差异的影响了。
5. 实测数据与干扰因素:缓存、文件系统与存储介质
我最早做这个实验的时候,第一次跑并行 du 快得飞起,第二次跑更快,差点以为自己发现了性能神谕。后来才反应过来,是 page cache 把目录项和 inode 都缓存了,第二次跑根本不需要碰磁盘,自然快到离谱。所以谈性能,必须先说清楚缓存。
5.1 一次带缓存的对比测试过程
如果你想自己复现对比,请严格按照下面的流程来。前提是你有 root 权限,并且目标目录的数据对你来说是只读访问的。
先清理缓存,确保每次测试都是从冷缓存开始:
bash复制sync
echo 3 > /proc/sys/vm/drop_caches
echo 3表示清空 page cache、dentries 和 inodes。执行后第一遍 du 的结果才是磁盘真实性能。然后分别跑单线程和不同并行度的测试:
bash复制time du -d1 --block-size=1M /data > /dev/null
time find /data -mindepth 1 -maxdepth 1 -type d -print0 | xargs -0 -P 2 -I {} du -s --block-size=1M {} > /dev/null
time find /data -mindepth 1 -maxdepth 1 -type d -print0 | xargs -0 -P 4 -I {} du -s --block-size=1M {} > /dev/null
每次测试前都需要重新echo 3 > /proc/sys/vm/drop_caches,否则上一次的结果会污染下一次。
我拿一台测试机(HDD 7200rpm,2000 个目录,约 50 万文件)跑出来的数据大概是这样的:
| 并发数 | 冷缓存耗时(秒) | 热缓存耗时(秒) |
|---|---|---|
| 1 | 438 | 12 |
| 2 | 261 | 9 |
| 4 | 178 | 8 |
| 8 | 159 | 7 |
| 16 | 162 | 8 |
看到没有,热缓存下所有并发数都极快,根本看不出差异。只有冷缓存才能反映真实的 I/O 性能。另外你会发现 P8 之后不仅没继续变快,P16 甚至略有回升,这就是前面说的锁竞争和队列饱和。
如果你用的是 NVMe 固态,并发收益会小很多,但依然有。还是那台机器换到 NVMe 上:单线程 62 秒,P4 31 秒,P8 29 秒,P16 30 秒。上升曲线更早进入平台期。
5.2 不同文件系统与 NFS 的并行差异
文件系统不同,并行效果也完全不同。ext4 的 htree 索引对大目录 readdir 有优化,P4 到 P8 收益明显;xfs 在某些内核版本上目录锁竞争比 ext4 更明显,高并发下提升更有限;ZFS 因为 ARC 缓存机制,目录项命中率很高,并行收益有时候会被缓存吃掉一部分——你以为并行快了,其实是 ARC 帮你挡住了读盘。
NFS 场景最特殊,也是最值得并行的场景。NFSv3 客户端没有复合操作,每个 stat 都是一次独立的网络 RTT,单线程 du 在网络延迟 2ms 的环境下扫 10 万个文件,光是 RTT 就要 200 秒,还没算传输时间。这时候并行开 16 个甚至 32 个请求,能把网络延迟掩盖掉,效果立竿见影。但要注意对 NFS 的服务端保护,并发太高可能把存储端的 CPU 打满,影响其他业务。NFSv4.1 之后有 COMPOUND 操作,本来单线程的 RTT 开销就小一些,并行收益反而没那么夸张,但对大目录仍然有效。
5.3 并行扫描对业务环境的影响
最后提醒一点:并行 du 是一把双刃剑。它加快了你获取磁盘信息的速度,但也意味着短时间内会对存储系统产生突发的大量元数据请求。在业务高峰期,这可能拖慢正常的文件读写,尤其是机械硬盘环境。
我的习惯是给扫描任务降级:
bash复制ionice -c 3 nice -n 19 find /data -mindepth 1 -maxdepth 1 -type d -print0 | xargs -0 -P 4 -I {} ionice -c 3 nice -n 19 du -s --block-size=1M {}
ionice -c 3是设置 IO 空闲优先级,nice -n 19是降低 CPU 优先级。这样扫描任务只会在系统空闲时才获得 IO 资源,不影响线上的核心业务。同时在执行期间开一个iostat -x 1,观察磁盘%util、await,如果已经到 90% 以上,果断把 -P 调小。
前面讲完了原理、策略、坑和干扰因素,最后沉淀一个可以直接拿去用的脚本,再聊聊除了 du 之外还有哪些并行扫描工具。
6. 我能直接用的脚本与扩展工具
这里给出一个完整的 bash 脚本,它实现了“先粗扫、再对 top 大目录并行精扫、最后合并输出”的完整流程。你把它存成 du_parallel.sh,加执行权限就能用。
bash复制#!/usr/bin/env bash
# du_parallel.sh - 并行 du 扫描脚本
# 用法: ./du_parallel.sh /data [TOP_N]
set -euo pipefail
TARGET="${1:-/data}"
TOP_N="${2:-10}"
# 统一使用 MiB 作为输出单位
BLOCK_SIZE=1M
LOG_FILE="/tmp/du_parallel_$$.log"
RESULT_FILE="/tmp/du_parallel_result_$$.txt"
echo "[1/3] 粗扫一层目录..."
du -d1 -x --block-size=${BLOCK_SIZE} "${TARGET}" 2>/dev/null \
| sort -hr > "${LOG_FILE}"
# 从粗扫结果中分离大目录和小目录
awk -v top_n="${TOP_N}" 'NR>1 {
size=$1; path=$2;
if (NR <= top_n + 1) {
print path > "/tmp/du_parallel_big_$$.txt"
}
}' "${LOG_FILE}"
# 对小目录结果直接保留,大目录后续用精扫结果覆盖
awk 'NR>1 {
path=$2;
if (NR <= '"${TOP_N}"' + 1) {
# 这些是大目录,跳过
} else {
print $0
}
}' "${LOG_FILE}" > "/tmp/du_parallel_small_$$.txt"
echo "[2/3] 并行精扫 top ${TOP_N} 个大目录..."
# 对每个大目录,找到它下一层子目录,用 xargs -P 并行深扫
while IFS= read -r big_dir; do
[ -d "${big_dir}" ] || continue
find "${big_dir}" -mindepth 1 -maxdepth 1 -type d -print0 2>/dev/null \
| xargs -0 -P 4 -I {} du -s -x --block-size=${BLOCK_SIZE} {} 2>/dev/null \
>> "${RESULT_FILE}"
done < "/tmp/du_parallel_big_$$.txt"
echo "[3/3] 合并粗扫与精扫结果..."
cat "/tmp/du_parallel_small_$$.txt" "${RESULT_FILE}" \
| awk -F '\t' '{sum[$2]+=$1} END {for (path in sum) printf "%d\t%s\n", sum[path], path}' \
| sort -nr | head -30
# 清理临时文件
rm -f "${LOG_FILE}" "${RESULT_FILE}" "/tmp/du_parallel_big_$$.txt" "/tmp/du_parallel_small_$$.txt"
脚本的逻辑是:先粗扫一层目录并排序,把最大的 TOP_N 个目录挑出来;对每个大目录,再列出它的下一层子目录,用 -P 4 并行精扫;最后把精扫结果和粗扫时的小目录结果合并,按路径去重聚合后输出 Top 30。这个脚本在实际生产环境我已经用了很久,唯一要注意的是路径里如果包含制表符,awk 分割会出问题,但绝大多数场景遇不到。
6.1 新一代并行 du 替代工具:gdu、dust、ncdu 对比
如果你不想自己维护脚本,也可以直接用现成的工具。这里给三个我实测过的代表:
| 工具 | 语言 | 是否并行 | 交互界面 | 适用场景 |
|---|---|---|---|---|
| ncdu | C | 单线程扫描 | TUI | 小型目录快速查看 |
| gdu | Go | 支持并行 | TUI | 中大型目录交互式排查 |
| dust | Rust | 多线程 | 命令行输出 | 快速输出 Top 目录,适合脚本管道 |
ncdu 是最老牌的交互式磁盘分析工具,扫完后用上下键浏览目录很舒服,缺点是大目录树扫描依然慢。gdu 在扫描阶段做了并行化,界面和 ncdu 类似,速度明显更快。dust 的风格更像“超快的 du”,输出带条形图,适合直接看结果。
它们不是 du 的完全替代品。原因有两个:一是 du 是 POSIX 标准命令,任何 Linux 发行版都自带,脚本里用它不用装额外依赖;二是这些工具的输出格式没有 du 那么稳定,在自动化服务里解析起来反而不方便。所以我个人建议:交互式排查用 gdu,脚本化统计用 du + xargs,两个都留着。
6.2 什么时候不该用并行 du
说了这么多并行化的好处,我也得泼点冷水。有些场景下并行 du 不仅没帮助,还可能添乱。
第一种是目录很小。总共几千个文件,单线程 du 一秒就扫完了,开并行反而增加进程调度开销,结果可能更慢。
第二种是目标存储是远程冷存储或对象存储挂载(比如 s3fs、rclone mount)。这种存储的 stat 延迟极高,一个请求可能几百毫秒甚至几秒。并行开上去,存储端的 API 请求量会暴增,可能触发限流,甚至产生额外费用。
第三种是数据正在被大量写入。你扫出来的大小是“瞬态”的,本身就不精确,并行只会让数值抖动得更厉害。这种场景不如先用单线程慢慢扫,等业务低峰再看。
第四种是你只需要一个粗略排名。du -d1 | sort -hr | head -20就够了,五分钟十分钟的等待换来的是准确排名,很多情况下完全值得。
说实话,用了这么久 du 并行,我最喜欢的组合还是“先用du -d1 --block-size=1M做粗扫,再挑最大的 5~10 个目录用 xargs 并行精扫,最后统一按 MiB 聚合”。这套组合的好处是通用性强、不依赖额外工具、执行时间可控,而且不容易因为目录规模差异导致长尾。最后再分享一个实用小技巧:脚本里所有 du 命令都加上2>/dev/null,因为总会有几个目录因为权限问题报错,你不想让几百行 Permission denied 刷屏,更不想让这些错误混进主流程里。排查磁盘空间是个低频但高价值的工作,花点时间把这一套并行流程搭建好,以后每次磁盘告警你都能比别人早几分钟定位到问题。
