服务器磁盘突然爆满:一次 Linux 磁盘空间排查的复盘

深夜服务器报 No space left on device,根分区 100%。可 du 和 df 算出来对不上,删了几十 G 空间纹丝不动,另一台磁盘有空间却写不进去。一夜把 Linux 磁盘排查梳理透:df 看空间与 inode、du 逐层定位、lsof 揪删除占用文件、inode 耗尽、logrotate 根治、磁盘预警。

2024 年的一个深夜,我们的一台核心服务器突然开始报错,业务大面积写入失败,日志里刷满了 No space left on device。我登上去一看,df 显示根分区 100% 满了。这种事看着简单——磁盘满了嘛,清一清不就好了?可真动起手来才发现,Linux 的磁盘空间问题处处是坑:有时候你 du 算出来的大小和 df 显示的对不上,有时候你明明删了几十 GB 文件、空间却纹丝不动,有时候磁盘空间一大把、系统却死活说没空间了。这一夜我把 Linux 磁盘空间排查这套东西从头到尾梳理了一遍,本文复盘这次实战,把每个坑都讲清楚。

问题背景

环境:CentOS 7 服务器,业务服务 + MySQL + 大量日志
事故现象:
- 业务写入失败,报 No space left on device
- 服务无法写日志,部分进程异常退出
- df 显示根分区 / 已 100%

现场排查:
# 1. 第一眼:看各分区使用率
$ df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/vda1        99G   99G   0G  100% /
tmpfs           3.9G     0  3.9G   0% /dev/shm
/dev/vdb1       500G  210G  290G  43% /data
# 根分区 / 满了,/data 还有空间

# 2. 想找出谁占的,du 一把
$ du -sh /* 2>/dev/null
# 算出来各目录加起来才 60G 多,可 df 说用了 99G
# —— du 和 df 对不上!这是第一个谜

# 3. 进一步发现:有进程一直在写一个"已删除"的文件
# 4. 还有一台机器:df 显示有空间,却照样报 No space

根因(这一夜揭开的几个真相):
1. 日志文件被 du 算漏 —— 有一个被删除但仍被
   进程占用的巨型日志,空间没释放
2. 另一台是 inode 耗尽:小文件太多,
   空间没满但 inode 用光了
3. 根本问题:日志无限增长、没有 logrotate、
   没有任何磁盘预警

修复 1:df 先看清——空间满,还是 inode 满

# === 第一步永远是 df,先看清战场 ===
$ df -h
# -h 人类可读(G/M),看每个分区的 Used / Avail / Use%

# 关键:先确认到底是【哪个分区】满了。
# 是 / 满了,还是 /data 满了,还是 /var 单独挂载满了?
# 不同分区,排查的目录范围完全不同。

# === 第二步:df -i,看 inode 用量 ===
$ df -i
Filesystem      Inodes   IUsed    IFree IUse% Mounted on
/dev/vda1      6553600 6553600        0  100% /
# ★ 注意这个场景:Use% 可能不到 100%,
#   但 IUse% 到了 100% —— inode 耗尽!

# === 为什么要分别看这两个 ===
# Linux 文件系统里,存一个文件需要两样东西:
#   1. 数据块(block):存文件【内容】,df -h 看的是它
#   2. inode:存文件的【元信息】(权限/大小/位置...),
#      每个文件、每个目录都要占用【一个】 inode
# 这两样,任何一个用光,你都会得到
# "No space left on device"。
# 所以排查磁盘满,df -h 和 df -i 必须【都看】。

# === 一个常见误判 ===
# 只看 df -h 发现没满,就以为磁盘没问题 ——
# 结果是 inode 满了。海量小文件(如 session 文件、
# 小缓存、邮件队列)最容易把 inode 耗尽。

修复 2:du 自顶向下,层层定位大目录

# === 确认是空间满之后,用 du 找出"谁在占" ===

# 列出根下每个一级目录的总大小
$ du -sh /* 2>/dev/null
# -s 只看汇总(summary),不列出每个子文件
# -h 人类可读
# 2>/dev/null 把 "Permission denied" 之类报错丢掉

# === 技巧:排序,让最大的目录浮上来 ===
$ du -sh /* 2>/dev/null | sort -rh | head -10
# sort -r 倒序,-h 按人类可读的大小排
# 一眼看到最占空间的 10 个目录

# === 然后"钻"进去,逐层收敛 ===
# 假设上面发现 /var 最大,就进 /var 再来一次
$ du -sh /var/* 2>/dev/null | sort -rh | head -10
# 发现 /var/log 大 -> 再钻
$ du -sh /var/log/* 2>/dev/null | sort -rh | head -10
# 这样一层层往下,很快能定位到具体是哪个大文件 / 大目录

# === 更顺手的工具:ncdu ===
$ ncdu /var
# ncdu 是个交互式的 du,用方向键就能进出目录、
# 实时看大小排序,比反复敲 du 高效得多。
# 服务器上值得装一个。

# === 一个关键陷阱:du 要在"正确的根"上算 ===
# 如果某个目录是另一个分区的挂载点,
# du / 会把那个分区的内容也算进去,
# 加上 -x 让 du 不跨文件系统:
$ du -shx /* 2>/dev/null | sort -rh
# -x:只统计和起点在【同一个文件系统】的内容

修复 3:最坑的一种——文件删了,空间却没回来

# === 我们这次的核心谜题:du 和 df 对不上 ===
# df 说根分区用了 99G,du 把所有目录加起来才 60G。
# 凭空消失的 39G 去哪了?

# === 答案:被删除、但仍被进程打开的文件 ===
# Linux 删除文件的真相:
# rm 一个文件,删的只是这个文件名(目录项)。
# 文件占用的磁盘空间,要等到【没有任何进程
# 还打开着它】时,才会真正被释放。
#
# 如果一个进程打开了一个日志文件正在写,
# 这时你 rm 掉这个日志文件:
#   - 文件名没了 -> du 扫不到它了(目录里找不到)
#   - 但进程还开着它、还在往里写
#   - -> 空间一直被占着,而且还在涨!
#   - -> df 算得到(空间真被占),du 算不到(没文件名)
# 这就是 du 和 df 对不上的最经典原因。

# === 用 lsof 揪出这些"幽灵文件" ===
$ lsof | grep deleted
java   12345  app  3w  REG  253,1  41875193856  /var/log/app/biz.log (deleted)
# 翻译:进程 java(PID 12345)还开着一个已被删除的
# 文件,大小 41875193856 字节(约 39G)—— 元凶找到了

# === 怎么把空间要回来 ===
# 方法 1(推荐):重启 / 优雅重载那个进程,
#   它一关闭文件句柄,空间立刻释放。
$ systemctl restart app

# 方法 2(不重启,应急):把文件内容清空
#   通过 /proc 找到那个打开的文件句柄,truncate 它
$ ls -l /proc/12345/fd | grep deleted
# 找到对应的 fd 号(比如 3),清空它:
$ : > /proc/12345/fd/3
# 这样空间立刻释放,进程也不用重启
# (进程会继续往这个 fd 写,只是从头开始)

# === 教训 ===
# 永远不要用 rm 去"清理"一个正在被写入的日志文件。
# 要清空一个在用的日志,正确做法是 truncate:
$ truncate -s 0 /var/log/app/biz.log
# 或: : > /var/log/app/biz.log
# 它清空内容、但保留文件本身和进程的句柄,空间立即回收。

修复 4:inode 耗尽——空间还有,却写不进去

# === 另一台机器的故事:df -h 没满,却报 No space ===
$ df -h /
/dev/vda1   99G   62G   37G  63%  /
# 空间用了 63%,明明还有 37G!

$ df -i /
/dev/vda1   6553600  6553600  0  100%  /
# inode 100% —— 真相大白:inode 耗尽了

# === inode 为什么会先于空间耗尽 ===
# 每个文件不管多小,哪怕是 0 字节,都要占 1 个 inode。
# 如果某个程序疯狂产生海量【小文件】:
#   - 几百万个几 KB 的缓存文件
#   - PHP session 文件没清理
#   - 某个目录堆了千万级小文件
# 它们占的总空间可能不大,但每个都吃掉一个 inode,
# inode 数量是文件系统【格式化时就定死的】,
# 用光了就再也建不了新文件。

# === 怎么定位:哪个目录的文件数最多 ===
# 统计某目录下各子目录的"文件个数"(不是大小)
$ for d in /var/*; do
    echo "$(find "$d" -type f 2>/dev/null | wc -l)  $d"
  done | sort -rn | head
# 找出文件数量最爆炸的那个目录

# 或直接在可疑目录里数
$ find /var/spool/cron -type f | wc -l

# === 处理 ===
# 找到海量小文件的目录后,清理掉无用的小文件。
# 注意:目录里文件极多时,ls 会很慢甚至卡住,
# rm * 还可能因为参数过长报错。用 find 删:
$ find /tmp/cache -type f -mtime +7 -delete
# 删除 /tmp/cache 下 7 天前的文件

# === 根治 ===
# 1. 让产生小文件的程序定期清理(自带过期机制)
# 2. inode 是格式化时定的,事后改不了 ——
#    如果业务天生就是海量小文件,
#    格式化时就要调大 inode 密度,或换 XFS
#    (XFS 的 inode 是动态分配的,不易耗尽)

修复 5:日志失控——logrotate 才是根治

# === 这次事故的真正根源:日志没人管 ===
# 那个 39G 的 biz.log,是从服务上线那天起,
# 一个文件一直写到现在,从来没切割过。
# 治标是清掉它,治本是让日志【自动轮转】。

# === logrotate:Linux 自带的日志轮转工具 ===
# 它能定期把大日志切成小份、压缩旧的、删除过老的。

# 给业务日志写一个 logrotate 配置
$ cat /etc/logrotate.d/app
/var/log/app/*.log {
    daily                # 每天轮转一次
    rotate 14            # 保留最近 14 份,更老的删除
    size 500M            # 或:超过 500M 就立刻轮转
    compress             # 旧日志用 gzip 压缩,省空间
    delaycompress        # 最近一份先不压(可能还在写)
    missingok            # 日志文件不存在也不报错
    notifempty           # 空文件不轮转
    copytruncate         # ★ 关键:见下方说明
}

# === copytruncate 为什么关键 ===
# 轮转日志有两种方式:
# 方式 A(默认):把 biz.log 改名成 biz.log.1,
#   再建一个新的 biz.log。
#   问题:进程的文件句柄还指着【那个被改名的文件】,
#   它会继续往 biz.log.1 里写 —— 新的 biz.log 是空的!
#   所以方式 A 必须配合给进程发信号(postrotate 里
#   reload),让它重新打开日志文件。
# 方式 B(copytruncate):先把 biz.log 拷贝一份出去,
#   再把 biz.log 本身 truncate 清空。
#   进程的句柄不变,继续往同一个文件写。
#   不需要通知进程 —— 这就是修复 3 讲的 truncate 思路。
# 不确定怎么选时,copytruncate 最省心。

# === 手动测试配置对不对 ===
$ logrotate -d /etc/logrotate.d/app   # -d 只演练,不真做
$ logrotate -f /etc/logrotate.d/app   # -f 强制立即轮转一次

# === logrotate 自己怎么被定时触发 ===
# 它通常由 /etc/cron.daily/logrotate 每天跑一次,
# 由 cron 或 systemd timer 驱动。确认这个机制是活的。

修复 6:别再等满了才知道——磁盘预警

# === 最后一课:磁盘满是"慢性病",完全可以提前发现 ===
# 它不是突然爆发的,是一点点涨上来的。
# 装个预警,就能在 80% 的时候提醒你,而不是 100% 才出事。

# === 一个最简单的自检脚本 ===
$ cat /usr/local/bin/disk_check.sh
#!/bin/bash
# 检查所有分区,使用率超过阈值就告警
THRESHOLD=85
df -h --output=pcent,target | tail -n +2 | while read pcent target; do
    use=${pcent%\%}
    use=${use// /}
    if [ "$use" -ge "$THRESHOLD" ]; then
        echo "[磁盘告警] $target 使用率 ${use}%"
        # 这里接入你的告警:webhook / 邮件 / 短信
    fi
done
# 别忘了 inode 也要查
df -i --output=ipcent,target | tail -n +2 | while read ipcent target; do
    use=${ipcent%\%}
    use=${use// /}
    if [ "$use" -ge "$THRESHOLD" ]; then
        echo "[inode 告警] $target inode 使用率 ${use}%"
    fi
done

# === 用 crontab 让它每 10 分钟跑一次 ===
$ crontab -e
*/10 * * * * /usr/local/bin/disk_check.sh

# === 生产环境更推荐:接入监控系统 ===
# node_exporter + Prometheus + Grafana,
# 把每个分区的 space 和 inode 使用率画成曲线,
# 设阈值告警。还能看到"增长趋势" ——
# 趋势比单个时刻的数值更有价值:
# 它能让你预判"再过 3 天就会满",提前处理。

命令速查

场景                        命令
=============================================================
看各分区空间使用率          df -h
看 inode 使用率             df -i
找占空间的大目录            du -sh /* | sort -rh | head
交互式找大文件              ncdu /path
du 不跨文件系统             du -shx /*
揪出删除但被占用的文件      lsof | grep deleted
清空在用的日志(不删文件)  truncate -s 0 file  或  : > file
应急清空被占用文件          : > /proc//fd/
统计目录下文件个数          find /path -type f | wc -l
批量删旧文件                find /path -type f -mtime +7 -delete
演练 logrotate 配置         logrotate -d /etc/logrotate.d/xxx
强制轮转一次                logrotate -f /etc/logrotate.d/xxx

排查顺序口诀:
df -h 看空间 -> df -i 看 inode -> du 定位大目录
-> du/df 对不上就 lsof grep deleted -> 治本靠 logrotate

避坑清单

  1. 排查磁盘满,df -h 和 df -i 必须都看,空间和 inode 任何一个用光都报 No space
  2. 先用 df 确认是哪个分区满了,不同分区排查的目录范围完全不同
  3. du -sh /* | sort -rh 自顶向下逐层定位,或用交互式的 ncdu 更高效
  4. du 默认会跨挂载点统计,加 -x 只算同一文件系统的内容,避免误判
  5. du 和 df 对不上,十有八九是删除了但仍被进程占用的文件,空间没释放
  6. 用 lsof | grep deleted 揪出这些幽灵文件,重启进程或 truncate 其 fd 即可回收
  7. 清空正在写入的日志要用 truncate 或 : > file,绝不能用 rm,rm 会导致空间不释放
  8. inode 耗尽时空间可能还有大把,海量小文件是元凶,用 find 统计文件数定位
  9. 日志失控才是根因,用 logrotate 自动轮转压缩,不确定就用 copytruncate
  10. 磁盘满是慢性病,务必做空间和 inode 的预警,看增长趋势提前处理而非事后救火

总结

这一夜的磁盘排查,表面上是处理一次"磁盘满了"的事故,实际上它把我对 Linux 文件系统的理解狠狠地补了一课。出事之前,如果你问我磁盘满了怎么办,我大概会轻飘飘地回答"清一清呗"。这一夜之后我才明白,"磁盘满"这三个字背后,藏着好几个截然不同、又彼此独立的真相,而排查的全部功夫,就在于先分清你撞上的到底是哪一个。第一个要建立的认知是:在 Linux 里,存一个文件需要两样东西,一样是存文件内容的数据块,另一样是存文件元信息的 inode,而这两样东西,任何一个用光,系统都会一视同仁地甩给你那句 No space left on device。这就是为什么排查磁盘满,第一步永远是 df,而且 df -h 看空间、df -i 看 inode,这两条命令必须成对地敲——我见过太多人只看了 df -h,发现空间还有富余就以为磁盘没问题,结果真正的元凶是 inode 早已耗尽。inode 耗尽这种情况尤其有迷惑性:空间明明还剩几十 GB,系统却死活写不进一个新文件,因为 inode 的数量在文件系统格式化的那一刻就被定死了,海量的小文件——没人清理的 session、堆积的小缓存——会在空间还很宽裕的时候,就先把 inode 这一侧悄悄掏空。第二个、也是这一夜最烧脑的一个真相,是 du 和 df 这两个数字对不上的谜题。df 言之凿凿地说根分区用掉了 99G,可我把所有目录用 du 加起来,怎么算都只有 60G,凭空消失的将近 40G,到底藏在哪里?答案藏在 Linux "删除文件"这个动作的本质里:你执行 rm,删掉的其实只是那个文件名、那个目录项而已;文件实际占用的磁盘空间,要一直等到再也没有任何进程打开着它的时候,才会被真正释放。于是就有了这样一种幽灵般的存在——一个进程正开着一个日志文件不停地写,这时有人 rm 掉了这个日志文件,文件名没了,du 顺着目录树再也扫不到它,可那个进程依然开着这个文件的句柄、依然在往里灌数据,空间不但没释放,还在持续地涨。df 统计的是文件系统真实被占用的块,所以它看得见这 40G;du 是顺着目录树去数文件的,所以它看不见。揪出这种幽灵文件,靠的是 lsof 配合 grep deleted,它会清清楚楚地告诉你哪个进程的句柄还攥着一个已被删除的大文件;要把空间要回来,要么优雅地重启那个进程让它松手,要么在不重启的情况下,通过 /proc 找到那个文件句柄、把它 truncate 清空。这个谜题也顺带教给我一条铁律:永远不要用 rm 去清理一个正在被写入的日志文件,你以为你删掉了它、腾出了空间,实际上空间一点没还给你,日志还在那个看不见的句柄里继续膨胀;清空一个在用的日志,正确的姿势是 truncate,它清掉内容、但保留文件本身和进程的句柄,空间会立刻、真实地回收。把眼前的空间抢救回来之后,我没有就此收手,因为我很清楚,那个一直写到 39G 的日志文件,它的存在本身才是这次事故真正的病根——它从服务上线那天起就一个文件写到底,从来没有被切割过,没人管它。治本的办法是 logrotate,让日志自动地按天或按大小轮转、把旧的压缩、把过老的删除。配置 logrotate 时有个细节我也是这次才真正理解:轮转日志有两种思路,默认的那种是把旧日志改名、再建一个新文件,但这样进程的句柄还指着那个被改了名的旧文件,新文件会一直是空的,所以这种方式必须配合给进程发信号让它重新打开日志;而 copytruncate 这种方式是先把日志拷贝出去再把原文件清空,进程的句柄始终不变——它本质上就是前面 truncate 那一招的自动化版本,在拿不准的时候用它最省心。这一夜让我最终想明白的一件事是:磁盘满,从来都不是一种突然爆发的急症,它是一种彻头彻尾的慢性病,是日志一天一天、一点一点地涨上来的,这意味着它百分之百是可以被提前发现的。我们之所以会在深夜里被它打个措手不及,不是因为它来得多突然,而是因为我们压根没有在它身上装一双"眼睛"。所以这次复盘的最后一步,我给所有服务器都加上了磁盘的预警——既盯空间也盯 inode,更重要的是盯它的增长趋势,趋势远比某一个时刻的数值有价值,因为它能让你提前三天就知道"这块盘快撑不住了",从而把一场需要在深夜里手忙脚乱救火的事故,变成一件白天从容处理的日常运维。从被动地等它满,到主动地看着它涨,这就是这一夜磁盘排查留给我的、最该记住的东西。

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

环形缓冲区 RingBuffer · 原理详解 完全指南:速查、踩坑与最佳实践

2026-5-19 0:28:20

Linux教程

服务器突然变慢:一次 Linux 性能排查的复盘

2026-5-20 17:17:38

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