上周五晚上我在值班,监控突然弹出一条磁盘告警,/ 分区使用率飙到 95%。登录服务器后,我先执行 df -h 确认了是根分区告急、不是数据盘,然后习惯性敲下 du -sh / 想看看到底是什么东西把空间吃掉了。结果发现输出里跳出一堆 Permission denied,汇总出来的总大小明显比 df 显示的已用空间小了不少。
很多刚接触 Linux 的朋友都会困惑:du 明明带着 root 权限跑,为什么还会漏算目录?后来我才意识到,排查磁盘空间不能只靠 df 看个总量就完事,真正要把"到底是谁占了我的磁盘"这个问题刨根问底,sudo du 的组合才是那个最趁手的工具。
这篇文章我把 sudo du 命令从权限原理、统计口径、常用参数到完整排查链路一次讲透,也会分享一些我在实际运维中踩过的坑。
1. 为什么排查磁盘空间必须加上 sudo:先说清楚权限这件事
1.1 权限不够时,du 会"漏报"哪些目录
du 命令的本质是遍历目录树,读取每一个目录项和文件元数据,然后累加文件大小。但如果当前用户对某个目录没有读权限,du 就无法进入该目录进行遍历,此时它会输出一行 du: cannot read directory 'xxx': Permission denied——然后跳过这个目录。
这就出现了一个很危险的信号:你以为自己统计了 / 下的所有文件,但实际上 /root、其他用户的家目录、某些权限收紧的服务目录统统被跳过了。如果这些目录恰好是数据增长的重灾区,你拿着统计结果去排查半天,却怎么也找不到那个把磁盘撑爆的元凶。
我在测试环境里模拟过这个场景。普通用户执行 du -sh /home,因为 /home 下既有自己的目录也有其他用户的目录,统计结果往往比真实占用少一大截。而只要加上 sudo,du 就能以 root 身份读取所有目录,统计结果才真正完整。
除了家目录,实际运维中常见的"权限死角"还有这些:
/root:管理员常用目录,很多脚本的临时文件、备份包都会丢在这里。/var/spool/mail:系统邮件队列,如果不清理,会积累大量邮件文件,而且默认只有 root 和属主能读。/var/lib/mysql:数据库目录通常只对 mysql 用户开放,普通用户du统计时基本都会被跳过。/var/log:部分日志文件权限是600,普通用户只能看到目录项,却读不到内部循环日志的大小。/run、/var/tmp:某些服务会在这类目录下生成临时文件,权限收得很紧。
所以我的习惯是:凡是涉及整机磁盘空间排查,du 前面必定加 sudo。不加 sudo 的 du 统计结果,只能当作参考,不能作为决策依据。
1.2 sudo du 和普通 du 的实际输出差异
我找了一台比较典型的服务器做过一次对比。同一时刻,分别执行:
bash复制# 普通用户执行
du -sh /var/log
# sudo 执行
sudo du -sh /var/log
两种命令的实际输出差异完全取决于 /var/log 下文件的权限。如果普通用户能看到大部分日志文件,两者差异不大;但像 /var/log/secure、/var/log/btmp 这类 600 权限的文件,普通用户统计时根本读不到 size,du 会直接跳过。最终 sudo 版本统计出来的大小可能比普通版本多出几百 MB 甚至几个 GB。
另一个差异体现在遍历能力上。普通用户执行 du 时遇到无权限目录会立刻跳过并打印错误,这不仅仅影响统计结果,还会拖慢执行速度——因为 du 每遇到一个无权限目录都要打印一行错误信息到 stderr,上万行的输出本身就是一种性能损耗。我见过一个极端例子,某台机器上普通用户跑 du -sh /,因为 /proc 下大量内核线程目录权限特殊,刷出来的 Permission denied 直接有几十万行,终端都卡住了。
加上 sudo 之后,这些问题一次解决:所有目录可读,错误输出大幅减少,统计也快得多。
1.3 关于 sudo du 的一个高频误区
有人可能会说:既然要统计完整大小,那我直接 sudo su - 切到 root 再跑 du 不就行了?
道理是一样的,但有一个细节不同。sudo du 和 su - 后执行 du 最终得到的统计结果是一致的,因为它们都以 root 身份读取文件系统。区别在于 sudo 保留了审计日志,谁在什么时间执行过什么命令都记录在 /var/log/secure 或 /var/log/auth.log 里。多人在一台机器上协作时,用 sudo 而不是直接切 root 跑命令,其实是给自己留了一条追溯路径。
另外一个误区是"sudo 一定能读到所有目录"。实际上 sudo 的权限取决于 /etc/sudoers 的配置。如果某台机器的 sudoers 只允许特定用户以特定身份执行特定命令,那 sudo du 也可能被拒绝。在比较严格的生产环境里,运维人员经常会被限制只能以 sudo -u app 的形式执行运维命令,这时候 du 统计到的范围就不是全盘,而只是特定用户目录。这是需要注意的边界。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. du、df 的账为什么总对不上:先搞懂统计口径再动手
2.1 两者统计的底层对象完全不同
排查磁盘空间时,很多人会同时用到 df -h 和 du -sh,然后发现一个奇怪现象:df 显示根分区已用 80G,但 du -sh / 统计出来却只有 60G,少了整整 20G。这个差异要是搞不明白,排查方向就会跑偏。
先看 df 的统计口径。df 读的是文件系统的超级块(superblock)中的元数据,统计的是整个文件系统已经分配的块数。换句话说,df 关心的是"这个分区上有多少块已经被占用了",它不关心这些块对应哪些文件,也不关心文件是不是被删除了。
而 du 的统计口径是遍历目录树,逐个文件累加文件大小。它关心的是"当前目录树里能看到的文件加在一起有多大"。
这两个口径天然就对不上。文件系统本身有元数据开销,ext4/xfs 都会预留一部分块给文件系统自身使用,这部分空间 df 会计入已用空间,但 du 永远不会统计到。此外,稀疏文件、文件系统日志、保留块这些也都会造成差异。
所以一个基本结论是:df 和 du 的结果存在差异是正常的,两者相差几个百分点完全不用纠结。但如果差异特别大,比如 dh -h 显示已用 90G,du -sh / 却只有 20G,那就需要警惕了。
2.2 文件被删除但进程仍占用时的经典场景
造成 df 和 du 差异最大的一个经典场景,就是文件被删除但进程仍然持有它的文件描述符。
运维上有个很经典的坑:某个程序写日志写得太猛,把磁盘写满了。你在 /var/log/ 下找不到那个日志文件了,du 也统计不到它,因为文件已经被 rm 掉了。但 df 却显示磁盘依然是满的。
原因是这样的:当进程打开一个文件后,rm 删除的只是文件名对应的目录项,文件本身并没有被立刻销毁,因为进程还持有它的文件描述符。只要进程不关闭这个 fd,文件占用的磁盘块就始终被标记为已分配,df 自然会计入这些块,但 du 沿着目录树去遍历时,已经找不到这个文件的目录项了。
排查方法很简单:
bash复制lsof | grep '(deleted)'
或者更精确一点:
bash复制lsof +L1 | awk '$7 > 0 {print $1, $2, $7, $10}'
+L1 表示只列出链接数小于 1 的文件,也就是已经被删除但仍然打开的文件。找到 PID 和对应的 fd 后,可以通过重启进程释放空间,或者直接清空文件内容:
bash复制# 用 truncate 或直接重定向清空
truncate -s 0 /proc/PID/fd/FD
我在实际处理中遇到过一次比较典型的 case:Java 应用一直在写一个很大日志文件,运维直接 rm 掉了,结果日志进程还在持续写入,文件句柄未释放,df 一直是满的。排查了半小时,最后用 lsof +L1 一下就定位到了。之后团队统一改成 logrotate 按天切割日志,避免再出现"日志被 rm 但进程仍写"的尴尬。
2.3 元数据开销和文件系统保留块
除了已删除未释放文件,文件系统自身的元数据开销也会让 df 和 du 产生差异。ext4 在格式化时会预留 5% 的块给 root 用户,避免系统日志写满后连 root 都无法登录。这部分空间 df 会显示为已用,但 du 统计不到。
另外,ext4 的日志(journal)通常占用几十到几百 MB,inode 表本身也会占用固定空间。这些开销在 df 里会体现在已用空间上,但 du 完全不感知。
所以如果你发现 df 比 du 多出几个 GB,先别慌,这有可能是正常的元数据开销。但如果差异到了 20%、30% 甚至更多,那基本可以断定是已删除文件未释放、或者某个目录下存在隐藏的大文件。
我在排查时习惯采用"双命令交叉验证"的思路:先用 df -h 确定哪个挂载点快满了,然后用 du -xsh /挂载点 统计该挂载点下的目录树占用,两者核对差异。如果差异异常,立刻用 lsof +L1 查已删除文件。
3. du 常用参数的选择逻辑:别只会 -sh
3.1 从根目录开始的层层下钻命令组合
很多人用 du 就只会 du -sh,这个用法没错,但效率太低。它只能看到目录总大小,没法一眼锁定"到底哪个子目录占得最多"。
我的下钻思路是这样的:先用 df 锁定哪个挂载点告急,然后从该挂载点的根部开始,逐层找出占用最大的子目录,层层下钻,直到定位到具体的文件或目录。
首轮扫描,我会执行:
bash复制sudo du -x -h --max-depth=1 / 2>/dev/null | sort -hr | head -20
这条命令拆开看,每个参数都有存在的理由:
-x:跳过其他挂载点。不加这个参数,du会把所有挂载到此目录下的子分区也统计进去。比如你有/data单独挂载了一块数据盘,执行du -sh /时如果不加-x,/data下的所有数据也会被算进/的总大小里,这显然不是我们想要的。-h:人类可读的显示格式,K/M/G 自动转换。--max-depth=1:只显示一层子目录的大小。这个参数很关键,它不会递归显示所有子目录,否则输出会特别长,根本无法快速定位重点。2>/dev/null:屏蔽掉杂乱的错误输出,保持终端清爽。sort -hr:按人类可读的数值从大到小排序。head -20:只看最大的前 20 个目录。
我实际执行过一次后,输出长这样:
code复制12G /var
3.2G /usr
1.8G /opt
800M /home
...
看到 /var 占 12G,立刻再往下钻一层:
bash复制sudo du -x -h --max-depth=1 /var 2>/dev/null | sort -hr | head -10
输出:
code复制8.5G /var/log
2.1G /var/lib
1.2G /var/tmp
此时基本可以断定问题的根源在 /var/log。继续对 /var/log 做同样的操作,很快就定位到具体的日志包或日志文件。
这一层层下钻的过程,看起来像在做地毯式搜索,实际上因为每层只输出十几个目录,配合 sort 排序,整个排查过程一分钟以内就能完成。
3.2 --exclude、--max-depth 和 -S 的适用场景
--max-depth 刚才已经用到了,它控制的是 du 递归输出的深度。但有些场景下 --max-depth 还不够,需要配合别的参数组合使用。
比如我要在 /var 下找哪些目录超过 1G,但不想看几十层深的小目录,可以加 -t 参数做过滤:
bash复制sudo du -x -h --max-depth=3 -t 1G /var 2>/dev/null
-t 1G 表示只显示大小超过 1G 的目录。这个参数在日志目录特别乱、子目录特别多的时候非常好用,避免输出被一堆小目录刷屏。
另一个参数 --exclude 也值得单独拎出来说。它支持模式匹配,可以排除我们不关心的目录或文件类型:
bash复制# 排除 .log 文件的统计
sudo du -sh --exclude='*.log' /var/log
# 排除多个目录
sudo du -x -h --max-depth=1 --exclude={/var/cache,/var/tmp} /var 2>/dev/null
这个参数用在"统计当前真正占用空间的目录,但日志我们不关心"的场景。比如业务方问我"为什么 /data 涨得这么快",我执行 du 的时候通常会排除掉日志文件,聚焦在业务数据文件上。
还有个参数 -S 很容易被忽略。du -sh 统计的是目录本身加上它所有子孙目录的总和。但有些时候我想单独看目录自身的文件大小,不包括子目录,就用 -S:
bash复制sudo du -S -h /var 2>/dev/null | sort -hr | head
这个用法在"目录本身有很多文件、子目录也很多"的场景下非常实用。比如 /var/spool 下面可能同时存在大量待发送邮件和若干子目录,-S 可以把目录本身和子目录的占用分开看。
3.3 对 sort 排序的一个小提醒
du -h 输出的是一串带单位的大小值,直接用 sort -h 按人类可读的大小排序是没问题的。但注意,sort -h 是 GNU coreutils 提供的扩展功能,部分精简版系统(如 BusyBox、某些容器镜像)可能不支持 -h 选项。如果遇到 sort: invalid option -- 'h' 的报错,可以改用纯数字排序:
bash复制sudo du -x --max-depth=1 /var 2>/dev/null | sort -nr | head
这种方式不指定 -h,输出的是以 KB 为单位的数字,然后再手动换算成 G。虽然不如 -h 直观,但兼容性更好。
4. 一次真实磁盘告警的完整排查过程
4.1 从监控告警到定位到具体大文件
说了这么多理论,下面拿一次真实的磁盘告警处理过程来完整走一遍。
监控系统发来告警:/ 分区使用率超过 90%。这是最常见的磁盘问题,我习惯按下面的顺序执行命令:
bash复制# 1. 确认挂载点状态
df -h /
# 输出
Filesystem Size Used Avail Use% Mounted on
/dev/vda1 50G 45G 2.1G 96% /
确认根分区确实紧张,接下来启动 sudo du 排查链路。
bash复制# 2. 从根目录开始,统计一级目录占用
sudo du -x -h --max-depth=1 / 2>/dev/null | sort -hr | head -10
输出结果:
code复制28G /var
12G /usr
3.5G /opt
1.8G /home
...
/var 占了 28G,这是重点。继续下钻:
bash复制sudo du -x -h --max-depth=1 /var 2>/dev/null | sort -hr | head -10
输出结果:
code复制16G /var/log
6.5G /var/lib
3.2G /var/cache
...
/var/log 16G,这里有蹊跷。再看一层:
bash复制sudo du -x -h --max-depth=1 /var/log 2>/dev/null | sort -hr | head -10
输出结果:
code复制11G /var/log/journal
3.1G /var/log/nginx
1.5G /var/log/messages
...
这里暴露了两个问题:journald 日志积累严重,nginx 访问日志也没有按天切割。接下来就需要处理了。
journald 日志的清理可以用这条命令:
bash复制# 只保留最近 7 天的日志
sudo journalctl --vacuum-time=7d
如果日志量大到 vacuum 都卡,可以临时调低 SystemMaxUse:
bash复制# 编辑 /etc/systemd/journald.conf
# 设置 SystemMaxUse=500M
sudo systemctl restart systemd-journald
nginx 日志的处理,要么用 logrotate 配置按天切割,要么直接清掉半个月前的历史日志。这里我选择先用 logrotate 做按天切割的配置,然后把旧日志清理掉。
4.2 大文件定位与清理前的确认习惯
du 逐层下钻定位到目录级别之后,接下来要找具体的大文件。可以用 find 按大小直接筛:
bash复制sudo find / -xdev -type f -size +1G -exec ls -lh {} \; 2>/dev/null
这条命令会找出根分区下所有超过 1G 的文件,并列出它们的大小和路径。-xdev 和 du 的 -x 作用一致,都是为了不跨越挂载点。
找到大文件之后,不要急着 rm。我在生产环境处理过太多因为误删文件导致的线上事故,所以现在养成了两个习惯:
第一,删之前先确认这个文件的用途。怎么确认?看路径、看时间戳、看属主。比如 /var/log/nginx/access.log.20240615.gz,路径和时间戳基本能说明它只是一个历史日志;但如果是 /opt/app/data/db.sqlite3,就算它再大,也不能随便删。
第二,优先用 truncate 清空而不是 rm 删除。对于不需要保留历史数据、但进程可能仍在写入的文件(比如运行中的服务日志),直接 truncate -s 0 比 rm 安全得多:
bash复制# 清空文件内容,但不删除文件本身
sudo truncate -s 0 /var/log/nginx/access.log
清空之后,df -h / 再看一眼,使用率会立刻降下来。这个操作对运行中的服务几乎无影响,非常实用。
4.3 清理后的验证和复盘
空间释放之后,我会再做一次全量确认:
bash复制df -h /
sudo du -x -h --max-depth=1 / 2>/dev/null | sort -hr | head -5
这次输出里 /var 从 28G 降到了 8G,根分区使用率从 96% 降到了 68%。到这里告警才算真正解除。
但这只是应急处理。磁盘告警的本质是缺少日志轮转和清理策略。所以我会在后续安排 logrotate 配置、journald 日志大小上限、监控项补充(比如对 /var/log 目录设置大小告警)这几件事。
5. 三个容易忽视的"磁盘空间隐形杀手"
5.1 已删除文件未释放空间(deleted file)
刚才在讲 df 和 du 差异时详细说过了这个场景,这里再补充一个实际判断技巧。
当你发现 df 使用率很高,但 du 统计目录树找不到对应大小的文件时,第一反应应该是查已删除但未释放的文件:
bash复制sudo lsof +L1 | head -30
如果列出的文件数量不多,可以直接看大小列,找到占空间最大的那个。接下来有两种处理方式:
- 如果能确认对应进程可以重启,直接重启进程即可释放空间。
- 如果进程不能重启(比如核心数据库实例),可以通过清空 fd 内容来释放空间:
bash复制sudo truncate -s 0 /proc/PID/fd/FD
注意:/proc/PID/fd/FD 是一个符号链接,执行 truncate 会直接清空对应文件的内容。这个操作比杀掉进程更温和,但同样要确保清空的是日志类文件,而不是数据库文件。
5.2 inode 耗尽:df -i 显示的另一种"满"
排查磁盘空间的时候,有一个隐性指标容易忽略:inode。df -h 显示使用率只有 40%,但应用却报错"No space left on device",这种情况一般是 inode 耗尽了。
你执行 df -i 看一下:
bash复制df -i /
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/vda1 3276800 3276800 0 100% /
Inode 用完了,说明文件系统里的小文件数量多到爆,每个文件都要占用一个 inode。这种情况 du 统计出来总大小可能并不大,但文件数量海量,导致元数据空间被耗尽。
排查哪些目录文件数量多,可以用这种笨办法:
bash复制sudo find / -xdev -type f | cut -d/ -f2 | sort | uniq -c | sort -nr
或者逐层统计:
bash复制sudo for i in / /var /var/log; do echo "$i: $(sudo find $i -xdev -type f | wc -l)"; done
常见的 inode 耗尽场景有:邮件队列中积累了大量小邮件文件、临时目录被程序疯狂创建文件、Docker 容器遗留下大量 overlay 层文件。处理方式和磁盘空间满不同,要针对性地清理小文件,比如用 find /var/spool -type f -delete。
5.3 Docker overlay2 目录的膨胀问题
现在很多服务器上跑着 Docker,/var/lib/docker/overlay2 这个目录经常成为磁盘空间大户。
当你执行 sudo du -x -h --max-depth=1 /var/lib/docker 时,会看到类似这样的输出:
code复制20G /var/lib/docker/overlay2
8G /var/lib/docker/containers
5G /var/lib/docker/volumes
overlay2 目录是镜像和容器层的存储位置,它膨胀的原因通常是:重复构建镜像导致大量中间层没有被清理、容器日志文件没有轮转、镜像仓库推送失败留下了悬空镜像。
处理思路:
bash复制# 查看悬空镜像
docker images -f dangling=true
# 清理悬空镜像
docker image prune
# 清理所有未使用的镜像、网络、构建缓存
docker system prune -a --volumes
但注意,docker system prune 会把没有容器使用的镜像全部删掉,执行前一定要确认当前环境里没有需要保留的历史镜像。
容器日志的清理也不能忽略,配置好 logrotate 或者使用 --log-opt max-size 限制容器日志大小,可以有效避免容器运行时日志无限增长。
6. 我对 sudo du 的一些日常用法和小建议
6.1 把它养成条件反射:排查脚本化
排查磁盘空间其实是一件重复性极高的事情。我后来干脆把这一套操作封装成一个简单的 shell 函数,放在 ~/.bashrc 里:
bash复制du_top() {
local dir=${1:-/}
sudo du -x -h --max-depth=1 "$dir" 2>/dev/null | sort -hr | head -20
}
这样日常定位只需要:
bash复制du_top /
du_top /var
du_top /var/log
命令短、好记、不用每次敲一长串参数。alias 也可以,但我更推荐函数,因为函数可以接受参数。
如果还想更进一步,可以配合 crontab 做一个定时巡检脚本。每天凌晨 2 点把重要的目录大小写到一个日志文件里,连续记录几天,就能清楚地看到哪个目录在持续增长。这个数据比监控告警的阈值更能反映问题的趋势。
6.2 关于性能:du 也不是万能的
du 虽然好用,但它的本质是遍历目录树,在文件数量特别多的目录上执行,会非常慢。以下场景我建议谨慎使用:
- 根目录直接
du -xsh /:需要遍历整个文件系统,文件量大时可能要几分钟到几十分钟。 - NFS 挂载的远程目录:
du会走网络遍历远程目录树,耗时取决于网络带宽和远程目录复杂度。 - 海量小文件目录:比如
/var/lib/containerd里大量镜像层文件,du会特别耗时。
如果只是想在短时间内看到某个挂载点的占用趋势,我建议用 df 做监控,用 du 做精确排查,两者结合,各司其职。
6.3 最后一个小技巧:把 du 的输出格式改成纯数字
-h 参数虽然可读性好,但不利于程序处理。某些自动化脚本需要拿到精确的数值,这时候用 -k 强制输出 KB 数值:
bash复制sudo du -xsk /var/log | awk '{printf "%.2f GB\n", $1/1024/1024}'
这种输出丢给监控脚本处理非常方便,不会因为单位换算产生歧义。另外,如果你用的是较新的 coreutils 版本,还可以用 --block-size=1M 控制输出单位为 MB,在可读性和精确度之间取一个平衡。
磁盘空间排查这件事,方法说穿了就这些:先 df 看趋势,再 sudo du 找大户,必要时用 lsof 和 find 补刀。真正拉开运维效率差距的,是对命令参数的理解深度,以及排查链路是否形成肌肉记忆。sudo du 这个组合,我用到现在没觉得有什么替代品能比它更直接、更高效,至少在纯粹的命令行环境下,它依然是我排查磁盘问题时的第一选择。
