Linux磁盘占用排查:从df/du到lsof定位空间神秘消失
我印象里大部分 Linux 服务器报警不是 CPU 飙高就是磁盘满了。CPU 飙高好歹还能通过 top 快速找到元凶磁盘满了反而麻烦你明明删了好几个大文件df 一看还是 100%折腾半天才反应过来是文件被进程占用没释放。这类问题在运维和开发里太常见了所以我一直觉得每个接触 Linux 的人都该有一套自己的磁盘占用分析流程而不是每次都在那瞎敲 du -sh 碰运气。这篇东西就把我平时排查磁盘占用的完整思路、常用工具和踩过的坑整理出来从最基本的 df、du 原理讲到 lsof、ncdu 这类进阶工具再配上真实场景的排查过程希望能帮你少走点弯路。不管你是刚入门 Linux 的开发者还是天天跟服务器打交道的运维这都值得花几分钟看看。1. 先搞清楚 df 和 du 为什么对不上磁盘占用分析最让人头大的问题就是明明 du 统计出来只有 50Gdf 却显示已用 80G这 30G 凭空消失了一样。很多人卡在这一步就开始慌了其实这是正常现象关键是理解 df 和 du 完全是两种统计思路。1.1 文件系统层与目录层的统计差异df 是站在文件系统角度看的它统计的是整个分区的块使用情况包括元数据、预留块、已经被删除但仍被进程占用的文件以及那些你没有权限读取的目录内容。而 du 是站在目录树角度看的它逐个遍历目录和文件把它们占用的块数加在一起所以它看不到进程持有的已删除文件句柄也会因为权限问题漏掉一些目录。打个比方df 像是一个仓库管理员看整个仓库还剩多少货架空间而 du 像是你拿着清单一件一件去数货架上的货物。如果有一件货物被移出仓库但送货单还在别人手里没销毁仓库管理员会认为它还在占地方但你数货物的时候根本看不到它。这就是为什么删了大文件之后 df 还显示空间没释放。我实际工作中遇到过最夸张的一次一个 Java 应用把日志文件删了但是进程一直没重启那个文件被句柄持有占了 80G 空间。df 显示满了du 扫遍整个根目录却怎么都找不到那 80G 去了哪里。1.2 为什么会留下已删除但未释放的文件Linux 的文件删除机制是只要有一个进程还持有这个文件的文件描述符fd那么 unlink 之后文件的 inode 不会被真正回收磁盘空间也就不会释放。这在应用服务器上特别常见log4j、logback 这类日志框架经常在运行中手动删除或轮转日志文件如果配置不当旧文件句柄会被一直持有空间就悄无声息地被吃掉了。要定位这种情况lsof 命令是关键工具后面我会详细讲。这里先记住一个排查顺序普通目录空间看 du文件系统整体使用看 df当两者对不上时优先怀疑已删除未释放的句柄然后用 lsof 查找。2. 核心工具选型每种命令解决的问题不一样Linux 下磁盘分析工具不少但没有一个全能选手我一般按场景把它们分成三类快速摸底类、深入分析类、交互可视化类。选对工具比盲目多敲几条命令重要得多。2.1 磁盘整体状况与目录大小统计df -h 是磁盘分析的第一步几乎所有排查都从它开始。-h 参数以人类可读的方式显示大小M、G 会自动换算。我还习惯加上 -T 参数显示文件系统类型比如 xfs、ext4、overlay因为看到 overlay 就说明这台机器可能是 Docker 宿主机后面排查方向就会往容器目录走。du 是目录层面最常用的统计工具但是很多人只会用 du -sh在几十T的数据盘上跑一次要好几分钟。我常用的组合是 du -x --max-depth1 -h 2/dev/null | sort -hr | head -20一次性列出当前目录下一级子目录的大小排名-x 参数让 du 不跨文件系统统计避免把 /proc、/sys 这些虚拟目录也扫进去这两个目录的内容在磁盘上根本不占空间但读取它们的元数据也会浪费时间。sort -hr 是按人类可读大小排序head 取前 20 个直接看到最大的目录是谁。这里有个小技巧du 输出是按目录名排的不加 sort 的话目录多的时候很难一眼看到最大值加上 sort -hr 之后信息就直观多了。不过注意 sort -h 在部分精简版系统上可能不支持可以用 sort -n 配合 du -k 输出 KB 数值来替代。2.2 文件句柄与空间释放排查lsof 是排查“空间神秘消失”问题的核心工具用法非常灵活。最常用的三个场景一是 lsof L1 列出所有已删除但仍有进程打开的文件L1 表示列出 link count 小于 1 的文件也就是被 unlink 掉但还占着空间的文件这是解决 df 和 du 不一致问题的首选命令。二是 lsof | grep deleted 也可以达到同样效果但输出会包含所有进程的信息量大的时候我更喜欢直接用 L1 减少干扰。查询某个进程打开了哪些文件用 lsof -p PID查询某个文件被哪些进程占用用 lsof /path/to/file。还有 lsof D /dir 可以递归列出目录下所有被打开的文件但我很少用这个因为扫描范围太大线上环境容易造成负载波动。2.3 交互式分析与大文件查找ncdu 是 du 的交互式升级版界面类似 ncurses 风格可以在终端里直接上下浏览目录它还带一个排序视图进入每个目录后会自动按大小降序排列。第一次在几十 G 的 log 目录里排查时用 ncdu 进入目录然后一路回车往下钻比一遍遍敲 du 命令效率高太多了。但 ncdu 也有局限它默认不显示被删除但未释放的文件所以这种场景还得靠 lsof。查找大文件还有一个组合命令 find / -type f -size 500M -exec ls -lh {} ; 或者更快的 find / -xdev -type f -size 100M -printf %s %p\n | sort -rn | head -20。-xdev 和 du 的 -x 同理不跨文件系统。这两个命令的基本逻辑就是只要超过 100M 的文件全列出来按大小排序取前 20 个基本能覆盖大部分磁盘突增场景。工具选型我还想提一点之前网络上关于“最大功率传输条件仿真分析实验”“视频分析”之类的热词我不太涉及但是在 Linux 磁盘分析这个方向除了命令行还有图形化工具比如 baobabGNOME 自带和 QDirStat适合在有图形界面的个人电脑上做可视化分析。不过服务器上根本没有 GUI所以命令行手段才是一个运维吃饭的本事。3. 完整排查流程实战从报警到定位到清理前面工具讲了不少但实际跑起来很多人还是会乱这里我把一条我常用的排查路径完完整整写出来每一步用什么命令、看到什么结果怎么判断、下一步该做什么照着走就行。3.1 第一步确认当前文件系统使用率收到磁盘告警后先登录机器执行 df -hT看哪一列 Usage 接近 100%。比如输出里有 /dev/vda1 / xfs 40G 38G 2G 95% /就说明根分区快满了需要立刻处理。如果 /home 或 /data 是独立分区那就需要去对应挂载点目录里排查不要从根目录开始扫避免无关目录影响效率。有时候 df 显示 100% 但实际只写满了旧数据没有新的写入请求系统还能勉强运行。但一旦接近 100%很多程序会报 No space left on device甚至 SSH 登录都会出问题因为写入登录记录需要磁盘空间。遇到这种情况优先释放空间比排查是谁占的更重要。3.2 第二步定位具体的大文件或大目录执行 du -x --max-depth1 -h / 2/dev/null | sort -hr | head先看根目录下哪个一级子目录最大。正常情况下 /var 或 /home 会是重点容器环境还有可能 /var/lib/docker 特别大。然后继续往下一级进入比如 du -x --max-depth1 -h /var 2/dev/null | sort -hr | head一层层钻。在实际操作中我经常用 find 直接跳过目录一层层比较的漫长过程针对已知容易膨胀的目录直接列出最大文件比如 journal 日志目录 /var/log/journal、Docker 的 overlay2 目录、软件打包目录 /opt 等。直接 find /var/log -type f -size 50M -exec ls -lh {} ; 一步到位。3.3 第三步检查已删除未释放的文件这一步骤最关键很多人第一次遇到 df 不降的问题就卡在这。执行 lsof L1看输出中是否有 SIZE 列特别大的记录比如下面这样java 2345 root 23w REG 253,1 8589934592 1048577 /var/log/app.log (deleted)这里面 java 是进程名2345 是 PID23w 表示文件描述符 23 以写模式打开SIZE 显示 8589934592换算过来 8G文件名后面标记了 deleted。这时候就算你在磁盘上把这个文件删了空间也释放不了因为 java 进程还持有句柄。处理办法有两种如果是应用自身的日志轮转配置有问题需要通知开发同学修复同时可以优雅重启应用让句柄释放如果暂时不能重启可以用 cat /dev/null /proc/2345/fd/23 的方式把文件内容清空这样进程后续写入的日志还是走同一个 fd但历史占用的空间会被释放掉。注意这里的 /proc/2345/fd/23 对应 PID 2345 的 fd 23是我前面举例的数字实际操作时要看 lsof 输出里的 FD 列。3.4 第四步用交互工具收尾复查清理完成后不要急着走重新 df -h 确认空间已恢复然后如果目录结构复杂我习惯用 ncdu 再扫一遍目标目录确认有没有漏网之鱼。比如刚才处理了 /var 下的日志可以 ncdu /var 进去看一眼现在哪些子目录占比高如果还有不合理的目录就继续追查。还有一点值得提的是有些云主机的磁盘在控制台看已经缩容或扩容过但操作系统内部文件系统没有同步这时候 df 显示的还是旧容量这种情况不是磁盘占用问题是分区表或文件系统没有扩容需要执行 growpart、resize2fs 或者 xfs_growfs 之类的操作这个不在本次主题范围内但现实中经常被误判为磁盘占用异常。4. 常见问题速查和我的排查心得实践多了就会总结出一些套路排查磁盘占用的时候有几个问题出现的频率非常高单拎出来说说。4.1 日志文件不断变大删了又不释放这个问题前面已经触及但实际场景会更复杂。很多 Java 应用使用 log4j2 或 logback配置文件里设置了滚动策略但滚动出来的旧日志文件如果被应用自身以某种方式持有句柄就会造成空间只增不减。还有一种情况是应用不滚动日志导致单个日志文件无限增大删除文件的话需要重启进程才能释放句柄。一个比较稳妥的做法是先 lsof L1 看看是否有 deleted 文件如果有按第三步处理如果没有再用 find / -xdev -type f -size 100M 找出超大日志文件对文件做截断操作 truncate -s 0 /path/to/log而不是直接 rm。截断保留文件且在 inode 不变的情况下应用写入不受影响磁盘空间却能释放出来。这个操作比 rm 再 touch 高明得多因为后者会改变 inode需要应用重新打开文件才能恢复写入很多应用不会自动重新打开就会陷入“日志不再写入”的坑。4.2 Docker overlay2 目录占用严重容器环境下df -h 经常显示 /var/lib/docker/overlay2 占了大量空间但明明容器镜像很少。这里的原因通常是容器日志没有限制大小。Docker 默认的 json-file 日志驱动可以无限增长除非你给容器设置了 --log-opt max-size10m --log-opt max-file3。很多线上容器没有配置这个参数跑了几周之后日志文件动辄几十 G。定位方法很简单du -h --max-depth2 /var/lib/docker/containers | sort -rh | head找到 containerd 目录下的容器 ID 目录里面 -json.log 结尾的文件就是罪魁祸首。临时解决可以把容器日志文件截断长期方案是在 /etc/docker/daemon.json 里配置 log-opts 限制大小和数量然后重建容器。还有一个容易被忽略的是 Docker 的 overlay2 层中 orphaned 的镜像层。有时候 docker system df 能告诉你镜像、容器、卷各自占用多少。如果镜像和容器没多少但 overlay2 仍然巨大可能是构建镜像时产生的中间层没有被及时清理可以执行 docker system prune -a 清理未被使用的镜像和构建缓存但操作前要确认是否有需要保留的镜像。4.3 inode 耗尽磁盘没满但无法写入inode 耗尽是一个不太常见但一出现就非常棘手的故障。df -h 显示还有 20G 空间但是 touch 新文件却提示 No space left on device这时要看 df -i如果 IUse% 接近 100%就是文件系统上 inode 用完了每个小文件都会消耗一个 inode当文件数量过多时即使数据块还有剩余也没有多余的 inode 来记录新文件。这种情况常见于缓存目录、邮件队列、/tmp 下的小文件堆积。定位方法也是 find / -xdev -type f | wc -l 看总文件数或者 find / -xdev -type d | wc -l 看目录数然后逐级排查。处理办法就是删除大量无用的小文件但数量特别大的时候直接 rm -rf 可能会提示参数太长可以配合 find /path -type f -delete 来删或者使用 rsync --delete 配合空目录同步的清空技巧。4.4 面试和工作中常见的命令考察方向最近有很多同学问 Linux 磁盘分析相关的面试题其实这类问题考察的核心就是三个点会不会用 df 看文件系统、会不会用 du 统计目录、知不知道 lsof 排查已删除未释放文件。再深一点会问为什么要用 -x 参数inode 是什么。面试官真正想看的是你有没有解决过实际问题而不是背命令手册。如果你能把“df 与 du 不一致——lsof L1 定位——cat /dev/null 清空文件——限制应用日志——Docker 日志参数”这一整套逻辑讲清楚比单纯罗列命令有说服力得多。我甚至遇到过面试官现场给一个场景说服务器上删了一个大文件但空间没释放问如果不用重启进程你怎么办其实答案就是 /proc/PID/fd 目录下手动截断那一招这个细节靠死记硬背很难答好得真处理过。4.5 建议形成定期巡检习惯磁盘占用不是等告警了才去分析的我个人的习惯是每周抽几分钟对核心服务器做一个快速巡检命令就三条df -hT 看整体水位df -i 看 inode还有 du -x --max-depth1 -h /var/log 看日志目录有没有异常增长。把这三个结果丢到一个文本里记录一下趋势哪个目录每周都在长大概率就是有隐患。日志和临时文件是磁盘占用的两大主要来源很多团队把日志输出到 stdout 由日志采集器统一收集这种做法释放了本地磁盘压力但也有副作用应用本地就没有日志可查了排查问题时需要去日志平台检索。我个人比较建议日志平台保留一份应用本地至少保留三天的小容量日志方便快速排查故障。5. 结尾一点掏心窝的废话把前面这些工具和套路串起来你会发现排查磁盘占用这件事本质上就三步先用 df 确认是不是真的满再用 du 找大的最后用 lsof 揪出看不见的。多数情况下跑完这三步问题就已经清楚了。如果还没解决大概率是遇到了特殊场景比如文件系统层级的快照、压缩包解压了一半、数据库预分配空间这类问题那就需要结合具体业务去分析了。我个人在实际操作中还有一个很小的习惯就是每次清理完之后会在屏幕前停顿两秒再看一遍 df -h 的所有挂载点避免只处理了一个分区忘了其他分区。另外清理大文件之前一定先用 du -s 确认大小不要凭感觉删毕竟有些文件看着小但是硬链接特别多删一个文件名并不能释放空间得把硬链接全部删除才行。这个坑我掉过一次希望你不用再掉一次。