1. 问题本质不是命令错了是文件系统在“说谎”你执行df -h看到根分区用了 92%但用du -sh /却只算出 68GB 占用——这差的 20GB 去哪了别急着删日志、清缓存、重启服务。这不是磁盘空间“消失”了而是 Linux 文件系统底层的一场“信任危机”df和du根本不是在看同一个东西它们的统计逻辑天然是错位的。dfdisk free读取的是文件系统超级块superblock里的元数据——它不扫描文件只查 inode 和 block 的分配状态。只要内核标记某块磁盘已被分配df就算它被占用了不管这块空间里实际有没有文件、文件是否还被进程打开、甚至文件是否已被删除但句柄未释放。而dudisk usage干的是体力活它从指定路径出发逐个遍历目录树对每个文件调用stat()系统调用累加st_blocks * 512字节数。它只认“看得见、摸得着”的文件实体对已删除但仍在内存中被进程持有的文件即“幽灵文件”du完全无视。所以当两者结果不一致时核心矛盾从来不是“哪个更准”而是“谁在反映当前真实可回收空间”。df告诉你系统认为这块磁盘已被占用du告诉你用户态可见的文件总共占了多少。差值 df认为已用但du找不到的那部分空间 —— 这正是我们排查的黄金靶区。我第一次遇到这个问题是在一台跑 Kafka 的生产服务器上df显示/var/log98% 满du却只显示 32GB。运维同事直接rm -rf /var/log/kafka/*结果df一点没变。后来发现是 Kafka 进程正以O_APPEND方式持续写入一个已被logrotaterename 并unlink的旧日志文件那个文件的 inode 还活着磁盘块没被释放。du找不到它路径没了df却牢牢记着它占的块。这种场景在数据库、消息队列、Web 服务器里极其常见不是 bug是 Unix 设计哲学的必然结果。关键词df、du、linux不是命令速查标签而是诊断文件系统健康状态的三把钥匙。真正要盯住的是那个差值背后隐藏的进程、inode 和挂载状态。下面我们就一层层剥开这个“空间幻觉”的外壳。2. 四大根源深度拆解为什么差值永远存在差值不是偶然误差而是由四类明确、可验证的系统行为导致。每一类都有其独特的触发条件、检测方法和清除路径。我按发生频率和危害程度排序把最常踩的坑放在前面。2.1 已删除但未关闭的文件Deleted but still open这是生产环境里占比超 70% 的原因。当一个文件被unlink()如rm命令后只要还有进程通过文件描述符fd持有它内核就不会真正释放其数据块和 inode。该文件在文件系统中“消失”但在/proc/pid/fd/下依然可见且df继续将其计入已用空间。原理还原Linux 的文件删除是引用计数机制。unlink()只是减少目录项dentry对 inode 的链接数link count。只有当 link count 降为 0且所有打开该文件的 fd 都被close()后内核才回收 inode 和 data blocks。du只统计 link count 0 的文件df则统计所有被 inode 引用的 blocks。实操验证# 查找所有被删除但仍被打开的文件需 root 权限 lsof L1 | head -20 # 输出示例 # COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME # java 1234 app 23u REG 253,0 2147483648 123456 /var/log/app.log (deleted)L1参数专为捕获 link count0 的文件设计。NAME列末尾的(deleted)是铁证。SIZE/OFF显示该文件实际占用的字节数直接对应df和du的差值。提示lsof L1在某些精简版系统如 Alpine可能不可用。此时可用find /proc/*/fd -ls 2/dev/null | grep deleted替代但输出更杂乱需配合awk过滤。2.2 挂载点覆盖Mount point overlay当你在非空目录上执行mount比如mount /dev/sdb1 /data而/data目录下原本就有文件那么这些原文件就被新挂载的文件系统“遮住”了。du会进入/data目录并统计新挂载的文件但df显示的是/dev/sdb1的空间使用与/data下原文件无关。然而如果umount /data后再du /data就能看到被覆盖的旧文件——它们一直占着/分区的空间只是被“隐身”了。典型场景管理员误操作在/home下挂载了新磁盘但没清空原/home目录Docker 容器启动时自动挂载/var/lib/docker/overlay2若宿主机/var/lib/docker下有残留文件也会被覆盖。检测方法# 查看所有挂载点及其源设备 findmnt -D # 或检查特定目录是否被覆盖 mount | grep $(dirname /path/to/dir) # 更直接卸载后检查谨慎生产环境慎用 umount /path/to/mount du -sh /path/to/mount mount /path/to/mount注意umount操作有风险必须确保无进程正在访问该挂载点。lsof D /path/to/mount可提前检查。2.3 文件系统元数据损坏Filesystem metadata corruptiondf依赖 superblock 中的free blocks和used blocks计数。如果因异常断电、硬件故障或内核 bug 导致 superblock 数据不一致df的统计就会失真。此时fsck是唯一救星但它不能在线修复ext4/xfs 需umount且修复过程可能耗时数小时。触发信号df和du差值巨大如差 50GB且lsof L1和挂载检查均无异常dmesg | grep -i ext4\|xfs\|error出现文件系统错误日志tune2fs -l /dev/sdXN显示Filesystem state: not clean。安全验证# 检查文件系统状态ext4 tune2fs -l /dev/sdXN | grep -E (Filesystem state|Last mounted on) # 检查 xfs 状态 xfs_info /mount/point # 查看内核错误日志 dmesg -T | grep -i buffer I/O error\|ext4\|xfs2.4 内核缓冲区与延迟写入Kernel buffer and delayed writedf统计的是文件系统层面的块分配而du统计的是用户态可见的文件大小。当大量小文件被write()写入内核会先缓存在 page cache 中待时机成熟如sync调用、脏页比例超阈值再批量刷盘。此时df已将块标记为“已分配”但du因文件尚未落盘可能无法准确统计其大小尤其对稀疏文件或未fsync()的文件。影响程度通常差值在几 MB 到几百 MB不会造成 GB 级偏差。但它是df和du天然存在的微小鸿沟是 Linux I/O 栈设计的必然代价。验证方式# 强制同步所有脏页慎用I/O 峰值 sync # 查看当前脏页状态 cat /proc/meminfo | grep -i dirty\|writeback # 观察 sync 前后 df/du 差值变化这四类原因不是并列关系而是有明确的排查优先级先查lsof L1秒级定位再看挂载覆盖分钟级确认最后才考虑fsck小时级风险操作。把fsck当第一招就像感冒先做开颅手术——方向性错误。3. 实战排查流程从定位到清理的完整闭环我总结了一套经过 200 台服务器验证的标准化排查流程。它不依赖任何第三方工具全部使用 Linux 内置命令每一步都有明确的预期输出和决策分支。记住目标不是让df和du数值相等而是让df显示的“已用空间”真实反映可回收资源。3.1 第一步精准量化差值并锁定分区不要凭感觉。先用一行命令抓出最可疑的分区# 获取所有挂载点的 df 和 du 差值单位MB按差值降序 awk NR1{next} {if($5~/^[0-9]%$/){gsub(/%/,,$5); print $5,$1}} (df -P) (du -xsm /* 2/dev/null | sort -k2) | \ awk {diff$1-$2; if(diff100) printf %-10s %6d MB diff\n, $3, diff} | sort -k2nr这个命令做了三件事df -P输出 POSIX 格式避免-h的单位干扰du -xsm /*以 MB 为单位-x跳过其他挂载点2/dev/null屏蔽权限错误awk计算差值只显示差值 100MB 的分区。输出示例/dev/sda1 2145 MB diff /dev/sdb1 320 MB diff立刻聚焦/dev/sda1通常是/。这是后续所有操作的起点。3.2 第二步直击核心——查找 deleted 文件在锁定的分区上执行# 查找所有被删除但仍被打开的文件并按大小排序 lsof L1 / | awk $NF ~ /\(deleted\)$/ {gsub(/[^0-9]/,,$7); sum[$1]$7; count[$1]} END {for (i in sum) print sum[i] MB\tcount[i] files\t i} | sort -nr关键解读$NF ~ /\(deleted\)$/精确匹配(deleted)结尾$7是文件大小列lsof默认输出格式sum[$1]按进程名$1累加大小count[$1]统计每个进程打开的 deleted 文件数。输出示例1824 MB 12 files java 210 MB 3 files nginx这说明java进程占了 1.8GB “幽灵空间”。下一步就是找到它的 PID 并决定如何处理。3.3 第三步进程级处置——重启 or 优雅关闭绝对禁止kill -9 PID。粗暴杀死进程可能导致数据丢失或服务中断。正确姿势分两步1. 定位具体文件和 fd# 查看 java 进程打开的所有 deleted 文件详情 lsof -p $(pgrep -f java.*kafka) | grep (deleted) # 输出示例 # java 1234 app 23u REG 253,0 2147483648 123456 /var/log/kafka/server.log.2023-09-01 (deleted)23u表示文件描述符 23REG是普通文件2147483648是字节数2GB。2. 选择处置方案方案 A推荐应用自身支持日志滚动。如 Kafka 有log4j配置可发送SIGUSR1信号触发日志重载kill -USR1 1234。这会让进程close()旧 fd 并open()新文件空间立即释放。方案 B重启服务。systemctl restart kafka-server。这是最稳妥的方式但有服务中断窗口。方案 C仅调试强制释放 fd。echo 23 /proc/1234/fd/无效/proc/pid/fd/是只读。Linux 不允许用户态强制关闭其他进程的 fd这是内核安全边界。实操心得我在一家电商公司处理过一个 MySQL 的 deleted 文件问题。lsof L1显示 mysqld 占了 15GB但kill -USR1对 MySQL 无效。最终发现是 binlog 日志被rm后未flush logs。解决方案是mysql -e FLUSH LOGS;MySQL 主动关闭旧 binlog fd 并创建新文件空间秒级释放。3.4 第四步挂载覆盖验证与清理如果lsof L1无结果立即转向挂载检查# 列出所有挂载点并标注其父目录是否为空 findmnt -D | awk {print $1,$2,$3,$4} | while read src target fstype opts; do if [ $target ! / ] [ -d $target ]; then count$(ls -A $target 2/dev/null | wc -l) echo $src - $target ($count items hidden) fi done | grep 0 items此脚本找出所有挂载点并统计其目标目录下的文件数。若输出0 items说明该挂载点很可能覆盖了非空目录。清理步骤umount /target确保无进程使用ls -la /target查看被覆盖的文件mv /target/* /backup/迁移旧文件mount /target重新挂载。注意/var/lib/docker是高危区。Docker daemon 运行时/var/lib/docker下的文件会被 overlay2 挂载覆盖。若此处有残留du /var/lib/docker会远小于df /var/lib/docker。清理前务必systemctl stop docker。3.5 第五步元数据修复——fsck 的正确打开方式当以上步骤均无效且dmesg有文件系统错误时fsck是最后手段。但必须严格遵循流程1. 确认文件系统类型blkid /dev/sda1 # 输出/dev/sda1: UUID... TYPEext42. 卸载分区关键# 检查是否被占用 lsof /dev/sda1 # 若有找出进程并停止服务 # 然后卸载 umount /dev/sda13. 执行 fsckext4 示例# 先试运行不修改 e2fsck -n /dev/sda1 # 若报告错误再执行修复 e2fsck -y /dev/sda1-n参数是安全阀它只读检查不写盘。-y表示自动确认所有修复。切勿跳过-n步骤。我见过因跳过此步fsck错误修复 superblock导致整个分区无法挂载的事故。4. 修复后验证# 重新挂载 mount /dev/sda1 / # 再次对比 df/du df -h / du -shx / | head -1整个流程下来95% 的df/du不一致问题都能定位并解决。剩下的 5%往往是硬件故障坏道或内核级 bug需要更深入的诊断。4. 预防性监控与自动化脚本让问题在爆发前消失被动排查是救火主动预防才是运维的终极目标。我把这套经验固化成了两个脚本部署在所有线上服务器上每天凌晨自动运行并告警。4.1 差值监控脚本df_du_monitor.sh#!/bin/bash # 阈值差值超过 500MB 或使用率超 90% 且差值 100MB THRESHOLD_MB500 CRITICAL_USAGE90 # 获取根分区信息 ROOT_DEV$(df / | tail -1 | awk {print $1}) ROOT_USAGE$(df / | tail -1 | awk {print $5} | sed s/%//) ROOT_DF$(df -B1 / | tail -1 | awk {print $3}) # bytes ROOT_DU$(du -sx --exclude/proc --exclude/sys --exclude/dev / 2/dev/null | awk {print $1}) DIFF$((ROOT_DF - ROOT_DU)) DIFF_MB$((DIFF / 1024 / 1024)) # 判断告警级别 if [ $DIFF_MB -gt $THRESHOLD_MB ] || ([ $ROOT_USAGE -gt $CRITICAL_USAGE ] [ $DIFF_MB -gt 100 ]); then echo ALERT: df/du diff $DIFF_MB MB on $(hostname) - $(date) echo df: $(numfmt --toiec-i $ROOT_DF), du: $(numfmt --toiec-i $ROOT_DU) # 输出 top 3 deleted files lsof L1 / 2/dev/null | head -5 | awk {print $1,$2,$9,$7} | column -t exit 1 else echo OK: diff $DIFF_MB MB - $(date) exit 0 fi部署方式chmod x df_du_monitor.sh加入 crontab0 3 * * * /path/to/df_du_monitor.sh /var/log/df_du.log 21配合 Zabbix/Prometheus 抓取 exit code实现可视化告警。4.2 自动化清理脚本auto_cleanup_deleted.sh针对已知的“日志狂魔”进程如 Java 应用、Nginx我们预设了信号触发规则#!/bin/bash # 自动向指定进程发送日志重载信号 declare -A PROC_SIGNALS( [java]USR1 [nginx]USR1 [rsyslogd]HUP ) for proc in ${!PROC_SIGNALS[]}; do pids$(pgrep $proc) if [ -n $pids ]; then for pid in $pids; do # 检查该进程是否有 deleted 文件 if lsof -p $pid 2/dev/null | grep -q (deleted); then echo Sending ${PROC_SIGNALS[$proc]} to $proc (PID: $pid) kill -${PROC_SIGNALS[$proc]} $pid 2/dev/null sleep 2 fi done fi done安全机制只对预设进程列表生效避免误杀发送信号前必查lsof -p无 deleted 文件则跳过每次只处理一个 PID防止并发冲突。4.3 日志轮转强化配置logrotate 最佳实践logrotate是预防 deleted 文件的源头。默认配置常有缺陷我做了三项加固1. 强制 postrotate 脚本/etc/logrotate.d/myapp/var/log/myapp/*.log { daily missingok rotate 30 compress delaycompress notifempty create 0644 app app sharedscripts postrotate # 关键确保应用收到通知 if [ -f /var/run/myapp.pid ]; then kill -USR1 $(cat /var/run/myapp.pid) 2/dev/null fi # 备用若 USR1 无效尝试 reload systemctl reload myapp 2/dev/null endscript }2. 设置 maxsize 限制size 100M替代daily避免单个日志文件过大。3. 使用 copytruncate 模式copytruncate会在cp后truncate原文件而非mv。这样即使应用不支持信号重载也能保证文件句柄不丢失空间及时释放。这些脚本和配置不是锦上添花而是把“救火队员”变成“防火员”的关键。上线三个月后我们团队的df/du告警下降了 92%。5. 常见问题与避坑指南那些文档里不会写的细节在上百次实战中我整理了最易被忽略、却最致命的细节。它们往往让一个本该 5 分钟解决的问题拖成 5 小时。5.1 为什么lsof L1什么都没输出但差值还在真相lsof L1只能捕获 link count0 的文件但有些情况 link count 0 却du统计不到。典型是硬链接断裂。例如ln /bigfile /tmp/link然后rm /bigfile但/tmp/link还在。此时 link count1lsof不报但du /tmp会漏掉这个链接指向的块。验证# 查找所有 link count 1 的大文件 find / -xdev -type f -links 1 -size 100M -printf %n %s %p\n 2/dev/null | sort -nr | head -5 # 输出2 1073741824 /tmp/link%n是 link count。若发现 link count 1 的大文件且其原始路径已不存在就可能是它。5.2du -sh /和du -shx /结果为什么不同-x参数--one-file-system是关键。没有-xdu会递归进入所有子挂载点如/proc,/sys,/dev而这些是虚拟文件系统du统计的是它们的“伪大小”导致结果虚高。-x限定只统计同一文件系统上的文件这才是和df对比的正确姿势。教训我曾在一个容器环境中du /显示 80GBdf /是 40GB。加-x后du -shx /立刻变成 38GB。差值来自/proc下的docker目录du把它当真实文件算了。5.3fsck修复后df空间反而更少了这是fsck的正常行为。fsck会清理 orphaned inodes孤儿 inode——那些 inode 存在但无目录项指向的文件。修复时fsck将这些 inode 放入/lostfound并标记其 blocks 为“空闲”。df的 free blocks 数因此增加已用空间减少。这不是错误是文件系统自我净化。验证修复后ls -la /lostfound会看到大量#inode_number文件这就是被救回的孤儿文件。5.4 Docker 环境下的特殊陷阱Docker 的存储驱动如 overlay2让df/du问题更复杂。df /var/lib/docker显示的是 host 的空间但du /var/lib/docker统计的是 overlay2 的 metadata 和 layer不包括容器运行时的 writable layer它在/var/lib/docker/overlay2/id/diff下。正确统计# 统计 Docker 实际占用含所有 layer 和 container docker system df -v # 或手动计算 du -sh /var/lib/docker/overlay2/*/diff 2/dev/null | sort -hr | head -55.5 云服务器上的“幽灵空间”快照与镜像在 AWS EC2 或阿里云 ECS 上df显示的空间包含 EBS 卷的全部容量但du只统计文件系统内的数据。如果卷做过快照或启用了“精简配置”thin provisioningdf可能包含未实际分配的预留空间。此时df/du差值是正常的无需处理。判断依据# 检查是否为精简配置卷AWS lsblk -o NAME,TYPE,FSTYPE,SIZE,MOUNTPOINT,ROTA,DISC-MAX /dev/xvda # DISC-MAX 列非 0 表示支持 discard差值可能与此相关这些细节没有一本 Linux 书会专门讲但它们真实地存在于每一次深夜告警电话里。记住df和du的不一致不是系统的缺陷而是 Unix 哲学的具象体现——它把“空间管理”的控制权交给了进程、内核和管理员三方的协作。理解这一点你就不再焦虑数字而开始思考背后的系统行为。我在生产环境里坚持一个原则df是资源水位尺du是资产清单表。水位尺告诉你该不该开闸放水扩容/清理资产清单告诉你水从哪来、往哪去。两者互补而非互斥。下次再看到那个刺眼的差值别慌打开终端按这个流程走一遍。你会发现所谓“最新解决方案”不过是把老手艺练得更熟一点。