一、问题背景
在 Linux 运维中,经常遇到这样的场景:磁盘空间告警触发, df -h 显示某个分区使用率已达 90% 以上,但用 du -sh /* 统计各目录占用空间时,加起来的总和远小于 df 显示的已用空间。更诡异的是,明明删除了几十 GB 的日志文件, df 显示的可用空间却没有增加。
这种现象在生产环境中非常常见,尤其是以下场景:
- 应用日志持续写入,日志文件被删除后空间未释放
- 数据库 binlog、redo log 被删除,但空间仍然占用
- 容器或进程 crash 后留下僵尸文件描述符
- 文件系统元数据占用大量空间
- 挂载点覆盖导致底层文件不可见
如果不理解这些现象背后的原理,很容易做出错误的判断和操作,比如:
- 误以为是磁盘硬件故障,申请更换硬盘
- 反复删除文件但空间不释放,最终只能重启服务
- 强制 umount 或 reboot,导致数据丢失或服务中断
本文从文件系统原理出发,系统梳理 du 与 df 不一致的常见原因、排查方法、解决方案和预防措施。
二、适用场景
本文适用于以下运维场景:
- 磁盘空间告警,但找不到占用空间的大文件
- 删除文件后,df 显示的可用空间未增加
- du 统计的空间总和小于 df 显示的已用空间
- 应用日志、数据库日志被删除,但空间未释放
- 需要理解 Linux 文件系统的 inode、文件描述符、挂载点机制
本文的排查方法和命令适用于常见 Linux 发行版 (CentOS、Ubuntu、Debian) 和常见文件系统 (ext4、xfs)。
三、核心知识点
3.1 du 和 df 的区别
du (Disk Usage):
- 统计文件和目录占用的磁盘空间
- 通过遍历目录树,读取每个文件的大小 (st_size)
- 只统计当前可见的文件
- 单位:KB、MB、GB
df (Disk Free):
- 统计文件系统的整体使用情况
- 通过读取超级块 (superblock) 获取文件系统的总大小、已用大小、可用大小
- 包括所有已分配的 block,无论文件是否可见
- 单位:KB、MB、GB,也显示 inode 使用情况
关键区别:
| 维度 |
du |
df |
| 数据来源 |
遍历目录树,统计文件大小 |
读取文件系统超级块 |
| 统计范围 |
当前可见的文件 |
所有已分配的 block |
| 是否包括已删除但未释放的文件 |
否 |
是 |
| 是否包括文件系统元数据 |
否 |
是 |
| 性能 |
慢 (需要遍历目录) |
快 (读取超级块) |
3.2 Linux 文件删除机制
Linux 文件系统使用 inode 存储文件元数据,文件内容存储在 data block 中。
文件删除的两个条件:
- 硬链接计数为 0 :没有目录项指向该 inode
- 文件描述符计数为 0 :没有进程打开该文件
只有两个条件同时满足,文件系统才会真正释放 inode 和 data block。
典型场景:
# 进程A打开文件
exec 3< /var/log/app.log
# 进程B删除文件
rm /var/log/app.log
# 此时du不统计该文件(目录项已删除)
# 但df仍然统计该文件(进程A持有文件描述符)
只有进程 A 关闭文件描述符 (或进程退出),空间才会真正释放。
3.3 文件系统元数据开销
文件系统需要额外的空间存储元数据:
ext4 元数据:
- 超级块 (superblock):存储文件系统基本信息
- inode 表:存储文件元数据 (权限、所有者、时间戳、block 指针)
- block bitmap:标记哪些 block 已使用
- inode bitmap:标记哪些 inode 已使用
- 日志 (journal):存储文件系统操作日志
xfs 元数据:
- AG (Allocation Group):xfs 将磁盘分成多个 AG,每个 AG 有独立的元数据
- inode B+树:存储 inode 分配信息
- extent B+树:存储文件数据块分配信息
元数据开销通常占总容量的 1%-5%,对于小文件很多的场景,开销更大。
3.4 挂载点覆盖
如果在一个已有文件的目录上挂载新的文件系统,原目录中的文件会被"隐藏", du 无法统计,但 df 仍然统计原文件系统的空间。
示例:
# /data目录下已有100GB文件
ls -lh /data
# total 100G
# 在/data上挂载新磁盘
mount /dev/sdb1 /data
# 此时du统计的是新磁盘的使用情况
du -sh /data
# 0
# 但原文件系统(/)的df仍然统计那100GB
df -h /
# 已用空间包括被覆盖的100GB
3.5 稀疏文件
稀疏文件 (sparse file) 是指文件中有大量空洞 (未分配实际 block) 的文件。
创建稀疏文件:
# 创建一个10GB的稀疏文件
dd if=/dev/zero of=sparse.img bs=1 count=0 seek=10G
# du显示实际占用空间(接近0)
du -h sparse.img
# 0
# ls显示文件大小(10GB)
ls -lh sparse.img
# 10G
# df统计实际占用的block
df -h .
# 只增加了inode,几乎不占用block
虚拟机镜像、数据库文件常使用稀疏文件节省空间。
四、常见原因与排查
4.1 原因1:已删除文件仍被进程打开
现象:
# 删除日志文件
rm /var/log/app.log
# df仍然显示空间已用
df -h /var
# 使用率90%
# du统计不到该文件
du -sh /var/log
# 总计80GB,但df显示90GB
根因:
应用进程仍然持有该文件的文件描述符,文件 inode 和 data block 未释放。
判断方法:
# 查看哪些进程持有已删除文件的描述符
lsof | grep deleted
# 输出示例
java 12345 root 3w REG 8,1 10737418240 /var/log/app.log (deleted)
关键字段解释:
3w :文件描述符编号 3,w 表示以写模式打开
REG :普通文件
8,1 :设备号
10737418240 :文件大小 (10GB)
(deleted) :文件已被删除
解决方法:
方法1:重启进程 (推荐)
# 找到进程PID
lsof | grep deleted | awk '{print $2}' | sort -u
# 重启进程
systemctl restart app.service
方法2:清空文件描述符 (不重启进程)
# 找到文件描述符路径
lsof -p 12345 | grep deleted
# java 12345 root 3w /var/log/app.log (deleted)
# 清空文件内容
: > /proc/12345/fd/3
# 或使用truncate
truncate -s 0 /proc/12345/fd/3
注意:
- 清空文件描述符后,文件内容丢失,但空间立即释放
- 应用可能因为日志丢失而出现异常,需要评估影响
统计已删除但未释放的总空间:
# 统计所有已删除文件占用的空间
lsof | grep deleted | awk '{sum+=$7} END {print sum/1024/1024/1024 " GB"}'
4.2 原因2:文件系统元数据占用
现象:
# df显示使用90%
df -h /var
# 已用900GB,可用100GB
# du统计只有850GB
du -sh /var
# 850GB
# 差异50GB是元数据开销
根因:
文件系统元数据 (inode 表、block bitmap、journal) 占用空间。
判断方法:
# 查看inode使用情况
df -i /var
# 输出示例
Filesystem Inodes IUsed IFree IUse%
/dev/sda1 62914560 55000000 7914560 88%
如果 inode 使用率很高,说明有大量小文件,元数据开销大。
查看文件系统元数据占用:
# ext4
dumpe2fs /dev/sda1 | grep -E "Block count|Free blocks|Inode count|Free inodes"
# 输出示例
Block count: 262144000 # 总block数
Free blocks: 26214400 # 可用block数
Inode count: 62914560 # 总inode数
Free inodes: 7914560 # 可用inode数
计算元数据开销:
- 总容量 = Block count × Block size (通常 4KB)
- 实际可用 = 总容量 - 元数据开销
- 元数据开销 ≈ 总容量 - (Free blocks × Block size + 已用文件大小)
解决方法:
方法1:清理小文件
# 找出小文件最多的目录
find /var -type f -size -1k | awk -F/ '{print $2}' | sort | uniq -c | sort -rn | head -10
# 删除不需要的小文件(如临时文件、缓存)
find /var/tmp -type f -mtime +7 -delete
方法2:调整文件系统参数 (重新格式化时)
# ext4减少inode数量
mkfs.ext4 -i 65536 /dev/sdb1 # 每64KB分配一个inode(默认16KB)
# xfs调整inode大小
mkfs.xfs -i size=512 /dev/sdb1 # inode大小512字节(默认256)
4.3 原因3:挂载点覆盖
现象:
# 在/data已有文件的情况下,挂载新磁盘
mount /dev/sdb1 /data
# du统计新磁盘
du -sh /data
# 10GB
# df统计根文件系统(包括被覆盖的文件)
df -h /
# 已用空间包括原/data下的100GB
根因:
挂载点覆盖后,原目录中的文件不可见,但仍然占用空间。
判断方法:
# 查看挂载点
mount | grep /data
# /dev/sdb1 on /data type ext4 (rw)
# 卸载挂载点
umount /data
# 查看原目录内容
ls -lh /data
# total 100G
# du统计原目录
du -sh /data
# 100GB
解决方法:
方法1:清理被覆盖的文件
# 卸载挂载点
umount /data
# 清理原目录
rm -rf /data/*
# 重新挂载
mount /dev/sdb1 /data
方法2:使用 bind mount 避免覆盖
# 创建新目录
mkdir /data_new
# 挂载到新目录
mount /dev/sdb1 /data_new
# 使用bind mount
mount --bind /data_new /data
检查所有挂载点是否覆盖:
# 查看所有挂载点
mount | grep -v "^proc\|^sysfs\|^devpts\|^tmpfs" | awk '{print $3}' | while read mp; do
echo "=== Checking $mp ==="
# 临时卸载并检查
if umount "$mp" 2>/dev/null; then
du -sh "$mp"
mount "$mp"
fi
done
注意: 此操作会短暂卸载挂载点,生产环境需在维护窗口执行。
4.4 原因4:稀疏文件
现象:
# 创建稀疏文件
qemu-img create -f qcow2 vm.img 100G
# ls显示100GB
ls -lh vm.img
# 100G
# du显示接近0
du -h vm.img
# 196K
# df统计实际占用
df -h .
# 几乎未增加
根因:
稀疏文件只分配了文件头部的 metadata block,数据 block 未实际分配。
判断方法:
# 查看文件的实际block数
stat vm.img | grep Blocks
# Blocks: 8 # 只分配了8个block(8 × 512B = 4KB)
# 对比文件大小
stat vm.img | grep Size
# Size: 107374182400 # 100GB
解决方法:
如果需要预分配空间,使用 fallocate :
# 预分配100GB
fallocate -l 100G vm.img
# 此时du和ls显示一致
du -h vm.img
# 100G
ls -lh vm.img
# 100G
找出所有稀疏文件:
# 找出ls大小 > du大小的文件
find /var -type f -exec sh -c '
ls_size=$(stat -c %s "$1")
du_size=$(du -b "$1" | cut -f1)
if [ $ls_size -gt $((du_size * 2)) ]; then
echo "$1: ls=$ls_size du=$du_size"
fi
' _ {} \;
4.5 原因5:文件系统损坏
现象:
# df显示空间已满
df -h /var
# 100% used
# du统计很少
du -sh /var
# 50GB
# 删除文件后空间仍未释放
根因:
文件系统元数据损坏,superblock 或 bitmap 不一致。
判断方法:
# ext4检查文件系统错误
dmesg | grep -i "ext4.*error"
# xfs检查文件系统错误
dmesg | grep -i "xfs.*error"
# 查看文件系统日志
journalctl -k | grep -i "filesystem"
解决方法:
# ext4修复文件系统(需要umount或只读模式)
umount /dev/sda1
fsck.ext4 -f /dev/sda1
# xfs修复文件系统
umount /dev/sda1
xfs_repair /dev/sda1
注意:
- fsck 可能导致数据丢失,修复前务必备份
- 生产环境建议先以只读模式 umount,快照后再修复
五、完整排查流程
5.1 第一步:确认差异范围
# 查看df统计
df -h /var
# 已用900GB
# 查看du统计
du -sh /var
# 850GB
# 计算差异
# 差异 = 900GB - 850GB = 50GB
如果差异超过 10%,需要继续排查。
5.2 第二步:排查已删除但未释放的文件
# 查找所有已删除但未释放的文件
lsof | grep deleted | awk '{sum+=$7} END {print sum/1024/1024/1024 "GB"}'
# 或者更详细的统计
lsof | grep deleted | awk '{print $1,$2,$7}' | while read proc pid size; do
size_gb=$(echo "scale=2; $size/1024/1024/1024" | bc)
echo "$proc (PID: $pid): ${size_gb}GB"
done | sort -t: -k2 -rn
如果这部分占用超过差异的 50%,重启相关进程即可解决。
5.3 第三步:排查挂载点覆盖
# 查看所有挂载点
mount | grep -v "^proc\|^sysfs\|^devpts\|^tmpfs"
# 对每个挂载点,检查是否覆盖了已有文件
for mp in $(mount | awk '{print $3}'); do
echo "=== $mp ==="
# 注意:此操作会短暂卸载挂载点
umount $mp 2>/dev/null && du -sh $mp && mount $mp
done
注意:
- 此方法会短暂卸载挂载点,影响服务
- 生产环境建议在维护窗口执行,或使用只读方式
5.4 第四步:排查稀疏文件
# 找出所有稀疏文件(ls大小 > du大小)
find /var -type f -exec sh -c '
ls_size=$(stat -c %s "$1" 2>/dev/null || echo 0)
du_size=$(du -b "$1" 2>/dev/null | cut -f1)
if [ $ls_size -gt $((du_size * 2)) ]; then
echo "$1: ls=$ls_size du=$du_size"
fi
' _ {} \; 2>/dev/null
稀疏文件在虚拟化、数据库场景常见,通常不需要处理。
5.5 第五步:检查 inode 使用情况
# 查看inode使用率
df -i /var
# 如果inode使用率>80%,查找小文件最多的目录
find /var -type f | awk -F/ '{print $2}' | sort | uniq -c | sort -rn | head -10
5.6 第六步:检查文件系统是否损坏
# 查看文件系统错误
dmesg | grep -E "ext4|xfs" | grep -i error
# 查看系统日志
journalctl -k --since "1 hour ago" | grep -i filesystem
如果有错误,考虑在维护窗口运行 fsck。
六、预防措施
6.1 日志轮转
配置 logrotate,避免日志文件无限增长。
# /etc/logrotate.d/app
/var/log/app/*.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
create 0644 app app
postrotate
systemctl reload app.service > /dev/null 2>&1 || true
endscript
}
关键参数:
daily :每天轮转
rotate 7 :保留 7 天
compress :压缩旧日志
delaycompress :延迟一天压缩 (避免进程仍在写入)
postrotate :轮转后重新加载应用 (释放旧文件描述符)
6.2 监控磁盘空间和 inode
Prometheus 监控规则:
# 磁盘空间告警
- alert: DiskSpaceLow
expr: (node_filesystem_avail_bytes / node_filesystem_size_bytes) < 0.1
for: 5m
annotations:
summary: "磁盘可用空间低于10%"
# inode使用率告警
- alert: InodeUsageHigh
expr: (node_filesystem_files_free / node_filesystem_files) < 0.2
for: 5m
annotations:
summary: "inode使用率超过80%"
# du与df差异告警
- alert: DiskUsageMismatch
expr: |
abs(
(node_filesystem_size_bytes - node_filesystem_avail_bytes) -
sum(du_bytes) by (mountpoint)
) > 10 * 1024^3
for: 10m
annotations:
summary: "du与df差异超过10GB"
6.3 定期清理临时文件
# 清理/tmp下7天未访问的文件
find /tmp -type f -atime +7 -delete
# 清理日志目录下30天前的压缩文件
find /var/log -name "*.gz" -mtime +30 -delete
配置 cron 定时执行:
# /etc/cron.daily/cleanup-tmp
#!/bin/bash
find /tmp -type f -atime +7 -delete
find /var/log -name "*.gz" -mtime +30 -delete
6.4 应用优雅重启
应用重启时,应先关闭所有文件描述符,再退出进程。
Java 应用示例:
// 注册shutdown hook
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
// 关闭日志文件
LogManager.shutdown();
// 关闭数据库连接
dataSource.close();
}));
Python 应用示例:
import atexit
import logging
def cleanup():
# 关闭日志文件
logging.shutdown()
atexit.register(cleanup)
6.5 避免挂载点覆盖
挂载前检查目标目录是否为空:
# 检查目录是否为空
if [ "$(ls -A /data)" ]; then
echo "Error: /data is not empty"
exit 1
fi
mount /dev/sdb1 /data
或使用 bind mount:
mkdir /mnt/data
mount /dev/sdb1 /mnt/data
mount --bind /mnt/data /data
七、实战案例
案例1:MySQL binlog 未释放
现象:
MySQL 服务器磁盘空间告警,删除 binlog 后空间未释放。
排查:
# df显示90%使用率
df -h /var/lib/mysql
# 已用900GB
# du统计只有500GB
du -sh /var/lib/mysql
# 500GB
# 查找已删除文件
lsof | grep deleted | grep mysql
# mysqld 1234 mysql 10w /var/lib/mysql/mysql-bin.000001 (deleted) 400GB
根因:
MySQL 进程持有已删除 binlog 的文件描述符。
解决:
# 方法1:重启MySQL(影响业务)
systemctl restart mysql
# 方法2:清空文件描述符(不重启)
lsof -p 1234 | grep deleted | awk '{print $4}' | sed 's/w$//' | while read fd; do
: > /proc/1234/fd/$fd
done
# 或使用MySQL命令删除
mysql -e "PURGE BINARY LOGS BEFORE NOW();"
预防:
配置 binlog 自动清理:
# my.cnf
[mysqld]
expire_logs_days = 7
案例2:Docker 容器日志占满磁盘
现象:
Docker 宿主机磁盘空间告警,但找不到大文件。
排查:
# df显示90%
df -h /var/lib/docker
# 已用900GB
# du统计只有600GB
du -sh /var/lib/docker
# 600GB
# 查找容器日志
du -sh /var/lib/docker/containers/*/
# 某个容器日志300GB
根因:
容器日志未限制大小,持续写入导致磁盘占满。
解决:
# 清理容器日志
find /var/lib/docker/containers/ -name "*-json.log" -exec truncate -s 0 {} \;
# 或重启容器
docker restart <container-id>
预防:
配置 Docker 日志限制:
# /etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "3"
}
}
案例3:XFS 文件系统元数据占用
现象:
XFS 文件系统使用率高,但找不到大文件。
排查:
# df显示90%
df -h /data
# 已用900GB
# du统计800GB
du -sh /data
# 800GB
# 查看inode使用率
df -i /data
# 85%
# 查看小文件数量
find /data -type f | wc -l
# 50000000 (5千万个文件)
根因:
大量小文件导致元数据开销大 (每个文件至少占用 512 字节 inode)。
解决:
# 清理小文件
find /data -type f -size -1k -mtime +30 -delete
# 或归档到tar
find /data -type f -size -1k -mtime +30 -print0 | tar -czf /backup/small_files.tar.gz --null -T -
find /data -type f -size -1k -mtime +30 -delete
八、总结
du 与 df 不一致的根本原因是两者统计方式不同:
du 统计可见文件,df 统计文件系统超级块。
常见原因和解决方法:
| 原因 |
现象 |
检测命令 |
解决方法 |
| 已删除文件未释放 |
删除文件后空间未增加 |
lsof | grep deleted |
重启进程或清空文件描述符 |
| 元数据占用 |
du 和 df 差异 5%-10% |
df -i |
清理小文件 |
| 挂载点覆盖 |
du 统计为 0 但 df 显示已用 |
mount + umount |
清理被覆盖的文件 |
| 稀疏文件 |
ls 显示大 du 显示小 |
stat + du |
预分配空间 |
| 文件系统损坏 |
删除文件空间不释放 |
dmesg | grep error |
fsck 修复 |
核心排查步骤:
- 确认 du 和 df 差异范围
- 检查已删除但未释放的文件
- 检查挂载点覆盖
- 检查稀疏文件
- 检查 inode 使用率
- 检查文件系统错误
预防措施:
- 配置日志轮转
- 监控磁盘空间和 inode
- 定期清理临时文件
- 应用优雅重启,关闭文件描述符
- 避免挂载点覆盖
掌握这些知识后,遇到磁盘空间问题时,能够快速定位根因,采取正确的解决措施,避免误操作导致的数据丢失或服务中断。如果还想深入了解更多 Linux 文件系统与内核机制 ,可以延伸到 TCP/IP、内核调度、Socket 等系统底层知识,对整体排查思路会更有帮助。
九、深入排查技巧
9.1 快速定位占用空间的进程
# 按进程统计已删除文件占用空间
lsof | grep deleted | awk '{process[$1]+=$7} END {for (p in process) print p, process[p]/1024/1024/1024 "GB"}' | sort -k2 -rn
9.2 定位大文件
# 查找大于1GB的文件
find /var -type f -size +1G -exec ls -lh {} \; | awk '{print $9, $5}'
# 查找TOP 20大文件
find /var -type f -printf '%s %p\n' | sort -nr | head -20 | awk '{print $2}' | xargs ls -lh
9.3 分析目录层级占用
# 递归统计每层目录占用
du -h /var --max-depth=2 | sort -hr | head -20
9.4 检查系统保留空间
ext 系列文件系统默认保留 5% 空间给 root 用户。
# 查看保留空间配置
tune2fs -l /dev/sda1 | grep "Reserved block count"
# 调整保留空间为1%
tune2fs -m 1 /dev/sda1
9.5 实时监控磁盘写入
# 监控哪个进程在写入磁盘
iotop -o -P
# 或使用pidstat
pidstat -d 1
# 监控文件变化
watch -n 1 "du -sh /var/log"
十、故障预案
10.1 紧急情况处理流程
磁盘空间 100% 使用时:
- 立即清理已删除文件
lsof | grep deleted | awk '{print $2}' | xargs -I {} kill -9 {}
- 清理系统临时文件
rm -rf /tmp/*
rm -rf /var/tmp/*
- 清理日志文件
find /var/log -name "*.log" -exec truncate -s 0 {} \;
- 清理包管理缓存
# CentOS/RHEL
yum clean all
# Ubuntu/Debian
apt-get clean
- 清理 Docker 资源
docker system prune -af
10.2 回滚方案
如果清理后服务异常:
- 恢复日志文件
# 从备份恢复
cp /backup/app.log /var/log/app.log
- 重启相关服务
systemctl restart app.service
- 检查应用状态
systemctl status app.service
journalctl -u app.service -n 100
十一、监控与告警
11.1 Prometheus 监控指标
# 磁盘空间监控
- record: disk:usage:percentage
expr: (1 - node_filesystem_avail_bytes / node_filesystem_size_bytes) * 100
# inode监控
- record: disk:inode:percentage
expr: (1 - node_filesystem_files_free / node_filesystem_files) * 100
# du与df差异监控
- record: disk:du_df:diff_gb
expr: (node_filesystem_size_bytes - node_filesystem_avail_bytes - sum(du_bytes) by (mountpoint)) / 1024^3
11.2 告警规则
groups:
- name: disk_alerts
rules:
- alert: DiskSpaceCritical
expr: disk:usage:percentage > 95
for: 5m
labels:
severity: critical
annotations:
summary: "磁盘使用率超过95%"
- alert: InodeExhausted
expr: disk:inode:percentage > 90
for: 5m
labels:
severity: warning
annotations:
summary: "inode使用率超过90%"
- alert: DiskUsageMismatch
expr: disk:du_df:diff_gb > 50
for: 10m
labels:
severity: warning
annotations:
summary: "du与df差异超过50GB"
十二、最佳实践清单
日常维护:
- [ ] 配置 logrotate,定期轮转日志
- [ ] 配置 tmpwatch 或 systemd-tmpfiles,定期清理临时文件
- [ ] 配置 Docker 日志限制
- [ ] 定期检查 du 与 df 差异
监控告警:
- [ ] 监控磁盘空间使用率,阈值 85% 告警
- [ ] 监控 inode 使用率,阈值 80% 告警
- [ ] 监控 du 与 df 差异,超过 10GB 告警
- [ ] 监控已删除但未释放的文件
应用开发:
- [ ] 应用退出时关闭所有文件描述符
- [ ] 日志输出到 stdout,由容器或 systemd 管理
- [ ] 避免应用长期持有已删除文件的描述符
- [ ] 定期 rotate 日志,避免单个文件过大
运维操作:
- [ ] 删除大文件前检查是否有进程打开
- [ ] 挂载前检查目标目录是否为空
- [ ] 定期备份重要数据
- [ ] 磁盘空间告警后,先排查再删除
容量规划:
- [ ] 根据业务增长预估磁盘需求
- [ ] 预留 20% 以上空间缓冲
- [ ] 定期 Review 磁盘使用趋势
- [ ] 提前规划磁盘扩容方案
这些措施能够有效预防和快速解决 du 与 df 不一致的问题,确保系统稳定运行。
十三、常见问题解答
Q1:为什么重启服务器后空间就释放了?
A: 重启会强制关闭所有进程,所有被进程持有的文件描述符都会释放。但重启代价高,生产环境应优先使用重启具体进程的方式。
Q2:可以强制释放文件描述符而不重启进程吗?
A: 可以通过清空 /proc/PID/fd/N 文件释放空间,但文件内容会丢失。更安全的方式是使用信号让进程重新打开文件:
# 发送SIGHUP信号(大部分应用会重新打开日志文件)
kill -HUP <PID>
# 或使用systemd reload
systemctl reload app.service
Q3:如何快速定位哪个目录增长最快?
# 记录当前各目录大小
du -sh /* > /tmp/du_baseline.txt
# 等待一段时间后再次统计
sleep 3600 # 等待1小时
du -sh /* > /tmp/du_current.txt
# 对比差异
paste /tmp/du_baseline.txt /tmp/du_current.txt | awk '{print $2, $1, $3}' | while read dir size1 size2; do
echo "$dir: $size1 -> $size2"
done
Q4:ext4 和 xfs 在元数据开销上有什么区别?
A:
- ext4:固定 inode 数量,格式化时确定,无法动态调整
- xfs:动态分配 inode,更适合小文件场景,但元数据开销可能更大
如果有大量小文件,xfs 通常表现更好。
Q5:可以在线调整文件系统保留空间吗?
A: ext 系列可以:
# 将保留空间从5%调整为1%
tune2fs -m 1 /dev/sda1
xfs 不支持保留空间配置。
Q6:如何避免日志文件占满磁盘?
A: 三种方案:
- logrotate 自动轮转 :最推荐
- 应用层限制日志大小 :如 log4j 的 RollingFileAppender
- 监控+告警+自动清理 :作为兜底方案
Q7:Docker overlay2 占用空间大怎么办?
# 查看Docker磁盘使用情况
docker system df
# 清理未使用的镜像、容器、卷
docker system prune -a
# 清理所有build cache
docker builder prune -a
Q8:如何预估文件系统元数据开销?
粗略估算:
- 大文件场景 (平均 >1MB):元数据开销约 1-2%
- 中等文件场景 (平均 100KB-1MB):元数据开销约 3-5%
- 小文件场景 (平均 <100KB):元数据开销可能超过 10%
精确计算:
# ext4
dumpe2fs /dev/sda1 | grep "Block count"
dumpe2fs /dev/sda1 | grep "Free blocks"
# 元数据开销 = (总block - 可用block - 已用文件占用block) × block大小
Q9:为什么有时候 df 显示的 Used + Available ≠ Size?
A: 几个原因:
- 文件系统保留空间 (ext 系列默认 5%)
- 元数据占用 (inode 表、bitmap、journal)
- 坏块标记占用的空间
- 对齐和填充占用的空间
这是正常现象,不是 bug。
Q10:生产环境磁盘空间紧急,哪些文件可以安全删除?
相对安全:
/tmp/* 临时文件
/var/tmp/* 临时文件
/var/log/*.gz 压缩日志
/var/cache/yum/* 或 /var/cache/apt/* 包管理缓存
~/.cache/* 用户缓存
需谨慎:
- 当前日志文件 (可能被进程持有)
- 数据库文件 (binlog、redo log 等)
- 应用数据文件
绝对不能删:
- 系统配置文件
/etc/*
- 系统二进制文件
/bin/* 、 /sbin/* 、 /usr/bin/*
- 系统库文件
/lib/* 、 /usr/lib/*
十四、扩展阅读
14.1 文件系统原理
- inode 与文件的关系
- 文件系统的 block 分配策略
- 日志型文件系统 (journaling) 的原理
- Copy-on-Write 文件系统 (btrfs、zfs)
14.2 相关命令深入
# lsof高级用法
lsof -p <PID> # 查看指定进程打开的文件
lsof +D /var/log # 查看目录下被打开的文件
lsof -u root # 查看指定用户打开的文件
lsof -i :8080 # 查看占用端口的进程
# stat查看文件详细信息
stat <file> # 查看文件inode、block、时间戳
stat -f <file> # 查看文件系统信息
# dumpe2fs查看ext文件系统信息
dumpe2fs /dev/sda1 # 查看superblock、inode表信息
# xfs_info查看xfs文件系统信息
xfs_info /dev/sda1 # 查看block大小、inode大小等
14.3 性能优化
- 选择合适的文件系统 (ext4、xfs、btrfs)
- 调整 inode 数量和大小
- 使用 SSD 时启用 trim
- 定期清理碎片 (ext4 使用 e4defrag)
- 监控 IOPS 和延迟
这些知识点能够帮助深入理解 Linux 文件系统的工作机制,更好地排查和预防磁盘空间相关问题。对于想系统提升 Linux 运维与 SRE 能力 的读者,可以从 Bash 脚本、ELK Stack 日志分析、配置管理等多维度构建完整的运维知识体系。
十五、自动化脚本
15.1 一键排查脚本
#!/bin/bash
# disk_analysis.sh - 一键排查du与df差异
echo "=== 磁盘使用情况 ==="
df -h | grep -v tmpfs
echo -e "\n=== du与df差异分析 ==="
for mp in $(df -h | grep -v tmpfs | grep -v Filesystem | awk '{print $6}'); do
df_used=$(df -BG $mp | tail -1 | awk '{print $3}' | tr -d 'G')
du_used=$(du -sg $mp 2>/dev/null | awk '{print $1}')
diff=$((df_used - du_used))
if [ $diff -gt 10 ]; then
echo "$mp: df=${df_used}GB du=${du_used}GB 差异=${diff}GB"
fi
done
echo -e "\n=== 已删除但未释放的文件 ==="
lsof 2>/dev/null | grep deleted | awk '{print $1,$2,$7}' | awk '{sum[$1]+=$3} END {for (p in sum) if (sum[p] > 1073741824) print p, sum[p]/1073741824 "GB"}' | sort -k2 -rn
echo -e "\n=== inode使用情况 ==="
df -i | grep -v tmpfs | awk '{if (NR>1 && $5+0 > 80) print $0}'
echo -e "\n=== TOP 10大文件 ==="
find /var /home -type f -size +1G 2>/dev/null -exec ls -lh {} \; | awk '{print $9, $5}' | sort -k2 -hr | head -10
15.2 自动清理脚本
#!/bin/bash
# auto_cleanup.sh - 自动清理磁盘空间
THRESHOLD=90 # 磁盘使用率阈值
for mp in $(df -h | grep -v tmpfs | grep -v Filesystem | awk '{print $6}'); do
usage=$(df -h $mp | tail -1 | awk '{print $5}' | tr -d '%')
if [ $usage -gt $THRESHOLD ]; then
echo "[$mp] 使用率${usage}%,开始清理..."
# 清理已删除文件
lsof 2>/dev/null | grep $mp | grep deleted | awk '{print $2}' | sort -u | while read pid; do
echo " 清空进程 $pid 的已删除文件"
for fd in /proc/$pid/fd/*; do
if [ -L $fd ] && readlink $fd | grep -q deleted; then
: > $fd 2>/dev/null
fi
done
done
# 清理旧日志
find $mp -name "*.log.*" -mtime +7 -delete 2>/dev/null
find $mp -name "*.gz" -mtime +30 -delete 2>/dev/null
echo " 清理完成,当前使用率:$(df -h $mp | tail -1 | awk '{print $5}')"
fi
done
15.3 监控集成脚本
#!/bin/bash
# disk_exporter.sh - 导出自定义磁盘指标到Prometheus
TEXTFILE_DIR="/var/lib/node_exporter/textfile_collector"
mkdir -p $TEXTFILE_DIR
# 统计已删除但未释放的文件
deleted_bytes=$(lsof 2>/dev/null | grep deleted | awk '{sum+=$7} END {print sum}')
echo "disk_deleted_but_unreleased_bytes ${deleted_bytes:-0}" > $TEXTFILE_DIR/disk_deleted.prom
# 统计du与df差异
for mp in $(df -h | grep -v tmpfs | grep -v Filesystem | awk '{print $6}'); do
df_bytes=$(df -B1 $mp | tail -1 | awk '{print $3}')
du_bytes=$(du -sb $mp 2>/dev/null | awk '{print $1}')
diff=$((df_bytes - du_bytes))
echo "disk_usage_diff_bytes{mountpoint=\"$mp\"} $diff"
done > $TEXTFILE_DIR/disk_diff.prom
这些脚本可以集成到 cron 或 systemd timer 中,实现自动化巡检和清理。
十六、总结
核心要点:
- du 统计可见文件,df 统计文件系统超级块,两者统计方式不同
- 已删除但被进程持有的文件不会释放空间,是最常见的原因
- 文件系统元数据、挂载点覆盖、稀疏文件也会导致差异
- 使用 lsof、df -i、stat 等工具快速定位问题
- 预防措施包括日志轮转、监控告警、应用优雅重启
排查流程:
确认差异 → 检查已删除文件 → 检查挂载点 → 检查稀疏文件 → 检查 inode → 检查文件系统错误
最佳实践:
配置日志轮转 + 监控告警 + 定期清理 + 应用优雅重启 + 避免挂载点覆盖
掌握这些知识和工具后,面对磁盘空间问题能够快速定位、正确处理、有效预防。像这类围绕 Linux 文件系统、inode、进程描述符的实战问题,在 云栈社区 的运维板块里也经常被拿出来讨论,如果你在实际排查中遇到过更隐蔽的情况,欢迎来一起交流。
ONE MORE THING :本文偏向 ext4/xfs 场景。如果你用的是 btrfs 或 zfs 这类 Copy-on-Write 文件系统, du 与 df 的差异本质上也会存在,但底层机制和排查工具会有差异,建议结合对应文件系统的文档进一步验证。