磁盘没满 df 却报 100%:一次 Linux 磁盘空间排查复盘

服务报 No space left on device,df 说根分区 100%,du 把整盘翻遍却对不上。排查梳理:df 与 du 为何对不上、被删除却没释放的幽灵文件、用 /proc/PID/fd 不重启释放空间、inode 耗尽、logrotate 与 copytruncate 切割日志、挂载点遮盖,以及磁盘清理纪律与告警。

2024 年某天凌晨,一个服务突然开始疯狂报错 No space left on device。我登上服务器一看,df -h 明明白白写着根分区 100%。我心想,清几个大文件不就完了?可接下来一个多小时,我被结结实实地教育了:du 把整个磁盘翻了个底朝天,加起来的占用量却比 df 说的少了几十个 G——磁盘明明"没那么满",df 却咬死了 100%。这件事最后查清,是一个被删除、但仍被进程死死攥着的日志文件在作祟。这次排查逼着我把 Linux 磁盘空间这套东西彻底理清了。本文复盘这次实战。

问题背景

环境:CentOS 7,一台跑了好几个服务的应用服务器
事故现象:
- 服务写文件报 No space left on device
- df -h 显示根分区 / 已用 100%
- 可是 du 把整个 / 扫一遍,加起来对不上 ——
  du 算出来比 df 说的少了好几十 G
- 磁盘"看着没那么满",服务却已经写不进去了

现场排查:
# 1. 先确认是不是真的满了
$ df -h
Filesystem  Size  Used Avail Use% Mounted on
/dev/vda1    50G   50G    0G 100% /
# —— df 说:满了,一点不剩

# 2. 那大文件在哪?du 找一圈
$ du -sh /* 2>/dev/null
...
# 把各目录加起来 —— 只有 18G 左右,
# 离 50G 差着十万八千里!df 和 du 对不上。

# 3. 关键一招:找"被删除但还被进程占用"的文件
$ lsof | grep deleted
java  3721 app  5w REG 253,1 31580911104 ...
      /www/logs/app.log.2024-05-01 (deleted)
# ★ 真相大白:一个 29G 的日志文件被 rm 删了,
#   但 java 进程还开着它 —— 空间根本没释放

根因(后来定位到的):
有人为了"清磁盘",直接 rm 掉了一个巨大的旧日志,
但写这个日志的 java 进程一直没重启。
文件在目录里是没了(du 自然也算不到它),
可只要进程还开着这个文件,它占的磁盘空间
就【一点都不会释放】。这就是 df 和 du 对不上的原因。

修复 1:df 和 du 对不上——被删除却没释放的文件

# === 先理解 df 和 du 到底在数什么,它俩本就不一样 ===
# df:问【文件系统】要数据 —— 整块盘还剩多少空间
# du:自己一个目录一个目录地【遍历文件】累加大小
# 正常情况两者接近,一旦差很多,几乎只有一个原因 ↓

# === 真凶:文件被 rm 了,但进程还开着它 ===
# Linux 删一个文件,只是把它从【目录树】里摘掉。
# 文件真正占的磁盘块,要等到【没有任何进程再打开它】
# 才会被释放。
# 所以:进程还开着 -> du 遍历目录找不到它(已不在目录树)
#       但它的磁盘块没还 -> df 照样算它占着空间。
# 这就是 df 多、du 少的全部秘密。

# === 怎么把这些"幽灵文件"揪出来 ===
$ lsof | grep deleted
java 3721 app 5w REG 253,1 31580911104 /www/logs/app.log (deleted)
#         │              └ 文件大小(字节)~29G
#         └ 进程 PID 3721
# (deleted) 标记 = 这文件已被删,但进程还攥着它

# === 怎么真正把空间要回来 ===
# 办法 A:重启占用它的那个进程(最干净)
$ systemctl restart myapp
# 进程一退出,它开着的所有文件句柄释放,空间立刻回来

# 办法 B:不能重启?用 /proc 把文件"清空" ★ 推荐技巧
$ lsof | grep deleted          # 先拿到 PID 和 fd 号(如 5w 里的 5)
$ : > /proc/3721/fd/5
# /proc/PID/fd/N 是进程那个文件句柄的"活引用",
# 往它写空,就把文件内容清零、空间立刻释放,
# 而且【不用重启进程】—— 救火时极其有用。

# === 验证空间回来了 ===
$ df -h /

修复 2:du——正确地把大文件大目录找出来

# === 排查磁盘,du 是主力,但要会用 ===

# === 找出某个目录下,谁是占用大户 ===
$ du -sh /*  2>/dev/null
# -s 只给汇总(summary)  -h 人类可读(G/M/K)
# 2>/dev/null 把"权限不够"之类的报错丢掉,清爽

# === 找到大户后,逐层往里钻 ===
$ du -h --max-depth=1 /www  | sort -hr | head
# --max-depth=1 只看下一层,不把每个小文件都列出来
# sort -hr 按大小【倒序】排(-h 识别 G/M/K,-r 倒序)
# head 只看最大的几个 —— 一层层 cd 进去重复这招

# === 直接找出单个的大文件(比如 > 500M)===
$ find / -type f -size +500M -exec ls -lh {} \; 2>/dev/null
# -size +500M:大于 500M 的文件
# 这招能直接定位"哪个文件巨大",不用一层层钻

# === 一个常见的坑:du 默认不算"挂载在子目录上的别的盘" ===
$ du -sh /data        # 如果 /data 是另一块盘的挂载点
# du 会把 /data 里的东西算进来,但那是另一块盘的空间。
# 排查 / 满了,要的是 / 这块盘自己的占用:
$ du -sh -x /          # -x 只统计【同一个文件系统】,
#                        不跨进别的挂载点 —— 排查时该加 -x

# === 按修改时间找"最近暴涨"的文件 ===
$ find /www/logs -type f -mtime -1 -size +100M 2>/dev/null
# -mtime -1:最近 1 天内改动过的
# 磁盘"突然"满了,八成是最近某个文件暴涨,这招很准

修复 3:另一种"满"——inode 耗尽

# === 有一种"磁盘满",df -h 看着还有空间,却照样
#     报 No space left on device —— 那是 inode 满了 ===

# === inode 是什么 ===
# 文件系统里,每个文件都要占一个 inode(存元信息:
# 权限、属主、数据块位置...)。inode 的总数,
# 在【格式化磁盘时就定死了】。
# 空间没满、但 inode 用光了 -> 一个新文件都创建不了。

# === 怎么看 inode 用量 ===
$ df -i
Filesystem  Inodes  IUsed IFree IUse% Mounted on
/dev/vda1   3276800 3276800   0  100% /
# ★ IUse% 100% —— inode 用光了!
# 这种情况 df -h 可能还显示有几个 G 空闲,极具迷惑性。

# === 什么场景会耗尽 inode ===
# 海量的【小文件】。比如:
# - session 文件、缓存碎片没清理,几百万个小文件
# - 某个程序 bug,疯狂在某目录生成小文件
# 文件再小也要占一个 inode,数量一多 inode 就见底。

# === 怎么定位:哪个目录下文件数量最多 ===
$ for d in /*; do echo "$(find "$d" 2>/dev/null | wc -l) $d"; done | sort -rn
# 对每个一级目录,数它里面有多少个文件,倒序排。
# 数量爆炸的那个目录,就是 inode 大户。

# === 找到后,删掉那些海量小文件 ===
$ find /var/spool/cache -type f -mtime +7 -delete
# 文件多到 rm * 会报 "参数列表过长" 时,用 find -delete
# 或者:
$ find /tmp/junk -type f -print0 | xargs -0 rm -f

修复 4:日志文件——磁盘被撑满的头号元凶

# === 生产环境磁盘满,十次有八次是日志没人管 ===

# === 救火:安全地清空一个正在被写的大日志 ===
# ★ 千万别 rm 它(就会变成修复 1 那个"幽灵文件")
# 正确做法是【清空内容】,而不是删除文件本身:
$ : > /www/logs/app.log
# 或 truncate:
$ truncate -s 0 /www/logs/app.log
# 文件还在、inode 还在、进程的句柄还有效,
# 只是内容清零、空间立刻释放。进程继续往里写,无感。

# === 治本:用 logrotate 让日志自动切割 ===
# CentOS 自带 logrotate,配置在 /etc/logrotate.d/
$ cat /etc/logrotate.d/myapp
/www/logs/*.log {
    daily                # 每天切一次
    rotate 7             # 只保留最近 7 份,更早的删掉
    compress             # 旧日志压缩成 .gz,省空间
    delaycompress        # 延后一天再压(避开正在写的)
    missingok            # 文件不存在不报错
    notifempty           # 空文件不切
    copytruncate         # ★ 关键:复制后清空原文件,
}                        #   而不是改名 —— 进程无需重启
# copytruncate 这一项尤其重要:它让 logrotate
# 切割时不破坏进程正打开的句柄,免去重启之苦。

# === 手动测试 logrotate 配置对不对 ===
$ logrotate -d /etc/logrotate.d/myapp   # -d 只演练,不真做
$ logrotate -f /etc/logrotate.d/myapp   # -f 强制立刻执行一次

# === 还要盯住系统日志自己 ===
$ journalctl --disk-usage          # systemd 日志占了多少
$ journalctl --vacuum-size=200M    # 把它压缩到只留 200M
$ journalctl --vacuum-time=7d      # 或只留最近 7 天

修复 5:挂载的坑——空间"藏"在被遮盖的目录里

# === 还有一类诡异现象:目录里的文件,被挂载点"遮"住了 ===

# === 先看清楚机器上有哪些盘、各挂在哪 ===
$ df -h
$ lsblk
# lsblk 用树状图把"物理盘 -> 分区 -> 挂载点"画出来,
# 一眼看清哪块盘挂在哪个目录。

# === 这个坑是怎么形成的 ===
# 假设 /data 本该挂一块独立的数据盘。
# 但如果某次重启挂载失败、或挂载前有人往 /data
# 这个【空目录】里写了一堆文件 ——
# 那些文件其实落在了【根分区】上。
# 等数据盘正常挂上来后,这些文件就被挂载点【遮住了】:
# 你 ls /data 看到的是数据盘的内容,
# 而那堆"藏"在下面、占着根分区的文件,你根本看不见。

# === 怎么把"被遮住"的文件揪出来 ===
# 办法:在挂载点【没挂载】的状态下看那个目录。
# 可以临时建个挂载点,用 --bind 把父级挂过去看:
$ mkdir /mnt/check
$ mount --bind / /mnt/check
$ du -sh /mnt/check/data      # 这里看到的才是被遮住的真身
$ umount /mnt/check
# 如果 /mnt/check/data 里有大量文件,就是它们
# 在偷偷占着根分区。把进程停掉、umount 数据盘、
# 清掉这些遮盖文件,再重新挂回去。

# === 检查 fstab,确保开机自动挂载是对的 ===
$ cat /etc/fstab
# 每块要开机挂载的盘都得在这里登记。
$ mount -a            # 按 fstab 把没挂的都挂上(测试用)
# ★ 改完 fstab 一定要 mount -a 验证一遍,
#   否则下次重启挂载失败,又会重演"遮盖"这一幕。

修复 6:磁盘清理的纪律与监控

# === 这次事故也暴露了我们磁盘管理上的随意 ===

# === 1. 删大日志:永远 truncate / : > ,绝不 rm ===
# rm 一个正被写的文件 = 制造"幽灵文件",空间不释放。
# 记住:清空内容用 : > file 或 truncate -s 0 file。

# === 2. 所有会涨的日志,都必须配 logrotate ===
# 应用日志、Nginx 日志、自己脚本的输出 —— 一个都别漏。
# 没接管的日志,就是下一次半夜磁盘报警的源头。

# === 3. 给磁盘加监控告警,别等服务报错才发现 ===
# 简单的自查脚本,配 crontab 跑:
$ cat /opt/scripts/disk_check.sh
#!/bin/bash
USE=$(df / | awk 'NR==2{print $5}' | tr -d '%')
if [ "$USE" -ge 85 ]; then
    echo "[告警] 根分区已用 ${USE}%" | \
      mail -s "磁盘告警 $(hostname)" ops@example.com
fi
# 用量到 85% 就告警 —— 留出处理的缓冲,别等 100%。
$ crontab -e
*/10 * * * * /opt/scripts/disk_check.sh   # 每 10 分钟查一次

# === 4. 排查顺序固化成一套口诀 ===
# 磁盘满了,按这个顺序走,基本不会乱:
#  ① df -h     真的满了吗?满的是哪个分区?
#  ② df -i     是不是 inode 满了(空间却没满)?
#  ③ du 找大户 大文件大目录在哪?
#  ④ lsof |grep deleted   df 和 du 对不上?查幽灵文件
#  ⑤ 挂载遮盖  空间"藏"在挂载点底下?

# === 5. 临时救急可以扩 swap / 清缓存,但别当解药 ===
$ yum clean all                  # 清 yum 缓存
$ rm -rf /var/cache/* 中无用项    # 清各种缓存(看清楚再删)
# 这些只是争取时间,真正要做的还是上面四条治本动作。

# === 6. 留一点"安全余量",别让磁盘真的写到 0 ===
# 磁盘 100% 时,很多服务(数据库尤甚)会直接挂或损坏。
# 监控阈值定在 85%,就是要让你永远在它写满之前介入。

命令速查

需求                        命令
=============================================================
看分区空间占用              df -h
看 inode 占用               df -i
看目录占用并排序            du -h --max-depth=1 /路径 | sort -hr
只统计本文件系统            du -sh -x /
找大文件                    find / -type f -size +500M 2>/dev/null
找最近暴涨的文件            find /路径 -mtime -1 -size +100M
找幽灵文件(已删未释放)    lsof | grep deleted
不重启释放幽灵文件空间      : > /proc/PID/fd/N
安全清空大日志              truncate -s 0 file  或  : > file
看物理盘与挂载树            lsblk
按 fstab 挂载所有盘         mount -a
看 systemd 日志占用         journalctl --disk-usage
清 systemd 日志             journalctl --vacuum-size=200M

口诀:df 看满没满 -> df -i 看 inode -> du 找大户
      -> df/du 对不上查 lsof deleted -> 还找不到查挂载遮盖

避坑清单

  1. df 问文件系统要数据、du 遍历目录累加,两者对不上几乎只因为幽灵文件
  2. rm 一个正被进程打开的文件,空间不会释放,要等进程关闭该文件句柄
  3. 用 lsof | grep deleted 找幽灵文件,重启进程或 : > /proc/PID/fd/N 释放
  4. df -h 显示有空间却报 No space left,十有八九是 inode 耗尽,用 df -i 确认
  5. inode 被海量小文件耗尽,按目录数文件数定位,用 find -delete 批量清理
  6. 清空正被写的大日志永远用 truncate -s 0 或 : > file,绝不能 rm
  7. 所有会增长的日志都要配 logrotate,且加 copytruncate 免重启进程
  8. du 排查单块盘要加 -x,否则会把挂载在子目录的别的盘也算进来
  9. 挂载点会遮盖底层目录里的文件,空间凭空消失时要查被遮盖的文件
  10. 磁盘监控阈值定在 85% 就告警,别等写到 100% 服务才报错甚至损坏

总结

这次"磁盘满了"的排查,把我对 Linux 磁盘空间那种自以为是的轻视,彻底打碎重塑了一遍。出事之前,如果你问我磁盘满了怎么办,我会满不在乎地说"删几个大文件呗",仿佛这是天底下最简单的运维操作。可这一夜让我明白,"磁盘满"这三个字背后,藏着好几种成因完全不同、症状却长得一模一样的问题,而把它们一锅炖、指望靠"删大文件"一招通杀,只会让你在凌晨的终端前越查越懵。我撞上的第一个、也是最颠覆认知的坎,就是 df 和 du 对不上:df 咬定根分区 100%,du 把整块盘翻遍却只数出来一半的占用。我一开始甚至怀疑是不是命令出了 bug,直到我理解了这两个命令根本是在回答两个不同的问题——df 是直接问文件系统这块盘整体还剩多少空间,而 du 是老老实实地一个目录一个目录遍历、把看得见的文件大小累加起来。一旦想通这层,真相就只剩一种可能:有文件占着磁盘空间,却已经不在目录树里了,所以 du 数不到它,df 却依然把它算在内。这正是 Linux 删除文件机制里一个最容易被忽略、却极其关键的事实:rm 一个文件,删掉的只是它在目录树里的那个名字(也就是那条链接),而文件真正占用的磁盘数据块,要等到系统里再没有任何一个进程打开它,才会被释放。所以当有人为了腾空间,直接 rm 掉了一个二十多 G 的旧日志,可写这个日志的 java 进程却一直没有重启——那个文件就成了一个"幽灵":它在 ls 和 du 的世界里已经消失,但它占的二十多个 G,被那个还攥着它句柄的进程死死扣着,一个字节都没还给系统。找出幽灵文件的钥匙是 lsof | grep deleted,而我学到的最实用的一招是:在不能重启进程的情况下,用 : > /proc/PID/fd/N 往进程的那个文件句柄里写空,就能让空间当场释放——这个技巧在救火时简直是雪中送炭。第二个让我开了眼界的坎,是 inode 耗尽。我一直以为"磁盘满"就只有"空间满"这一种,直到我见到 df -h 还显示着好几个 G 空闲、系统却斩钉截铁地报 No space left on device。df -i 一敲,IUse% 赫然是 100%——原来文件系统里每一个文件都要占用一个 inode 来记录它的元信息,而 inode 的总数在格式化磁盘的那一刻就被定死了,空间再富裕,inode 一旦被海量的小文件耗光,你连一个新文件都创建不出来。这让我意识到,排查磁盘问题,df -h 和 df -i 必须是成对出现的两个动作,少看任何一个,都可能在错误的方向上白费好几个小时。第三个坑更隐蔽,是挂载点的遮盖效应:如果在一块数据盘挂载上来之前,有人往那个本该是空的挂载点目录里写过文件,那些文件其实落在了根分区上;等数据盘正常挂回来,它们就被严严实实地盖在了挂载点底下,你 ls 那个目录看到的全是数据盘的内容,而那批偷偷占着根分区的文件,你正常情况下根本瞧不见。这次排查从最初的手忙脚乱,到最后沉淀出一套清晰的口诀——先 df -h 看是哪个分区满、再 df -i 排除 inode、然后 du 去找大户、若 df 和 du 对不上就用 lsof 揪幽灵文件、若还是哪儿都找不到就去查挂载遮盖——我最大的收获,不是记住了多少条命令,而是终于明白:面对"磁盘满"这种症状单一、病因却分好几种的问题,真正靠得住的不是某一招杀手锏,而是一套不跳步、一层层往下排除的诊断顺序。除此之外,这次也逼着我把治本的功夫补上:每一个会持续增长的日志文件,都必须老老实实地交给 logrotate 去自动切割和清理,而且要配上 copytruncate,这样切割时既不破坏进程正打开的句柄、又无需重启服务;同时给磁盘用量加上一个 85% 就告警的监控,把发现问题的时机,牢牢地卡在它写满崩溃之前。磁盘空间的管理,从来不是"满了再删"的被动救火,而是把每一个增长源都提前接管起来的主动治理。

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

端口明明开了却连不上:一次 Linux 防火墙排查复盘

2026-5-20 17:38:23

Linux教程

内存被吃光了?——一次 Linux 内存使用率排查复盘

2026-5-20 17:44:30

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