☰
磁盘容量排序实战:从du命令到Top-N堆排序的运维指南
2026/10/5 3:01:00 网站建设 项目流程

1. “磁盘满了”背后:容量排序到底要解决什么问题

干运维和开发这些年,我几乎每个月都会遇到一次“磁盘满了”的告警。刚开始那会儿,一看到告警就慌,登录服务器先df -h看一眼,哪个分区满了就手动去翻目录,一层一层du下去,翻得头大。后来才慢慢琢磨明白:磁盘满了本身不是最要命的事,最要命的是你不知道空间到底被谁吃掉了。这个时候,“磁盘容量排序”就成了解救我的第一把钥匙。

所谓磁盘容量排序,说白了就是把某个目录下所有子目录和文件按占用空间从大到小排一遍,让你一眼看到那个“罪魁祸首”。这个需求听起来简单,但实际做起来,里面坑多得让我栽了好几次跟头。排序本身是计算机里最经典的问题,但当你把“排序”和“磁盘容量”放在一起,就多了很多现实约束:数据量大、收集数据慢、单位不统一、权限有盲区、还有跨文件系统的问题。这篇文章我就把这几年折腾磁盘容量排序的经验完整梳理一遍,从最基础的命令组合,到处理大数据量时的算法思路,再到长期监控的自动化方案,整个过程都是在生产环境里验证过的。

适合谁来读呢?如果你是刚入行的运维、后端工程师,或者只是自己电脑磁盘总是莫名其妙被塞满的普通用户,这里面的方法都能直接用。我不会堆砌一堆理论,讲的全是我自己敲过的命令、写过的脚本、踩过的坑。

2. 命令行三步走:du 收集、sort 排序、head 取 Top

2.1 最常用的命令组合,大部分时候够用

先说说我最常用的一组命令。假设我现在要查/var目录下谁占空间最大,我会执行:

du -h --max-depth=1 /var 2>/dev/null | sort -rh | head -20

这串命令拆开看,每一段都有讲究:

  • du -h:统计磁盘使用空间,-h参数表示用人类可读格式输出,也就是自动换算成 K、M、G 等单位。如果不加-h,输出的是裸的 KB 数字,虽然也能排,但你看结果的时候还得自己换算,不够直观。
  • --max-depth=1:只统计到一级子目录的深度。这是最常用的深度,因为大多数场景下,我们只需要先看目录下每个一级子目录占了多少空间,定位到大头之后再继续往下一层钻。如果不用这个参数,du会递归统计整个目录树的所有子目录,输出几百上千行,结果里全是嵌套的子目录,反而看不清主次。
  • 2>/dev/null:把错误输出丢掉。权限不足、目录不存在这类报错信息,在统计的时候刷屏很烦,直接丢弃能让输出清爽不少。但这里有个隐蔽的问题我后面会专门讲,你现在先记住这个用法。
  • sort -rh:-r是倒序,-h是人类可读数字排序。这个-h参数是整个命令里的灵魂所在,没有它,排序结果会乱得一塌糊涂。为什么这么说?我下面单独讲。
  • head -20:只取排序后的前 20 行。磁盘上目录数量经常成千上万,你没必要全部看完,只看最大的一批就够了。

跑完之后你会看到类似这样的输出:

4.2G /var/log 1.8G /var/lib 870M /var/cache

操作上没什么难度,但用head -20取 Top 是有讲究的。我见过不少同事先sort -rh然后把整屏输出全拉出来,再手动找最大的。机器帮你干活已经干到排序这一步了,后面筛选最危险的几个头部项,交给head就行,别浪费眼睛。

2.2 为什么不能直接用默认的 sort,必须用 -h

这里得解释一个非常多朋友踩过的坑:du输出的容量同时有 K、M、G 三种单位,如果直接用sort -r按字典序排列,结果会出现“10M 排在 9G 前面”这种离谱情况。

看个实际例子。假设原始数据是这样的:

9.1G /var/lib 10.2M /var/tmp 890M /var/cache

如果执行sort -r(字典序倒序),你会得到:

9.1G /var/lib 890M /var/cache 10.2M /var/tmp

因为字典序比较字符串的时候,是先从头比较字符的。“9”大于“8”,所以9.1G排在890M前面;“8”大于“1”,所以890M又排在10.2M前面。这种排法完全违背直觉。

sort -h就是专门用来解决这个问题的。它能够识别 K、M、G、T 这些人类可读单位,先把数值部分和单位部分分别解析出来,再按数值大小做比较。加了-h之后,上面三个目录的正确顺序是:

9.1G /var/lib 890M /var/cache 10.2M /var/tmp

顺带提一句,如果你用的系统比较老,sort版本可能不支持-h(GNU coreutils 在 7.5 版本以后才加入这个参数,差不多是 2009 年),那就只能先du -k拿到纯 KB 数字排序,最后再手动换算。但现在主流服务器基本都支持,只要你不是在特别老的 AIX、Solaris 上跑,直接用-h就行。

2.3 很容易被忽略的 locale 排序问题

另一个让我栽过跟头的坑,是 sort 在特定 locale 下的排序行为。有一次我在一台新服务器上执行这条命令,结果输出顺序乱得莫名其妙——有些目录名带着特殊符号_、-、@,排序出来跟我想的完全不一样。

原因在于sort默认会使用当前环境变量LANG指定的 locale 规则来排序。在en_US.UTF-8这类 UTF-8 locale 下,字符串排序不仅仅按 ASCII 码,还会把大小写、标点符号都考虑进去,排序的规则和纯字节序是有差异的。

解决办法也很简单,在命令前加一个环境变量设置:

LC_ALL=C du -h --max-depth=1 /var 2>/dev/null | sort -rh | head -20

用LC_ALL=C强制按纯字节序排列,虽然某些多字节语言的目录名排序看起来没那么“智能”,但容量排序场景里我们关心的是数字大小,不是目录名的字典序,所以用 C locale 更稳。

实际上我后面写脚本的时候,都会统一在脚本开头加一句export LC_ALL=C,省得在不同机器上因为 locale 配置不同导致结果不一致。

3. 目录深度、权限盲区和跨文件系统的“测量误差”

3.1 max-depth 怎么选:先粗筛后细查,别指望一步到位

我之前提到--max-depth=1是最常用的深度,但实际使用中还有一个选择:你要根据服务器类型和目录内容来决定初始深度。

比如排查一个 Web 服务器的空间占用,我通常的做法是:

第一轮:du -h --max-depth=1 / 找一级目录的大头 第二轮:对着大头目录 du -h --max-depth=1 /var 第三轮:再往下一层 du -h --max-depth=1 /var/log

这种一层层“下钻”的方式,虽然看起来土,但却是最可靠、最不容易遗漏的排查路径。因为目录树的结构千变万化,有些人喜欢把所有文件堆在一个深层目录里,如果你一上来就用--max-depth=3或更深的深度,输出会拉出长长的一串,中间还夹杂大量浅层目录的重复统计,肉眼很难抓住重点。

你可能会问:有没有可能用一条命令直接找出全盘最大的 10 个文件?答案是很难,因为du的输出是目录维度,不是文件维度。要找最大的文件,得用find / -type f -printf '%s %p\n'来收集文件大小信息,然后排序。但这种方式在大目录下会非常慢,而且很多文件你根本无权访问。所以我的原则是:目录用du下钻,文件用find配合排序,两者分开做。

3.2 权限不足时,被 2>/dev/null 吞掉的真相

前面我提到2>/dev/null会把错误信息丢掉,但这里有一个非常重要的细节:如果你丢掉了错误,你可能根本不知道自己漏统计了多少空间。

有一次我排查一个应用服务器的磁盘占用,用du统计/home目录,结果显示某个用户目录只占了几百 MB。但后来用sudo一查,发现实际占了 6 个 G。原因就是那个用户目录下的部分子目录权限设置成了 700,当前用户(root 之外的管理账号)根本进不去,du会打印类似du: cannot read directory '/home/user/secret': Permission denied的错误信息。我当时加了2>/dev/null,这些信息全被吞了,导致统计结果严重偏低。

正确做法有好几种,我推荐的做法是:不要盲目丢弃错误,而是把错误重定向到日志文件里,统计完再检查一遍:

du -h --max-depth=1 --exclude=/proc /home 2> du_error.log | sort -rh | head -20 cat du_error.log

如果错误日志里出现了大量cannot read,你就得考虑两个方向:要么用 root 权限重跑,要么接受统计结果有偏差,在报告里标注“部分目录无权限未计入”。这比被2>/dev/null蒙在鼓里强得多。

3.3 挂载点、硬链接和跨文件系统的“重复统计”

还有一个让容量排序结果出现幻觉的问题:跨文件系统。du默认会进入挂载点下的子目录继续统计,比如/mnt/backup挂了一个独立硬盘,你统计/根目录的时候,会把整个备份盘的空间也“算进”/mnt/backup这个目录里。这本身不算错,但如果你的目的是看“当前文件系统里谁占空间”,把另一块盘的容量混进来就会误导决策。

解决办法是给du加-x参数:

du -h -x --max-depth=1 / 2> du_error.log | sort -rh | head -20

-x的含义是“不要跨文件系统”,只在当前文件系统内进行统计。对根目录排查的时候这个参数几乎必备,否则你看到的结果往往是挂在/mnt或者/data下面的外部存储盘占了大头,而根分区本身的问题被你忽略了。

硬链接也是个隐蔽的坑。du默认情况下,同一个文件有多个硬链接时只计算一次,这符合大多数人的期望。但有些场景下,比如备份软件创建了大量硬链接,你希望每个目录里的链接都算作占用空间的话,就得加-l参数。不过这个需求比较小众,我一般不加,只在意识到某个备份目录特别异常时才手动试一下。

4. 当数据量上万级:从快排到 Top-N,算法选择开始影响效率

4.1 sort 命令内部用的是归并排序,但你其实不太需要关心它

聊到排序,不可避免要绕到算法上。热搜词里有“快速排序”、“归并排序”、“选择排序”这些,我在写自己的容量统计工具时也专门认真想过这个问题。GNUsort命令内部用的是平衡归并排序(外部排序的一种变体),特别适合处理“数据量超过内存容量”的场景——它会把待排序数据分成多个块,每个块排序后写到临时文件,再不断归并这些块,最终得到有序输出。

这个算法层面的事实对普通使用者的意义在于:sort处理几万行甚至几十万行文本是绰绰有余的,你不用自己写一个快速排序来对磁盘容量排序。容量数据的数量级一般也就是几千到几万条目录条目,用sort已经够快了。真正慢的,永远在du收集阶段——遍历目录树要触及每个 inode,瓶颈在磁盘 IO 上,而不在排序本身。

但如果你想做一个更“聪明”的容量分析工具,自己写程序处理数据是绕不开的。这时候“排序算法的选择”就成了一个真实问题。

4.2 Top-N 问题:用堆排序替代全量排序

大多数磁盘容量分析场景里,我们并不需要完整的排序结果,只需要找出“最大的 50 个”或者“最大的 100 个”。这就是经典的 Top-N 问题。

假如你用 Python 写一个容量分析脚本,原始数据是从目录遍历拿到的全量元组列表,第一直觉是:

data = [] # 假设已经通过 os.walk 收集了所有目录的大小 data.sort(key=lambda x: x[1], reverse=True) top_50 = data[:50]

这段代码是“全量排序后取前 50”,数据量在几万条的时候完全没问题,Python 内置排序的性能足够。但如果你的遍历目标是海量文件,比如一个存了数百万文件的存储服务器,把所有条目都 gather 到一个 list 里再排序,内存开销和耗时都会变得不可忽略。

更稳妥的做法是维护一个大小为 50 的最小堆:

import heapq heap = [] limit = 50 # 假设 size 是目录大小,path 是目录路径 for path, size in walked_data(): if len(heap) < limit: heapq.heappush(heap, (size, path)) elif size > heap[0][0]: heapq.heapreplace(heap, (size, path)) top_50 = sorted(heap, reverse=True)

这段代码的逻辑是:堆顶始终是当前最大的 50 个元素中最小的那一个;新元素只要比堆顶大,就替换掉堆顶并重新调整堆。整个过程只需要 O(n log 50) 的时间,内存固定只占 50 个元素,不需要把全量数据装进内存。而全量排序的时间复杂度是 O(n log n),堆排序在这里有明显的优势。

我实际写过一个磁盘容量报告脚本,遍历一个 200 万文件的目录结构,用全量排序内存峰值接近 2GB,改成 Top-50 堆之后,内存降到几百 MB 以内,耗时也没明显增加。机器上资源不是无限的话,这种优化很有价值。

4.3 现实中的瓶颈在磁盘 IO:排序优化救不了 du 的慢

话说回来,算法优化帮的是“数据处理”环节,但磁盘容量排序的真实瓶颈往往在数据采集。du为什么慢?因为它要遍历目录树,对每个文件和目录执行 stat 系统调用,深入到硬盘的每个角落去读取元数据。硬盘 IOPS 有限,文件越多,耗时越长。

我遇到过一台存储服务器,某个目录下有超过 1000 万个文件,光是对这个目录执行du -sh就花了快 20 分钟。这种场景下,排序本身再快也无济于事。通用的应对思路有三个:

  • 第一次全量统计之后,把结果存下来,之后只做增量更新,假设没有变化的目录大小沿用上次的统计值。
  • 白天业务高峰期不做全量统计,放到凌晨低峰期跑定时任务。
  • 用ionice降低统计进程的 IO 优先级,避免du拖垮正常业务:
ionice -c 3 du -h --max-depth=1 /data 2>/dev/null | sort -rh | head -50

ionice -c 3表示让这个进程以“空闲”级别的 IO 调度运行,只有当系统没有其他 IO 请求时才执行,这样统计大目录的同时,不会明显影响线上服务的读写性能。

5. 从一次性命令到长期监控:我构建目录容量排序报告的经验

5.1 手动排查不能满足日常需求,定时采集才靠谱

如果你管理的服务器数量不多,偶尔手动跑一下du命令就够了。但生产环境里,磁盘容量是持续变化的——日志天天写、缓存不断构建、备份脚本按周期跑。等告警出来再去查,往往已经晚了。

我的做法是写一个定时采集脚本,每天凌晨跑一次,把关键目录的容量快照存起来。脚本本身不复杂,核心逻辑就是把刚才的命令组合用 shell 或 Python 包装一下:

#!/bin/bash export LC_ALL=C du -h -x --max-depth=1 / 2>/tmp/du_error.log | sort -rh > /var/log/disk_report/$(date +%F)_root.txt

但如果只是存文本文件,日积月累之后很难对比趋势。我更推荐用 SQLite 存历史数据,这样要查“过去一个月哪些目录涨得最快”就非常方便。

5.2 用 SQLite 存历史快照,查询“涨得最快的目录”成为可能

我设计的表结构很简单:

CREATE TABLE disk_usage ( id INTEGER PRIMARY KEY, scanned_date DATE NOT NULL, directory TEXT NOT NULL, size_kb INTEGER NOT NULL, UNIQUE(scanned_date, directory) );

采集脚本每次扫描得到某个目录的大小,先换算成 KB 整数,再 upsert 进这张表。注意存纯数值而不是人类可读字符串,这样后续 SQL 排序和计算都不用处理“1.2G”这种格式,直接按整数比较。

有了历史数据,我可以直接用一个查询找出最近两次采集之间增长最快的目录:

SELECT t1.directory, t2.size_kb - t1.size_kb AS growth_kb FROM disk_usage t1 JOIN disk_usage t2 ON t1.directory = t2.directory AND t2.scanned_date = ( SELECT MAX(scanned_date) FROM disk_usage ) WHERE t1.scanned_date = ( SELECT MAX(scanned_date) FROM disk_usage WHERE scanned_date < (SELECT MAX(scanned_date) FROM disk_usage) ) ORDER BY growth_kb DESC LIMIT 20;

这个查询的思路是:把最近一次采集日的记录和上一次采集日的记录按目录关联起来,差值就是这段时间内增长的空间,按差值倒序排列就能定位“最近涨得最凶”的目录。这个信息比单纯看当前容量更有价值,因为它直接指向正在出问题的地方——比如日志清理策略失效、临时文件堆积、数据库备份过度膨胀。

我跑这个查询的时候,遇到过一个情况:有一个目录当天容量只排在第十位,但过去一周涨了 400 多 G,明显是某个服务在疯狂写临时文件。如果只做一次性的“当前容量排序”,根本注意不到它;看了趋势排序之后,问题当天就定位到了。

5.3 交互式工具让排序结果更好用:ncdu、dua 和 dust

如果你是个人电脑或少数几台服务器上排查,命令行加head就够用。但如果你喜欢更直观的交互界面,我推荐几个工具,它们本质上也是在“排序”的基础上做了可视化增强:

  • ncdu:最经典的交互式磁盘统计工具。它启动后会像面板一样列出目录树,默认按占用空间从大到小排列,你可以直接用方向键在目录间跳转,按 d 删除文件,按 n 切换排序方式。对于排查一个大目录,比反复敲du命令高效得多。
  • dua:用 Rust 写的,速度比 ncdu 快,支持并行统计,界面风格接近 ncdu,但更现代。
  • dust:类似du的增强版,输出自带条形图可视化,一眼就能看出哪个目录最大,适合快速预览。

这些工具底层做的事情,仍然脱不开“收集目录大小 + 排序展示”这两个步骤,只是把交互体验做好了。我的建议是:日常排查用ncdu,写报告和自动化监控用自己的脚本,两者不冲突。

5.4 给磁盘告警换上“趋势排序”的大脑

到了自动化这一步,很多人会问:既然容量采集和排序脚本都有了,能不能直接接监控告警?完全可以。你可以在告警规则里不只是判断“磁盘使用率超过 90%”,而是结合历史快照判断“哪些目录过去 24 小时增长超过 10G”,然后把这个信息一并带在告警通知里。

我实际搭过一套轻量方案,逻辑不复杂:

  1. 每天凌晨定时跑采集脚本,数据入 SQLite。
  2. 每天生成一个 Top 涨跌报告文本文件。
  3. 写一个检查脚本,把最近一次采集的容量和 SQLite 里上一次的差值算出来,超过阈值的目录直接触发生成钉钉或邮件告警。

这套方案跑了大半年,效果非常稳定。真正把告警从“告诉我磁盘满了”升级成了“告诉你哪里长大的、长了多少”,排障效率完全不是一个级别。

6. 我回头再看磁盘容量排序这件事

做磁盘容量排序这几年,我最大的体会是:排序算法本身不是核心,核心是你对数据的理解程度。同样是“从大到小排”,直接执行du | sort -rh | head是最快解决当下问题的方法;加上-x和权限日志是避免被误导的防御习惯;引入 Top-N 堆和 SQLite 历史趋势则是从“应急响应”走向了“长效治理”。

如果你现在面临的问题只是“磁盘满了,赶紧找到大头”,那记住这一条命令就够了:

du -h -x --max-depth=1 / 2>/tmp/du_error.log | sort -rh | head -20

然后顺着最大的目录一层层往下钻,定位到具体文件,该清理清理,该迁移迁移。但如果你希望以后不再被同样的告警反复折腾,花一个下午把定时采集和历史趋势查询搭起来,性价比极高。

我相信随着机器上数据越来越多,磁盘容量管理还会变得更复杂。但“先把占用空间最大的东西揪出来”这个朴素的诉求,永远不会过时——因为它解决的是最基础也是最紧迫的问题:你的数据到底放在哪里,谁在悄悄吃掉你的容量。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询