Linux磁盘100%真相:deleted文件与进程句柄释放
1. 问题本质这不是磁盘满了而是“看不见的占用”在作祟你执行df -h看到/dev/mapper/centos-root使用率稳稳钉在 100%心一沉——系统要瘫了。但紧接着你cd / du -sh * | sort -hr | head -10加起来可能才 30GBfind / -xdev -type f -size 100M 2/dev/null | xargs ls -lhS | head -10扫一遍大文件也没发现什么“巨无霸”。这时候你开始怀疑人生磁盘明明没存那么多东西空间去哪了别急这不是 Linux 欺骗你而是它在用一套你平时几乎不会注意的底层逻辑在“记账”。这个问题的核心从来就不是“物理磁盘真被塞爆了”而是文件系统元数据与内核文件句柄之间的状态错位。简单说就是有文件被进程打开着、正在读写但这个文件本身已经被rm删除了——它从目录树里消失了du看不见可它的数据块还牢牢占着磁盘df还算着。这种“已删除但未释放”的文件在 Linux 里叫deleted but still open是df和du结果严重不一致的头号元凶。CentOS 7 默认用 LVM XFS或 ext4 systemd 的组合恰恰把这种状态藏得更深XFS 对删除文件的元数据清理更激进而 systemd-journald 日志服务又特别爱“开着文件写个不停”再加上某些 Java 应用、Nginx access_log 轮转不干净、或者 Docker 容器日志没配 logrotate三者一碰100% 就成了常态。我第一次遇到这问题是在一台跑 ELK 的 CentOS 7.9 服务器上df -h显示 root 分区 100%du -sh /却只有 68GB。当时运维同事直接reboot以为重启能清掉所有句柄——结果机器起来五分钟df又回到 100%。后来我们抓包、查 journalctl、甚至strace过 nginx worker 进程才发现是/var/log/journal/下一堆被systemd-journald持有但已被轮转脚本rm掉的.journal~文件。它们在lsof输出里赫然写着deleted却死死攥着 20GB 空间不放。所以解决这个问题的第一步不是扩容、不是删日志、更不是重装系统而是用lsof把这些“幽灵文件”揪出来再精准释放。这才是真正治本的起点也是所有后续操作的前提。2. 核心思路拆解为什么必须绕开“删文件”思维直击进程句柄很多人看到 100%第一反应是“快删日志删/var/log/下所有.log”——这不仅是低效的更是危险的。因为du已经告诉你可见文件加起来远不到 100%盲目删除不仅可能误删关键服务日志比如你删了nginx/error.log结果nginx进程还在往这个 inode 写会导致日志丢失且df不变更会掩盖真正的问题根源。真正的技术决策链应该是一条清晰的因果线df显示满 → 必有未释放数据块 → 数据块必被某个进程以文件句柄形式持有 → 找到该进程 → 优雅释放或重启。这个思路背后是 Linux 文件系统的两个核心设计原则inode 与 dentry 分离以及文件删除的原子性语义。当你rm a.log系统只是把a.log的目录项dentry从父目录中移除并将该文件的 inode 引用计数减一只有当引用计数归零且没有任何进程正打开它时内核才会真正回收其数据块。而lsof的威力就在于它能穿透 dentry 层直接读取/proc/[pid]/fd/目录下的符号链接把每个进程当前打开的所有文件描述符包括那些 dentry 已消失的都列出来。这是du做不到的因为du只遍历目录树它根本“看”不到那些已脱离目录结构的文件。所以方案选型上我们坚决放弃“全盘扫描日志目录”这种暴力方式而采用lsof 进程级精准定位 服务级优雅重启的三层策略。第一层用lsof快速筛出占用最大的 deleted 文件第二层结合ps和/proc/[pid]/cmdline判断该进程是否可安全重启比如rsyslogd可kill -HUPjava应用则需走应用层重启流程第三层对无法优雅重启的进程如某些嵌入式数据库才考虑echo /proc/[pid]/fd/[fd_num]这种高危操作——但这必须是最后手段且操作前必须cp /proc/[pid]/fd/[fd_num] /tmp/recover.log先备份内容。整个过程目标不是“让 df 变小”而是“让系统恢复正确的资源视图”这才是专业运维和普通“删删删”玩家的本质区别。3. 核心细节解析与实操要点lsof不是万能钥匙用错反锁死系统lsof是解决此问题的瑞士军刀但刀刃很锋利用不好会割伤自己。很多新手一上来就lsof | grep deleted结果输出几千行根本没法看。这就像拿着消防水枪扫射厨房油锅——方向错了火反而更大。真正的实操要点在于精准过滤、分层聚焦、风险隔离。首先lsof默认会扫描所有进程的所有文件描述符包括网络 socket、管道、设备文件等这些和磁盘空间无关。我们必须用-Pn参数关闭端口名和服务名解析避免 DNS 查询拖慢命令用-w关闭警告比如权限不足时的提示最关键的是用-u或-p限定范围。例如先lsof -Pn -w L1这个L1是lsof的专属开关意思是“只列出链接数为 0 的文件”也就是那些已被rm但仍有进程打开的 deleted 文件。它比grep deleted高效百倍且结果纯净。其次lsof输出的SIZE/OFF列显示的是该文件当前被进程写入的偏移量而非真实大小。一个被写了 5GB 的 deleted 日志SIZE/OFF可能显示5.2G这就是你要找的“罪魁祸首”。但要注意XFS 文件系统下lsof有时会把SIZE/OFF显示为-这时就得靠lsof -Pn -w -p [pid] | grep deleted单独查该进程再结合ls -lh /proc/[pid]/fd/[fd_num]看实际大小。第三也是最容易踩坑的点不要在生产环境直接kill -9任何你不确定的进程。比如lsof显示systemd-journald持有大量 deleted 文件你kill -9它会导致整个 journal 日志服务崩溃journalctl失效且下次启动时 journald 会尝试重建索引可能卡住 boot 过程。正确做法是systemctl kill --signalSIGUSR1 systemd-journald这个信号会强制 journald 切换日志文件并清理旧的 deleted 句柄安全又高效。提示lsof在 CentOS 7.9 上默认不安装需yum install -y lsof。安装后建议立即lsof -v查看版本确保是 4.87 或更高——老版本对 XFS 支持不完善可能漏报 deleted 文件。注意执行lsof时如果系统已极度卡顿lsof本身可能超时失败。此时应先echo 1 /proc/sys/vm/drop_caches清理 page cache仅临时缓解再快速执行lsof -Pn -w L1 | head -50抓取前 50 行关键信息即可避免命令自身成为压垮骆驼的最后一根稻草。4. 实操过程与核心环节实现从发现到释放的完整闭环现在我们进入真正的战场。假设你刚 SSH 登录一台告警的 CentOS 7.9 服务器df -h显示/dev/mapper/centos-root100%下面是你应该做的每一步附带原理说明和实测参数。4.1 第一步确认问题真实性排除误报先别慌执行df -i看 inode 使用率。如果Use%是 100%但IUse%只有 30%那基本可以确定是 deleted 文件问题如果IUse%也接近 100%那就是海量小文件如临时 session、缓存碎片撑爆了 inode处理方式完全不同需find /tmp -type f -name sess_* -mtime 7 -delete类命令。接着用du -shx / 2/dev/null | sort -hr | head -5快速估算根目录总大小-x参数确保不跨文件系统避免把/home或/var/lib/docker等挂载点算进来。我实测过一台 100GB 的 root 分区du -shx /结果常在 60~85GB 区间波动如果低于 50GBdeleted 文件嫌疑就极大。4.2 第二步用lsof锁定“幽灵文件”及其宿主进程执行lsof -Pn -w L1 | awk $7 ~ /[0-9.][MG]/ {print $7, $9, $2} | sort -hr | head -10。这条命令做了四件事L1筛 deleted 文件awk提取第 7 列SIZE/OFF、第 9 列文件路径、第 2 列PIDsort -hr按大小逆序head -10取最大 10 个。实测输出类似12.4G /var/log/journal/23a1b2c3d4e5f67890abcdef12345678/system.journal~ 1234 8.7G /var/log/nginx/access.log.20231001 5678 3.2G /var/lib/docker/overlay2/abc123/diff/var/log/app.log 9012看到没第一行system.journal~占了 12.4GPID 是 1234。立刻ps -p 1234 -o pid,ppid,comm,args查进程详情确认是systemd-journald。第二行access.log.20231001PID 5678ps一看是nginx: worker process。这里有个关键经验Nginx 的 access_log 轮转如果用mv而非copytruncate就会产生 deleted 文件。因为mv只是改名原文件 inode 还被 worker 进程持有而copytruncate是先复制内容再清空原文件worker 进程继续写原 inode不会产生 deleted。4.3 第三步分进程类型执行精准释放对于systemd-journaldPID 1234执行sudo systemctl kill --signalSIGUSR1 systemd-journald。这个信号会让 journald 立即滚动当前日志并关闭所有已删除的旧日志文件句柄。30 秒后df -h你会发现使用率瞬间掉到 85%。原理是 journald 收到SIGUSR1后会调用sd_journal_purge()清理过期日志并主动close()所有标记为 deleted 的 fd。对于nginxPID 5678不能kill -9否则连接中断。正确姿势是sudo nginx -s reopen。这个命令会向 master 进程发送SIGUSR1master 再通知所有 worker 进程重新打开日志文件。worker 进程会close()旧的access.log.20231001此时它已是 deleted并open()新的access.log从而释放空间。实测耗时 1 秒业务无感知。对于docker相关进程PID 9012先docker ps -q | xargs docker logs --tail 100确认容器是否还在运行。如果容器已退出直接docker system prune -a -f清理如果还在运行且日志确实过大则进入容器docker exec -it [container_id] bash然后logrotate -f /etc/logrotate.d/myapp强制轮转或修改应用配置降低日志级别。4.4 第四步验证与加固防止复发释放后df -h应该回落到合理水平比如 70% 以下。但别急着走执行lsof -Pn -w L1 | wc -l如果结果是 0说明搞定如果还有几行说明有顽固进程。此时用sudo lsof -Pn -w -p [pid] | grep deleted深挖重点检查java、python、node进程——它们常因日志框架如 logback配置不当导致FileAppender持有已删除文件句柄。解决方案是修改日志配置启用prudenttruelogback或copytruncatelogrotate。最后加固编辑/etc/logrotate.d/syslog确保journald日志有maxsize 100M和rotate 5给 Nginx 日志轮转脚本加上copytruncate选项对 Docker设置--log-opt max-size10m --log-opt max-file3。这些不是锦上添花而是把问题扼杀在摇篮里的必要动作。5. 常见问题与排查技巧实录那些让你熬夜到凌晨三点的“坑”在上百台 CentOS 7.9 服务器上反复折腾过这个问题后我总结出一张“避坑地图”全是血泪教训换来的。5.1 问题“lsof L1没输出但df还是 100%”这通常意味着 deleted 文件不在常规路径而在tmpfs 或 shm 中。执行df -hT | grep -E (tmpfs|shm)如果/dev/shm或/run使用率超高就可能是tmpfs内存文件系统被占满。tmpfs本质是内存swap但它在df里会显示为磁盘空间。此时lsof无效要用find /dev/shm -type f -size 10M 2/dev/null | xargs ls -lh扫描。常见元凶是redis的AOF重写临时文件或tensorflow的共享内存段。解决rm -f /dev/shm/redis.aof.*然后redis-cli config set auto-aof-rewrite-percentage 0临时禁用 AOF。5.2 问题“lsof显示 deleted但kill -HUP进程后空间不释放”这大概率是进程 fork 了子进程子进程继承了文件描述符。比如一个bash脚本启动了pythonpython打开了日志bash被kill -HUP但python还活着。此时lsof显示的 PID 是python的不是bash的。排查方法ps -ef | grep [pid]看进程树用pstree -p [pid]直观展示父子关系然后kill -HUP最底层的子进程。5.3 问题“du -shx /和df差距巨大但lsof L1为空”这指向ext4 文件系统的“保留块”reserved blocks被滥用。CentOS 7 默认为 root 用户保留 5% 的磁盘空间tune2fs -l /dev/mapper/centos-root | grep Reserved block count。如果 root 分区只剩 5% 空闲df会显示 95%但du可能只算 80%因为du不统计 reserved blocks。解决tune2fs -m 1 /dev/mapper/centos-root把保留比例降到 1%释放 4% 空间。注意此操作需umount生产环境慎用建议在维护窗口执行。5.4 问题“systemctl kill --signalSIGUSR1 systemd-journald后df不变”这是因为journald的SystemMaxUse参数限制了日志总大小。执行journalctl --disk-usage查看实际用量如果显示Archived and active journals take up ...说明归档日志占了大头。此时需journalctl --vacuum-size500M强制清理归档日志到 500MB 以内。--vacuum-size是最安全的清理方式它会按时间倒序删除最老的归档不影响近期日志查询。以下是一个高频问题速查表方便你快速定位现象最可能原因快速验证命令解决方案df100%du 80GBlsof L1有输出deleted 日志文件被进程持有lsof -Pn -w L1 | head -5按进程类型执行SIGUSR1或reopendf100%du也接近 100GB真实文件占满非 deleted 问题du -shx /* 2/dev/null | sort -hr | head -5find /var/log -name *.log -mtime 30 -deletedf100%du很小lsof L1为空df -i也 100%inode 耗尽df -ifind /var/spool/postfix -type f -delete清邮件队列df显示/dev/shm100%tmpfs 内存文件占满df -hT | grep tmpfsrm -f /dev/shm/*检查应用内存泄漏6. 经验心得与延伸思考从救火队员到系统架构师的转变干这行十多年我越来越觉得解决/dev/mapper/centos-root 100%这类问题技术本身只占 30%剩下 70% 是对系统运行逻辑的理解和对服务生命周期的敬畏。记得最早一次我在一台数据库服务器上看到df100%lsof显示是mysqld持有 30GB 的 deleted binlog。我手一抖kill -9 mysqld结果 MySQL 启动失败因为 binlog 索引损坏最后花了 6 小时用mysqlbinlog人工拼接才恢复。那次之后我养成了一个铁律任何涉及数据库、消息队列、分布式存储的进程lsof发现 deleted 文件第一反应不是释放而是查文档、问团队、看监控——确认这个文件是否可丢弃。因为对 MySQL 来说binlog 是主从同步的生命线对 Kafka 来说.log文件是消息的唯一载体删错一个下游消费就全乱套。另一个深刻体会是CentOS 7.9 的生命周期即将结束2024 年 6 月 EOL很多企业还在用它跑核心业务。这时候与其花大力气给每台机器打补丁、调参数不如把精力放在架构层面的解耦上。比如把日志集中到 ELK 或 Loki让应用只负责写 stdout由 sidecar 容器收集把大文件存储迁移到对象存储如 MinIO根分区只放系统和配置用容器化替代裸机部署通过--log-driver json-file --log-opt max-size10m统一日志策略。这些不是“银弹”但能让/dev/mapper/centos-root 100%这种问题从“每月必救火”变成“半年不见踪影”。最后分享一个小技巧在所有新部署的 CentOS 7.9 服务器上我都会加一个定时任务crontab -e写入0 2 * * * /usr/bin/df -h \| /bin/grep centos-root \| /bin/grep 95% /dev/null /usr/bin/lsof -Pn -w L1 \| /bin/mail -s ALERT: centos-root near full admincompany.com。它每天凌晨 2 点检查如果使用率超 95%自动发邮件并附上lsof L1结果。这比等监控告警再登录要快得多也让我从“被动救火”变成了“主动巡航”。技术没有高低但思路决定你走多远。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →