ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

磁盘容量排序实战:从du命令到Top-N堆排序的运维指南

磁盘容量排序实战:从du命令到Top-N堆排序的运维指南 1. “磁盘满了”背后容量排序到底要解决什么问题干运维和开发这些年我几乎每个月都会遇到一次“磁盘满了”的告警。刚开始那会儿一看到告警就慌登录服务器先df -h看一眼哪个分区满了就手动去翻目录一层一层du下去翻得头大。后来才慢慢琢磨明白磁盘满了本身不是最要命的事最要命的是你不知道空间到底被谁吃掉了。这个时候“磁盘容量排序”就成了解救我的第一把钥匙。所谓磁盘容量排序说白了就是把某个目录下所有子目录和文件按占用空间从大到小排一遍让你一眼看到那个“罪魁祸首”。这个需求听起来简单但实际做起来里面坑多得让我栽了好几次跟头。排序本身是计算机里最经典的问题但当你把“排序”和“磁盘容量”放在一起就多了很多现实约束数据量大、收集数据慢、单位不统一、权限有盲区、还有跨文件系统的问题。这篇文章我就把这几年折腾磁盘容量排序的经验完整梳理一遍从最基础的命令组合到处理大数据量时的算法思路再到长期监控的自动化方案整个过程都是在生产环境里验证过的。适合谁来读呢如果你是刚入行的运维、后端工程师或者只是自己电脑磁盘总是莫名其妙被塞满的普通用户这里面的方法都能直接用。我不会堆砌一堆理论讲的全是我自己敲过的命令、写过的脚本、踩过的坑。2. 命令行三步走du 收集、sort 排序、head 取 Top2.1 最常用的命令组合大部分时候够用先说说我最常用的一组命令。假设我现在要查/var目录下谁占空间最大我会执行du -h --max-depth1 /var 2/dev/null | sort -rh | head -20这串命令拆开看每一段都有讲究du -h统计磁盘使用空间-h参数表示用人类可读格式输出也就是自动换算成 K、M、G 等单位。如果不加-h输出的是裸的 KB 数字虽然也能排但你看结果的时候还得自己换算不够直观。--max-depth1只统计到一级子目录的深度。这是最常用的深度因为大多数场景下我们只需要先看目录下每个一级子目录占了多少空间定位到大头之后再继续往下一层钻。如果不用这个参数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版本可能不支持-hGNU 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_ALLC du -h --max-depth1 /var 2/dev/null | sort -rh | head -20用LC_ALLC强制按纯字节序排列虽然某些多字节语言的目录名排序看起来没那么“智能”但容量排序场景里我们关心的是数字大小不是目录名的字典序所以用 C locale 更稳。实际上我后面写脚本的时候都会统一在脚本开头加一句export LC_ALLC省得在不同机器上因为 locale 配置不同导致结果不一致。3. 目录深度、权限盲区和跨文件系统的“测量误差”3.1 max-depth 怎么选先粗筛后细查别指望一步到位我之前提到--max-depth1是最常用的深度但实际使用中还有一个选择你要根据服务器类型和目录内容来决定初始深度。比如排查一个 Web 服务器的空间占用我通常的做法是第一轮du -h --max-depth1 / 找一级目录的大头 第二轮对着大头目录 du -h --max-depth1 /var 第三轮再往下一层 du -h --max-depth1 /var/log这种一层层“下钻”的方式虽然看起来土但却是最可靠、最不容易遗漏的排查路径。因为目录树的结构千变万化有些人喜欢把所有文件堆在一个深层目录里如果你一上来就用--max-depth3或更深的深度输出会拉出长长的一串中间还夹杂大量浅层目录的重复统计肉眼很难抓住重点。你可能会问有没有可能用一条命令直接找出全盘最大的 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-depth1 --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-depth1 / 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(keylambda x: x[1], reverseTrue) 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, reverseTrue)这段代码的逻辑是堆顶始终是当前最大的 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-depth1 /data 2/dev/null | sort -rh | head -50ionice -c 3表示让这个进程以“空闲”级别的 IO 调度运行只有当系统没有其他 IO 请求时才执行这样统计大目录的同时不会明显影响线上服务的读写性能。5. 从一次性命令到长期监控我构建目录容量排序报告的经验5.1 手动排查不能满足日常需求定时采集才靠谱如果你管理的服务器数量不多偶尔手动跑一下du命令就够了。但生产环境里磁盘容量是持续变化的——日志天天写、缓存不断构建、备份脚本按周期跑。等告警出来再去查往往已经晚了。我的做法是写一个定时采集脚本每天凌晨跑一次把关键目录的容量快照存起来。脚本本身不复杂核心逻辑就是把刚才的命令组合用 shell 或 Python 包装一下#!/bin/bash export LC_ALLC du -h -x --max-depth1 / 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”然后把这个信息一并带在告警通知里。我实际搭过一套轻量方案逻辑不复杂每天凌晨定时跑采集脚本数据入 SQLite。每天生成一个 Top 涨跌报告文本文件。写一个检查脚本把最近一次采集的容量和 SQLite 里上一次的差值算出来超过阈值的目录直接触发生成钉钉或邮件告警。这套方案跑了大半年效果非常稳定。真正把告警从“告诉我磁盘满了”升级成了“告诉你哪里长大的、长了多少”排障效率完全不是一个级别。6. 我回头再看磁盘容量排序这件事做磁盘容量排序这几年我最大的体会是排序算法本身不是核心核心是你对数据的理解程度。同样是“从大到小排”直接执行du | sort -rh | head是最快解决当下问题的方法加上-x和权限日志是避免被误导的防御习惯引入 Top-N 堆和 SQLite 历史趋势则是从“应急响应”走向了“长效治理”。如果你现在面临的问题只是“磁盘满了赶紧找到大头”那记住这一条命令就够了du -h -x --max-depth1 / 2/tmp/du_error.log | sort -rh | head -20然后顺着最大的目录一层层往下钻定位到具体文件该清理清理该迁移迁移。但如果你希望以后不再被同样的告警反复折腾花一个下午把定时采集和历史趋势查询搭起来性价比极高。我相信随着机器上数据越来越多磁盘容量管理还会变得更复杂。但“先把占用空间最大的东西揪出来”这个朴素的诉求永远不会过时——因为它解决的是最基础也是最紧迫的问题你的数据到底放在哪里谁在悄悄吃掉你的容量。
返回列表