磁盘明明满了 du 却找不到:一次 Linux 删了文件空间不释放排查复盘

一台核心服务器根分区报 100% 满,服务写入失败,可 du 把所有目录加起来只有一半容量,占空间的文件像凭空蒸发。排查梳理:df 数文件系统的块、du 顺着文件名遍历,两者本不是一回事;rm 删的只是目录项不是数据;文件被删但进程仍持有句柄则空间不释放;lsof grep deleted 揪出幽灵文件、: > /proc/PID/fd/N 就地清空;df -i 看 inode 是否占满;清在用日志只能 truncate 不能 rm,以及一套磁盘空间排查纪律。

2024 年的一个深夜,监控把我叫醒:一台核心服务器的根分区报 100% 占满,服务开始大面积写入失败。我登上去,df -h 一看,/ 分区确实红得发亮——用了 100%。我心想这好办,无非是哪个目录堆了垃圾,du 一把找出来删掉就行。可当我老老实实用 du 把根下面每个目录的体积都加起来,却撞见了一件彻底违背常识的事:所有目录加在一起,只用了不到一半的容量。磁盘明明写着满了,可那些占着空间的文件,我用 du 怎么找都找不到——它们像是凭空蒸发了,只留下一个被占满的磁盘和一脸懵的我。一个文件,既能实实在在地占着磁盘空间,又能让 du 完全看不见它,这件事本身就说明,我对"删除一个文件"到底意味着什么,理解得太浅了。我顺着这个矛盾一路挖下去,最后挖到的真相,把 Linux 里"文件""inode""文件句柄"这几个概念彻底给我串了起来。这件事逼着我把 Linux 的磁盘空间、dfdu、删除文件的底层机制这一整套彻底理清了。本文复盘这次实战。

问题背景

环境:CentOS 7,一台核心服务器,根分区 50G
事故现象:
- df -h 显示 / 分区 use% = 100%,服务写入失败
- ★ du -sh 把所有目录加起来,只有 20 多 G
- 磁盘明明满了,可那 20 多 G 的空间,du 怎么都找不到

现场排查:
# 1. df 确认:根分区确实满了
$ df -h /
Filesystem      Size  Used Avail Use% Mounted on
/dev/vda1        50G   50G   20K 100% /            # ★ 满了

# 2. ★ du 统计:全部加起来对不上
$ du -sh /* 2>/dev/null
... (各目录加总)约 23G                            # ★ 只有 23G!
# 磁盘说用了 50G,du 说只有 23G —— 27G 凭空消失

# 3. ★ 关键一招:找"被删除、但还被进程占着"的文件
$ lsof | grep -i deleted
java  9001 app  5w  REG  253,1  28911062528  /opt/app/logs/app.log (deleted)
#                              ^^^^^^^^^^^ 约 27G   ^^^^^^^^^ ★ (deleted)
# ★ 真相大白:一个 27G 的日志文件,被 rm 删掉了,
#   但 java 进程(PID 9001)还开着它 —— 空间根本没释放!

根因(后来想清楚的):
1. ★ 之前有人发现 app.log 太大,直接 rm 把它删了。
2. ★ 但 rm 删的只是【目录里那个文件名】,不是数据本身。
3. 当时 java 进程还【开着】这个文件在持续写日志。
4. ★ 文件名没了,所以 du(按目录名遍历)看不见它;
   但只要进程还开着它,数据和空间就【一直占着】,
   所以 df(按文件系统块统计)看得见这 27G。
5. du 看不见、df 看得见 —— 矛盾的根源就是这个
   "已删除但未释放"的文件。
6. ★ 真正的释放:要么重启/通知进程关掉这个文件,
   要么 truncate 把它清空 —— rm 在这种场景下没用。
df 与 du 对不上,先查"被删除却没释放"的文件。

修复 1:df 与 du 到底在量什么——为什么会对不上

# === ★ df 和 du 都说"用了多少空间",但它们量的【不是一个东西】===

# === df:站在【文件系统】的角度数"块" ===
$ df -h /
Filesystem      Size  Used Avail Use%
/dev/vda1        50G   50G   20K 100%
# df 问的是文件系统:"你一共有多少数据块?用掉了多少?"
# ★ 只要一个文件的数据【还占着块】,df 就把它算进 Used,
#   它【根本不关心】这个文件还有没有名字。

# === du:站在【目录树】的角度,一个个文件名累加 ===
$ du -sh /opt
# du 干的事是:从某个目录出发,【遍历目录项】,
#   把每个【还有名字的文件】的大小加起来。
# ★ 它是顺着"文件名"去找文件的 —— 一个文件如果
#   【没有名字了】,du 这条路就【永远走不到它】。

# === ★ 于是矛盾产生了 ===
# 一个文件被 rm 删了名字,但进程还开着它:
#   - 数据块还占着  -> df 算得到(df 数块)
#   - 文件名没了    -> du 找不到(du 顺着名字找)
# 这就是 df 满、du 对不上的【根本原因】。

# === 还有一个小差异:du 默认按"占用的块"算 ===
$ du -h --apparent-size file    # 看文件【逻辑大小】
$ du -h file                    # 看文件【实际占块】(默认)
# 稀疏文件、小文件,这两个值会不一样,但通常差不大。

# === ★ 一句话记住 ===
# df 数的是"文件系统里被占的块"(看得见无名文件);
# du 数的是"目录树里还有名字的文件"(看不见无名文件)。
# 两者对不上,差额几乎总是"已删除但未释放"的文件。

修复 2:被删除却没释放的文件——这次的真凶

# === ★ 核心问题:在 Linux 里,rm 一个文件到底删掉了什么 ===

# === rm 删的是"目录项(文件名)",不是"数据" ===
# 一个文件在磁盘上,其实是两部分:
#   - 一个 inode + 数据块  —— 真正的内容
#   - 一个目录项           —— 把"文件名"指向那个 inode
# ★ rm 干的事,只是删掉【目录项】,并把 inode 的
#   "链接数"减 1。

# === ★ 数据什么时候才真的被释放 ===
# 文件系统释放数据块,要同时满足【两个】条件:
#   1. 没有任何目录项指向它了(链接数 = 0)
#   2. ★ 没有任何进程还开着它(打开的文件句柄数 = 0)
# 只要还有一个进程开着它,哪怕名字早删了,
#   数据和空间【一直保留】—— 这就是这次 27G 的去向。

# === ★ 找出这种"被删除却没释放"的文件 ===
$ lsof | grep -i deleted
java  9001 app  5w  REG  253,1  28911062528  /opt/app/logs/app.log (deleted)
#     ^^^^      ^^^                ^^^^^^^^^^^                      ^^^^^^^^^
#     PID       fd(第5号,w=写)    占用字节数                       ★ 已删除标记
# ★ (deleted) 就是铁证:文件名没了,但进程还开着它。

# === 更聚焦的查法:只看占空间大的 ===
$ lsof +L1
# +L1 = 只列出"链接数 < 1"的文件(即已被删除却仍打开的)
# 比 grep deleted 更准,专门干这个。

# === ★ 怎么把空间真正要回来 ===
# 办法 A:重启持有它的进程(最干净)
$ systemctl restart myapp        # 进程一关,句柄释放,空间立即回来

# 办法 B:不能重启?用 /proc 把那个文件【就地清空】
$ ls -l /proc/9001/fd/5          # 9001=PID,5=lsof 里看到的 fd
lrwx... /proc/9001/fd/5 -> /opt/app/logs/app.log (deleted)
$ : > /proc/9001/fd/5            # ★ 把这个已删文件的内容清空
# 进程还开着句柄,但文件内容被清掉,空间立刻释放,
#   而且进程不用重启、继续往这个(空的)句柄写。

修复 3:inode 也会满——另一种"明明没满却写不进"

# === ★ 磁盘"满"有两种,空间满只是其一,inode 满是另一种 ===

# === 现象:df -h 还有空间,却报 "No space left on device" ===
# 这种诡异情况,八成是 inode 用尽了。

# === inode 是什么 ===
# 每一个文件(哪怕是 0 字节的空文件),都要占用【一个 inode】。
# 文件系统在格式化时,inode 的【总数就定死了】。
# ★ 如果你有海量的小文件 —— 比如几百万个 session 缓存、
#   几百万个邮件碎片 —— 空间没用完,inode 先用光了。

# === ★ 用 df -i 看 inode 使用情况 ===
$ df -i /
Filesystem      Inodes  IUsed  IFree  IUse% Mounted on
/dev/vda1      3276800 3276800     0   100% /         # ★ inode 满了!
# IUse% 100% = 一个新文件都创建不了,即使 df -h 显示有空间。

# === ★ 找出"哪个目录文件数最多" ===
$ for d in /*; do echo "$(find $d 2>/dev/null | wc -l) $d"; done | sort -rn
2891022 /opt/app/cache       # ★ 这个目录,289 万个文件!
   5102 /var
   ...
# 顺着这个目录进去,你会发现海量的小碎文件。

# === 也可以直接统计某目录下的文件数 ===
$ find /opt/app/cache -type f | wc -l
2891022

# === 解决:清掉海量小文件;治本要改程序 ===
$ find /opt/app/cache -type f -mtime +7 -delete   # 删 7 天前的
# ★ 治本:让程序别再产生海量碎文件 —— 比如缓存换成
#   Redis、小文件合并存储等。
# ★ 排查"磁盘满"养成习惯:df -h 和 df -i 【两个都看】。

修复 4:找出真正的大文件、大目录——du 的正确姿势

# === ★ 确认是"空间"满了之后,要快速定位是谁占的 ===

# === 第一步:看根下面哪个一级目录最大 ===
$ du -sh /* 2>/dev/null | sort -rh | head
23G   /opt
8.1G  /var
2.3G  /usr
...
# -s 只看汇总  -h 人类可读  2>/dev/null 屏蔽没权限的报错
# sort -rh 按人类可读大小【倒序】,head 取最大的几个。

# === ★ 第二步:钻进最大的目录,逐层往下挖 ===
$ du -sh /opt/* | sort -rh | head
20G   /opt/app
3G    /opt/data
...
$ du -sh /opt/app/* | sort -rh | head
19G   /opt/app/logs        # ★ 一层层挖,锁定到 logs
# 这种"看汇总 -> 进最大的 -> 再看汇总"的钻取,
#   几下就能定位到真正的大头。

# === 只挖到某个深度,不被刷屏 ===
$ du -h --max-depth=1 /opt | sort -rh
# --max-depth=1 = 只统计到下一层,不把所有子文件都列出来。

# === ★ 直接找出全盘最大的几个【文件】 ===
$ find / -xdev -type f -size +500M -exec ls -lh {} \; 2>/dev/null
# -xdev   ★ 不跨文件系统(不然会跑去 /proc /sys 乱统计)
# -size +500M  只找大于 500M 的文件
# 一条命令把"巨型文件"直接揪出来。

# === ★ 一个高频疏忽:du 看不见"被删未释放"的文件 ===
# 如果你 du 半天找不到大头,而 df 又确实满 ——
# ★ 立刻回到【修复 2】,查 lsof | grep deleted。
# 这次事故的教训就是:别在 du 这条路上死磕,
#   du 找不到的空间,要去 lsof 里找。

修复 5:日志文件的正确清理方式——别再 rm 了

# === ★ 这次事故的直接导火索:有人用 rm 删了一个【在用的】日志 ===

# === 错误示范:对正在被写的日志 rm ===
$ rm /opt/app/logs/app.log
# ★ 后果:程序还开着这个文件的句柄在写,
#   - 空间不释放(链接数 0 但句柄还在)
#   - 程序继续往那个"已删除"的句柄里写,数据进了黑洞
#   - df 满、du 找不到 —— 就是这次的局面。

# === ★ 正确做法 1:用 truncate 把文件【清空】 ===
$ truncate -s 0 /opt/app/logs/app.log
# 或者等价的:
$ : > /opt/app/logs/app.log
$ cat /dev/null > /opt/app/logs/app.log
# ★ 这是把文件【内容】清掉,但【文件名/inode 还在】。
#   程序的句柄还指向同一个文件,继续写没问题,
#   空间【立刻】释放。这才是清在用日志的正解。

# === ★ 正确做法 2:交给 logrotate 管,根本不手动删 ===
$ cat /etc/logrotate.d/myapp
/opt/app/logs/*.log {
    daily                  # 每天切割
    rotate 7               # 保留 7 份
    compress               # 旧的压缩
    missingok
    notifempty
    copytruncate           # ★ 关键:复制一份后,truncate 原文件
}
# ★ copytruncate:先把日志复制走,再 truncate 原文件 ——
#   程序的句柄不受影响,不用重启,空间正常回收。
# 没有 copytruncate 时,rotate 后要用 postrotate 通知程序重开文件。

# === 验证 logrotate 配置对不对 ===
$ logrotate -d /etc/logrotate.d/myapp     # -d 只调试不执行
$ logrotate -f /etc/logrotate.d/myapp     # -f 强制立即切一次

# === ★ 一句话纪律 ===
# 清理"正在被程序写的"日志:用 truncate / : > ,绝不用 rm。
# rm 只适合清理【没有任何进程在用】的文件。

修复 6:磁盘空间排查纪律

# === 这次事故暴露的认知盲区,定几条纪律 ===

# === 1. ★ 磁盘满,df -h 和 df -i 两个都要看 ===
$ df -h /        # 看空间满没满
$ df -i /        # 看 inode 满没满(海量小文件会先满这个)

# === 2. ★ df 满但 du 对不上 -> 查"被删除未释放"的文件 ===
$ lsof | grep -i deleted
$ lsof +L1
# 这是 df/du 矛盾的【头号原因】,务必第一时间想到。

# === 3. ★ 清在用的日志,用 truncate,绝不用 rm ===
$ truncate -s 0 大日志        # 对  —— 空间立即释放
$ rm 大日志                   # ★ 错 —— 进程开着就不释放

# === 4. 已经 rm 了、空间没回来,两条路 ===
$ systemctl restart 进程      # A:重启进程(最干净)
$ : > /proc/PID/fd/N          # B:就地清空那个已删文件句柄

# === 5. 定位大头:du 钻取 + find 找巨型文件 ===
$ du -sh /* | sort -rh | head
$ find / -xdev -type f -size +500M 2>/dev/null

# === 6. 日志交给 logrotate,配 copytruncate ===
# 别等磁盘满了手动救火,从一开始就让日志自动切割。

# === 7. 排查磁盘问题的命令链 ===
$ df -h                       # ① 哪个分区空间满
$ df -i                       # ② 是不是 inode 满
$ du -sh /* | sort -rh | head # ③ 哪个目录最大
$ lsof | grep deleted         # ④ ★ 有没有被删未释放的
$ find / -xdev -size +500M    # ⑤ 全盘巨型文件
# 按这个顺序,磁盘空间问题基本能定位。

命令速查

需求                        命令
=============================================================
看分区空间使用              df -h
看 inode 使用               df -i
看目录占用并排序            du -sh /* | sort -rh | head
只统计到下一层              du -h --max-depth=1 目录
找被删除却没释放的文件      lsof | grep deleted  /  lsof +L1
就地清空已删文件句柄        : > /proc/PID/fd/N
清空正在写的日志            truncate -s 0 文件  /  : > 文件
全盘找巨型文件              find / -xdev -type f -size +500M
统计某目录文件数(查inode)  find 目录 -type f | wc -l
调试 logrotate 配置         logrotate -d /etc/logrotate.d/xxx

口诀:df 满 du 找不到 -> lsof grep deleted 查被删未释放的文件
      清在用的日志只能 truncate,rm 会让空间永远要不回来

避坑清单

  1. 磁盘满先 df -h 看空间、df -i 看 inode,海量小文件会先把 inode 占满
  2. df 显示满但 du 加总对不上,第一时间查 lsof | grep deleted
  3. rm 删的只是目录项(文件名),不是数据本身,数据释放还需要没进程开着它
  4. 文件被 rm 后进程仍持有句柄,空间一直不释放,du 也看不见这个无名文件
  5. 清理正在被程序写入的日志必须用 truncate 或 : >,绝不能用 rm
  6. 已经 rm 了空间没回来,重启进程或用 : > /proc/PID/fd/N 就地清空
  7. du 默认数有名字的文件,df 数文件系统的块,两者本就不是一回事
  8. du 找大文件时加 -xdev 不跨文件系统,否则会跑去统计 /proc /sys
  9. df -h 有空间却报 No space left,八成是 inode 用尽,用 df -i 确认
  10. 日志交给 logrotate 自动切割,配 copytruncate 避免重启进程

总结

这次"磁盘明明满了、du 却怎么都找不到那些空间"的事故,纠正了我一个埋藏得很深的、关于"删除一个文件到底发生了什么"的错误认知。在这次之前,我心里对"删文件"的理解,朴素得近乎天真:一个文件,就是磁盘上的一坨数据;我执行 rm,就是把这坨数据抹掉;数据没了,它占的空间自然就回来了。在这个理解里,"文件名"和"文件数据"是浑然一体、不分彼此的同一个东西,rm 这个动作,干净利落,一步到位。正是这个假设,让我面对那个矛盾的现场时彻底卡住了:df 斩钉截铁地说磁盘用了 50G,du 把每个目录都翻遍了却只数出 23G,中间那 27G,既不在任何一个目录里,又确确实实地占着磁盘——一个文件怎么可能"既存在又不存在"?复盘到根上,我才真正看清,在 Linux 里,我以为浑然一体的那个"文件",其实是被拆成了两个彼此独立的东西。一个,是 inode 加上它名下的数据块,这是文件真正的"实体"——内容本身;另一个,是目录里的一个"目录项",它仅仅是一个"名字",一根指向那个 inode 的指针。我执行 rm 的时候,我以为我抹掉了实体,可我实际抹掉的,只是那个"名字"——那根指针。Linux 把这件事看得很清楚:一个文件的数据,要到什么时候才真正被释放?它要同时满足两个条件——没有任何"名字"再指向它了,并且,没有任何"进程"还开着它。rm 只完成了前一个条件,而这次,那个 27G 的日志文件,后一个条件死死地没满足:就在它被 rm 删掉名字的那一刻,有一个 Java 进程正开着它、正一刻不停地往里写日志。于是这个文件就进入了一种我从未想象过的中间状态:它没有名字了,所以顺着目录树、顺着文件名去遍历的 du,再也找不到它——对 du 而言它已经"死了";可它的数据块还被一个活着的进程牢牢攥在手里,文件系统一个块都不敢回收——对数着磁盘块的 df 而言它"活得好好的"。du 看不见、df 看得见,这个把我搞懵的矛盾,恰恰就是这个"已删除但未释放"的幽灵文件存在的铁证。揭穿它的,是 lsof | grep deleted 这一行命令——那个带着 (deleted) 标记、却依然占着 27G 的文件,清清楚楚地躺在输出里。这次从一个"空间凭空消失"的谜题出发,我最大的收获,是终于把"文件名"和"文件数据"在脑子里彻底分了家。rm 从来不是我想象中那个一步到位的"抹除",它只是"解除一个名字的绑定";数据真正的生死,还攥在"有没有进程开着它"这只看不见的手里。也正因如此,清理一个正在被程序写入的日志,rm 是彻头彻尾的错误——它删掉名字,却既要不回空间,又让程序继续往一个没有名字的黑洞里倾倒数据;正确的做法,是用 truncate 把文件的"内容"清空,而把那个"名字"原封不动地留给还开着它的进程。一个文件,远不只是一坨数据那么简单;而"删除",也远不是抹掉那么干脆。当 dfdu 给出两个对不上的数字时,它们没有一个在撒谎——它们只是在从两个不同的角度,共同向你描述一个你过去从不知道其存在的、悬在名字与数据裂缝之间的幽灵。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
Linux教程

调外部接口莫名卡 5 秒:一次 Linux DNS 解析排查复盘

2026-5-20 19:45:53

Linux教程

服务跑着跑着报 Too many open files:一次 Linux 文件描述符限制排查复盘

2026-5-20 19:52:00

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索