一、概述
1.1 背景介绍
Linux 文件系统的空间管理建立在两个核心数据结构之上:inode 和 block。block 是实际存储数据的最小单元,ext4 文件系统默认 block 大小为 4KB,xfs 同样默认 4KB。inode 则是文件的元数据记录,保存了文件的权限、时间戳、数据 block 指针等信息,但不包含文件名——文件名存储在目录项(dentry)中。
每个文件系统在格式化时会分配固定数量的 inode。ext4 默认每 16KB 分配一个 inode,意味着一块 1TB 的分区大约有 6700 万个 inode。xfs 则采用动态 inode 分配策略,在需要时才分配新的 inode 块,因此 xfs 很少出现 inode 耗尽的问题。
磁盘空间爆满从技术角度可以归为五大类原因:
第一类:数据 block 耗尽。 这是最常见的情况。大文件不断写入,block 被逐步消耗完毕。典型场景是日志文件未做切割,单个文件增长到几十甚至上百 GB。
第二类:inode 耗尽。 磁盘空间显示还有剩余,但无法创建新文件。这种情况多发生在大量小文件堆积的场景,例如邮件队列目录、session 文件目录、缓存目录等。
第三类:已删除文件未释放空间。 文件已经被 rm 删除,但仍有进程持有该文件的文件描述符。在 /proc 文件系统中仍能看到该文件的引用,du 统计的空间与 df 显示的空间出现明显差异。
第四类:预留空间占用。 ext4 默认为 root 用户预留 5% 的磁盘空间,在大容量磁盘上这个比例会造成显著浪费。一块 10TB 的磁盘,5% 就是 500GB。
第五类:文件系统损坏或异常挂载。 某些情况下,文件系统的 superblock 损坏或 mount 操作覆盖了已有数据目录,导致原有文件被"隐藏"但空间未释放。
理解这五类原因是精准定位磁盘空间问题的基础。不同类型的问题需要不同的排查工具和处理手段。
1.2 技术特点
磁盘空间排查涉及的核心技术栈包括:
- 文件系统层:ext4 的 extent tree 结构、xfs 的 B+ tree 分配组、btrfs 的 CoW 机制
- VFS 层:Linux 虚拟文件系统的 inode 缓存、dentry 缓存、文件描述符管理
- 进程层:/proc/pid/fd 目录下的文件描述符映射、lsof 工具的工作原理
- 块设备层:LVM 卷管理、RAID 阵列的空间计算、分区表与文件系统的关系
ext4 文件系统从 Linux 内核 2.6.28 引入,经过十多年发展已经非常成熟。ext4 支持最大 1EB 的文件系统和最大 16TB 的单文件(4K block size)。ext4 引入了 extent 机制替代传统的间接块指针,大幅提升了大文件的读写性能。
xfs 在高并发 I/O 场景下表现更优,它使用分配组(Allocation Group)实现并行写入,适合大规模数据存储。RHEL/Rocky Linux 从 7.x 开始将 xfs 设为默认文件系统。
1.3 适用场景
- 应用部署失败,报错 "No space left on device"
1.4 环境要求
| | |
|---|
| Ubuntu 24.04 LTS / Rocky Linux 9.5 | |
| | |
| | 提供 lsblk/findmnt/blkid 等工具 |
| | |
| | |
| | |
| | |
| | |
二、详细步骤
2.1 准备工作
2.1.1 系统检查
登录目标服务器后,首先获取全局概览:
# 查看操作系统版本
cat /etc/os-release | grep -E "^(NAME|VERSION)="
# 查看内核版本
uname -r
# 查看所有挂载点的磁盘使用情况(-T 显示文件系统类型)
df -Th
# 查看 inode 使用情况
df -i
# 查看块设备拓扑
lsblk -f
df 命令输出示例:
Filesystem Type Size Used Avail Use% Mounted on
/dev/sda3 ext4 100G 95G 320M 97% /
/dev/sdb1 xfs 2.0T 1.9T 87G 96% /data
tmpfs tmpfs 16G 2.3G 14G 15% /dev/shm
/dev/sda1 ext4 512M 198M 278M 42% /boot
从这个输出可以直接看到 / 分区和 /data 分区都处于危险水位。注意 Avail 列不等于 Size - Used,差值就是 ext4 预留给 root 的空间。
2.1.2 安装排查工具
# Ubuntu 24.04
sudo apt update
sudo apt install -y lsof ncdu sysstat iotop-c tree
# Rocky Linux 9.5
sudo dnf install -y lsof ncdu sysstat iotop tree
ncdu 2.x 是用 Rust 重写的版本,性能比旧版 C 实现提升了 3-5 倍,特别是在扫描百万级文件数的目录时差距明显。
2.1.3 快速判断空间问题类型
#!/bin/bash
# 快速判断磁盘空间问题类型
# 用法: bash disk_check_type.sh /dev/sda3
DEVICE=${1:-"/dev/sda3"}
MOUNT=$(findmnt -n -o TARGET "$DEVICE" 2>/dev/null)
if [ -z "$MOUNT" ]; then
echo"设备 $DEVICE 未挂载"
exit 1
fi
echo"=== 检查设备: $DEVICE 挂载点: $MOUNT ==="
# 检查 block 使用率
BLOCK_USE=$(df --output=pcent "$MOUNT" | tail -1 | tr -d ' %')
echo"Block 使用率: ${BLOCK_USE}%"
# 检查 inode 使用率
INODE_USE=$(df --output=ipcent "$MOUNT" | tail -1 | tr -d ' %')
echo"Inode 使用率: ${INODE_USE}%"
# 检查 du 与 df 差异(已删除未释放文件的指标)
DF_USED=$(df -B1 --output=used "$MOUNT" | tail -1 | tr -d ' ')
DU_USED=$(sudo du -sb "$MOUNT" 2>/dev/null | awk '{print $1}')
DIFF=$((DF_USED - DU_USED))
DIFF_MB=$((DIFF / 1024 / 1024))
echo"df 报告已用: $((DF_USED/1024/1024)) MB"
echo"du 统计已用: $((DU_USED/1024/1024)) MB"
echo"差异(可能是已删除未释放): ${DIFF_MB} MB"
# 判断问题类型
if [ "$BLOCK_USE" -ge 95 ] && [ "$INODE_USE" -lt 80 ]; then
echo">>> 诊断: Block 空间不足,需要清理大文件"
elif [ "$INODE_USE" -ge 95 ] && [ "$BLOCK_USE" -lt 80 ]; then
echo">>> 诊断: Inode 耗尽,需要清理海量小文件"
elif [ "$DIFF_MB" -gt 1024 ]; then
echo">>> 诊断: 存在大量已删除但未释放的文件(差异 > 1GB)"
elif [ "$BLOCK_USE" -ge 95 ] && [ "$INODE_USE" -ge 95 ]; then
echo">>> 诊断: Block 和 Inode 均耗尽,需要全面清理"
else
echo">>> 诊断: 磁盘状态正常"
fi
2.2 核心排查操作
2.2.1 df/du/lsof 三板斧定位
第一步:df 定位问题分区
# 只显示使用率超过 80% 的分区
df -Th | awk 'NR==1 || $6+0 > 80'
# 对于 LVM 环境,需要同时查看 VG 剩余空间
sudo vgs
sudo lvs
第二步:du 逐层下钻找到大目录
# 从根目录开始,找到占用最大的一级目录
# --exclude 排除虚拟文件系统,避免误判
sudo du -sh /* --exclude=/proc --exclude=/sys --exclude=/dev --exclude=/run 2>/dev/null | sort -rh | head -15
输出示例:
85G /var
4.2G /usr
2.1G /home
1.3G /opt
512M /boot
确定 /var 是主要占用者后继续下钻:
sudo du -sh /var/* 2>/dev/null | sort -rh | head -10
78G /var/log
3.2G /var/lib
2.1G /var/cache
890M /var/tmp
继续下钻到 /var/log:
sudo du -sh /var/log/* 2>/dev/null | sort -rh | head -10
72G /var/log/app
3.8G /var/log/journal
1.2G /var/log/nginx
890M /var/log/syslog
三层 du 就能精确定位到问题目录。
第三步:lsof 检查已删除未释放文件
# 列出所有已删除但仍被进程持有的文件
# +L1 表示 link count < 1,即文件已从目录中删除
sudo lsof +L1 2>/dev/null | grep -i deleted
# 按文件大小排序显示
sudo lsof +L1 2>/dev/null | awk '$7 ~ /^[0-9]+$/ {print $7, $1, $2, $9}' | sort -rn | head -20
输出示例:
34359738368 java 12345 /var/log/app/application.log (deleted)
8589934592 nginx 6789 /var/log/nginx/access.log (deleted)
第一列是字节数,34359738368 字节约等于 32GB。这表明 Java 进程 PID 12345 仍持有已删除的 32GB 日志文件。
2.2.2 已删除文件仍占空间的处理
当 lsof 发现大量已删除未释放的文件时,有三种处理方式:
方式一:重启相关进程(推荐,最彻底)
# 确认进程信息
ps aux | grep 12345
# 如果是可以重启的应用服务
sudo systemctl restart app-service
# 重启后验证空间释放
df -Th /var
方式二:清空文件描述符对应的内容(不重启进程)
# 找到进程持有的文件描述符
sudo ls -la /proc/12345/fd/ | grep deleted
# 假设找到 fd 编号为 15
# 将文件内容截断为 0(不会影响进程继续写入)
sudo truncate -s 0 /proc/12345/fd/15
# 验证效果
sudo ls -la /proc/12345/fd/15
df -Th /var
truncate 操作是原子性的,进程会继续向该文件描述符写入数据,但文件大小从 0 重新开始增长。这种方式适合无法立即重启的核心服务。
方式三:发送信号让进程重新打开日志文件
# 很多守护进程支持 SIGHUP 信号来重新打开日志文件
# Nginx 使用 USR1 信号
sudo kill -USR1 $(cat /run/nginx.pid)
# syslog-ng / rsyslog
sudo systemctl reload rsyslog
2.2.3 大文件定位
# 方法一:find 查找超过 1GB 的文件
sudo find / -xdev -type f -size +1G -exec ls -lh {} \; 2>/dev/null | sort -k5 -rh
# 方法二:find 查找最近 7 天内修改的大文件(>500MB)
sudo find / -xdev -type f -size +500M -mtime -7 -exec ls -lh {} \; 2>/dev/null
# 方法三:使用 ncdu 进行交互式分析
sudo ncdu / --exclude /proc --exclude /sys --exclude /dev
# 方法四:查找最近 24 小时内快速增长的文件
sudo find /var/log -xdev -type f -mmin -1440 -size +100M -printf'%s %p\n' 2>/dev/null | sort -rn | head -20
find 的 -xdev 参数非常重要,它限制搜索范围在同一个文件系统内,避免跨入 /proc、/sys 等虚拟文件系统造成误判或性能问题。
对于需要定期监控文件增长趋势的场景:
#!/bin/bash
# 记录指定目录下大文件的大小变化
# 每小时通过 cron 执行一次
LOG_DIR="/var/log/disk_monitor"
mkdir -p "$LOG_DIR"
TIMESTAMP=$(date '+%Y%m%d_%H%M%S')
# 记录 /var/log 下所有超过 100MB 的文件
sudo find /var/log -xdev -type f -size +100M -printf'%s %p\n' 2>/dev/null | \
sort -rn > "${LOG_DIR}/large_files_${TIMESTAMP}.txt"
# 与上一次记录对比
PREV=$(ls -t "${LOG_DIR}"/large_files_*.txt 2>/dev/null | sed -n '2p')
if [ -n "$PREV" ]; then
echo"=== 文件大小变化 ($(basename $PREV) -> ${TIMESTAMP}) ===" >> "${LOG_DIR}/changes.log"
diff "$PREV""${LOG_DIR}/large_files_${TIMESTAMP}.txt" >> "${LOG_DIR}/changes.log" 2>&1
fi
2.2.4 inode 耗尽排查
inode 耗尽时的典型报错:
cannot create regular file 'test.txt': No space left on device
但 df -h 显示仍有大量可用空间。此时需要检查 inode:
# 查看 inode 使用情况
df -i
# 输出示例
# Filesystem Inodes IUsed IFree IUse% Mounted on
# /dev/sda3 6553600 6553600 0 100% /
定位 inode 消耗最多的目录:
# 统计每个一级目录下的文件数量
for dir in /*; do
[ -d "$dir" ] || continue
count=$(sudo find "$dir" -xdev -type f 2>/dev/null | wc -l)
echo"$count$dir"
done | sort -rn | head -10
输出示例:
5234567 /var
234567 /usr
56789 /home
12345 /opt
继续下钻 /var:
for dir in /var/*; do
[ -d "$dir" ] || continue
count=$(sudo find "$dir" -xdev -type f 2>/dev/null | wc -l)
echo"$count$dir"
done | sort -rn | head -10
4987654 /var/spool
123456 /var/lib
98765 /var/log
如果确定了具体目录(如 /var/spool/postfix/maildrop),可以批量清理:
# 先确认文件数量和时间范围
sudo find /var/spool/postfix/maildrop -type f | head -5
sudo find /var/spool/postfix/maildrop -type f -mtime +30 | wc -l
# 删除 30 天前的文件(使用 xargs 提高效率,避免 arg list too long)
sudo find /var/spool/postfix/maildrop -type f -mtime +30 -print0 | xargs -0 -r rm -f
# 如果文件数量极大(百万级),分批删除避免 I/O 风暴
sudo find /var/spool/postfix/maildrop -type f -mtime +30 -print0 | xargs -0 -r -n 10000 rm -f
2.2.5 日志文件暴涨处理
日志暴涨是磁盘爆满最常见的原因。处理分为紧急止血和长期治理两步。
紧急止血:
# 方法一:截断日志文件(保留文件句柄,不影响正在写入的进程)
sudo truncate -s 0 /var/log/app/application.log
# 方法二:只保留最后 1000 行
sudo tail -n 1000 /var/log/app/application.log > /tmp/app_tail.log
sudo cp /tmp/app_tail.log /var/log/app/application.log
rm /tmp/app_tail.log
# 方法三:压缩历史日志
sudo gzip /var/log/app/application.log.1
sudo gzip /var/log/app/application.log.2
注意:不要直接 echo "" > /var/log/app/application.log,在高频写入场景下可能丢失数据。truncate 是原子操作,更安全。
长期治理:配置 logrotate
# 创建应用日志切割配置
sudo tee /etc/logrotate.d/app-service << 'EOF'
/var/log/app/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 0640 appuser appgroup
sharedscripts
postrotate
# 通知应用重新打开日志文件
# 方式取决于应用类型
systemctl reload app-service > /dev/null 2>&1 || true
endscript
maxsize 500M
su appuser appgroup
}
EOF
# 测试配置是否正确(dry-run 模式)
sudo logrotate -d /etc/logrotate.d/app-service
# 手动执行一次切割
sudo logrotate -f /etc/logrotate.d/app-service
# 验证切割结果
ls -lh /var/log/app/
logrotate 关键参数说明:
delaycompress:延迟一个周期再压缩(避免正在读取的文件被压缩)maxsize 500M:文件超过 500MB 也触发切割(不等到定时任务)sharedscripts:多个日志文件匹配时,postrotate 只执行一次su appuser appgroup:以指定用户身份执行切割(避免权限问题)
2.2.6 /tmp 和 /var 空间异常
/tmp 清理:
systemd 默认通过 systemd-tmpfiles-clean.timer 定期清理 /tmp:
# 查看 tmpfiles 清理配置
cat /usr/lib/tmpfiles.d/tmp.conf
# 典型内容:
# q /tmp 1777 root root 10d
# 含义:/tmp 中超过 10 天未访问的文件会被自动清理
# 查看清理定时器状态
systemctl status systemd-tmpfiles-clean.timer
# 手动触发清理
sudo systemd-tmpfiles --clean
# 查看 /tmp 目录占用详情
sudo du -sh /tmp/* 2>/dev/null | sort -rh | head -10
/var/lib 常见大户:
# Docker 镜像和容器数据
sudo du -sh /var/lib/docker/*
# 清理未使用的 Docker 资源
sudo docker system prune -a --volumes
# snapd 缓存(Ubuntu 特有)
sudo du -sh /var/lib/snapd/*
# 移除旧版本 snap
sudo snap list --all | awk '/disabled/{print $1, $3}' | whileread name rev; do
sudo snap remove "$name" --revision="$rev"
done
# systemd journal 日志
sudo du -sh /var/log/journal/
# 只保留最近 7 天的 journal
sudo journalctl --vacuum-time=7d
# 或限制总大小为 500MB
sudo journalctl --vacuum-size=500M
/var/cache 清理:
# APT 缓存(Ubuntu/Debian)
sudo du -sh /var/cache/apt/archives/
sudo apt clean
# DNF 缓存(Rocky Linux/RHEL)
sudo du -sh /var/cache/dnf/
sudo dnf clean all
# pip 缓存
du -sh ~/.cache/pip/
pip cache purge
2.2.7 预留空间(Reserved Blocks)释放
ext4 默认预留 5% 的空间给 root 用户,防止普通用户把磁盘写满导致系统无法正常运行。但在纯数据分区上,这个预留是不必要的。
# 查看当前预留块比例
sudo tune2fs -l /dev/sdb1 | grep -i reserved
# 输出示例:
# Reserved block count: 26214400
# Reserved blocks uid: 0 (user root)
# Reserved blocks gid: 0 (group root)
# 计算预留空间大小
# block count * block size = 预留字节数
sudo tune2fs -l /dev/sdb1 | grep -E "Block count|Reserved block count|Block size"
# 将预留比例从 5% 调整为 1%(数据分区推荐)
sudo tune2fs -m 1 /dev/sdb1
# 纯数据分区可以设为 0%(但不建议用于系统分区)
sudo tune2fs -m 0 /dev/sdb1
# 验证修改结果
df -Th /data
在一块 2TB 的 ext4 分区上,5% 预留空间约 100GB。将其调整为 1% 可以立即释放约 80GB 空间,这在紧急情况下非常有用。
xfs 文件系统没有 reserved blocks 机制,因此不需要进行此操作。
2.3 启动和验证
完成清理后,需要进行系统性验证:
#!/bin/bash
# 磁盘清理验证脚本
echo"=== 1. 磁盘空间使用 ==="
df -Th | grep -v tmpfs | grep -v devtmpfs
echo""
echo"=== 2. Inode 使用 ==="
df -i | grep -v tmpfs | grep -v devtmpfs
echo""
echo"=== 3. 已删除未释放文件 ==="
DELETED_SIZE=$(sudo lsof +L1 2>/dev/null | awk '$7 ~ /^[0-9]+$/ {sum+=$7} END {printf "%.0f", sum/1024/1024}')
echo"已删除未释放文件总大小: ${DELETED_SIZE} MB"
echo""
echo"=== 4. 各分区最大文件 TOP5 ==="
for mount in $(df --output=target | tail -n +2 | grep -v tmpfs); do
echo"--- $mount ---"
sudo find "$mount" -xdev -type f -size +100M -printf'%s %p\n' 2>/dev/null | sort -rn | head -5 | awk '{printf "%.1f MB %s\n", $1/1024/1024, $2}'
done
echo""
echo"=== 5. logrotate 配置检查 ==="
sudo logrotate -d /etc/logrotate.conf 2>&1 | grep -c "error"
echo"(0 表示无配置错误)"
echo""
echo"=== 6. 预留空间检查(ext4 分区)==="
for dev in $(df --output=source -t ext4 | tail -n +2); do
reserved=$(sudo tune2fs -l "$dev" 2>/dev/null | grep "Reserved block count" | awk '{print $NF}')
total=$(sudo tune2fs -l "$dev" 2>/dev/null | grep "^Block count" | awk '{print $NF}')
if [ -n "$reserved" ] && [ -n "$total" ]; then
pct=$(echo"scale=1; $reserved * 100 / $total" | bc)
echo"$dev: 预留 ${pct}%"
fi
done
三、示例代码和配置
3.1 完整配置示例
3.1.1 logrotate 标准配置模板
# 文件路径:/etc/logrotate.d/app-logs
# 适用于 Java/Python/Go 等应用日志
/var/log/myapp/*.log
/var/log/myapp/**/*.log {
# 切割频率:每天
daily
# 保留 30 个历史文件
rotate 30
# 压缩历史日志(使用 zstd 替代 gzip,压缩率更高)
compress
compresscmd /usr/bin/zstd
compressoptions -T0 --rm
compressext .zst
uncompresscmd /usr/bin/unzstd
# 延迟一个周期压缩
delaycompress
# 文件不存在不报错
missingok
# 空文件不切割
notifempty
# 切割后创建新文件
create 0640 appuser appgroup
# 日期后缀格式
dateext
dateformat -%Y%m%d-%H%M%S
# 单文件超过 200MB 也触发切割
maxsize 200M
# 最小切割间隔 1 小时
minsize 1M
# 以应用用户身份执行
su appuser appgroup
# 多文件匹配时 postrotate 只执行一次
sharedscripts
postrotate
# Java 应用:通过 copytruncate 方式无需通知进程
# 如果使用 copytruncate,注释掉 create 行
# 其他方式:发送信号让应用重新打开日志
if [ -f /var/run/myapp.pid ]; then
kill -USR1 $(cat /var/run/myapp.pid) 2>/dev/null || true
fi
endscript
}
3.1.2 系统级日志保留策略配置
# 文件路径:/etc/logrotate.d/system-logs
# syslog
/var/log/syslog {
daily
rotate 7
compress
delaycompress
missingok
notifempty
postrotate
/usr/lib/rsyslog/rsyslog-rotate
endscript
}
# auth 日志(安全相关,保留更长时间)
/var/log/auth.log {
weekly
rotate 52
compress
delaycompress
missingok
notifempty
postrotate
/usr/lib/rsyslog/rsyslog-rotate
endscript
}
# kern 日志
/var/log/kern.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
postrotate
/usr/lib/rsyslog/rsyslog-rotate
endscript
}
3.1.3 journald 持久化配置
# 文件路径:/etc/systemd/journald.conf
[Journal]
# 持久化存储到 /var/log/journal/
Storage=persistent
# 最大占用磁盘空间
SystemMaxUse=1G
# 单个日志文件最大大小
SystemMaxFileSize=128M
# 保留最近 30 天的日志
MaxRetentionSec=30day
# 磁盘空间不足时的行为(保留 15% 可用空间)
SystemKeepFree=15%
# 限制日志写入速率(防止日志风暴)
RateLimitIntervalSec=30s
RateLimitBurst=10000
# 压缩存储
Compress=yes
# 转发到 syslog
ForwardToSyslog=no
# 应用配置
sudo systemctl restart systemd-journald
# 验证配置生效
journalctl --disk-usage
3.2 实际应用案例
案例一:Java 应用日志未切割导致单文件 200GB
场景描述: 生产环境某 Java 微服务运行 3 个月后,磁盘告警。排查发现 /var/log/app/stdout.log 单文件达到 213GB。原因是开发团队使用 System.out.println 输出调试信息,且未配置 logrotate。
排查过程:
# 1. 确认问题
df -Th /var
# Filesystem Type Size Used Avail Use% Mounted on
# /dev/sda3 ext4 250G 241G 2.3G 99% /var
# 2. 定位大文件
sudo find /var -xdev -type f -size +10G -exec ls -lh {} \;
# -rw-r--r-- 1 appuser appgroup 213G Mar 10 14:32 /var/log/app/stdout.log
# 3. 查看文件增长速度(对比文件头尾时间戳)
head -1 /var/log/app/stdout.log
# 2025-12-15 03:22:11 INFO Application started
tail -1 /var/log/app/stdout.log
# 2026-03-13 14:32:45 DEBUG Processing request id=abc123
# 4. 计算日均增长量
# 213GB / 88天 ≈ 2.4GB/天
# 5. 紧急处理:保留最后 10000 行并截断
sudo tail -n 10000 /var/log/app/stdout.log > /tmp/stdout_tail.log
sudo cp /tmp/stdout_tail.log /var/log/app/stdout.log
rm /tmp/stdout_tail.log
# 6. 验证空间释放
df -Th /var
# Filesystem Type Size Used Avail Use% Mounted on
# /dev/sda3 ext4 250G 28G 210G 12% /var
# 7. 配置 logrotate 防止复发(参见 3.1.1 配置模板)
sudo vim /etc/logrotate.d/app-service
长效措施:
# 8. 推动开发团队修改日志配置
# 建议使用 SLF4J + Logback,配置日志级别和文件大小限制
# 9. 添加 cron 监控脚本
cat > /etc/cron.d/disk-monitor << 'EOF'
# 每小时检查磁盘使用率,超过 85% 发送告警
0 * * * * root /opt/scripts/disk_alert.sh
EOF
案例二:已删除但未释放的 Nginx 日志文件
场景描述: 运维人员手动执行了 rm /var/log/nginx/access.log 来释放空间,但 df 显示空间没有减少。
排查过程:
# 1. du 和 df 数据对比
sudo du -sh /var
# 12G /var
df -Th /var
# Filesystem Type Size Used Avail Use% Mounted on
# /dev/sda3 ext4 100G 95G 320M 97% /var
# du 显示 12G,df 显示 95G,差值 83G 就是已删除未释放的空间
# 2. lsof 查找元凶
sudo lsof +L1 | grep nginx
# nginx 1234 root 5w REG 8,3 89120571392 0 /var/log/nginx/access.log (deleted)
# nginx 1234 root 6w REG 8,3 1073741824 0 /var/log/nginx/error.log (deleted)
# 89120571392 字节 = 83GB,与差值吻合
# 3. 释放空间(不重启 Nginx)
# 方法 A:截断文件描述符
sudo truncate -s 0 /proc/1234/fd/5
sudo truncate -s 0 /proc/1234/fd/6
# 方法 B:重新打开日志文件(推荐)
# 先创建新的日志文件
sudo touch /var/log/nginx/access.log
sudo touch /var/log/nginx/error.log
sudo chown www-data:adm /var/log/nginx/access.log /var/log/nginx/error.log
# 发送 USR1 信号让 Nginx 重新打开日志
sudo kill -USR1 $(cat /run/nginx.pid)
# 4. 验证
df -Th /var
# Filesystem Type Size Used Avail Use% Mounted on
# /dev/sda3 ext4 100G 12G 83G 13% /var
sudo lsof +L1 | grep nginx
# (无输出,确认已释放)
根本原因和教训: 直接 rm 正在被进程写入的日志文件是错误操作。正确做法是使用 logrotate 或 truncate。Nginx 的日志切割标准流程是:mv 旧文件 -> 创建新文件 -> kill -USR1 通知 Nginx 重新打开文件描述符。
案例三:小文件海量堆积导致 inode 耗尽
场景描述: PHP 应用的 session 文件堆积在 /var/lib/php/sessions/ 目录下,数量达到 650 万个,inode 耗尽。
排查过程:
# 1. 确认 inode 耗尽
df -i /
# Filesystem Inodes IUsed IFree IUse% Mounted on
# /dev/sda3 6553600 6553597 3 100% /
# 2. 定位 inode 消耗大户
for dir in /var/lib/*; do
[ -d "$dir" ] || continue
count=$(sudo find "$dir" -xdev -type f 2>/dev/null | wc -l)
[ "$count" -gt 1000 ] && echo"$count$dir"
done | sort -rn | head -5
# 6512345 /var/lib/php
# 23456 /var/lib/dpkg
# 12345 /var/lib/apt
# 3. 确认具体目录
sudo ls /var/lib/php/sessions/ | head -5
# sess_a1b2c3d4e5f6g7h8i9j0
# sess_b2c3d4e5f6g7h8i9j0k1
# ...
sudo ls /var/lib/php/sessions/ | wc -l
# 6489123
# 4. 查看文件时间分布
sudo find /var/lib/php/sessions/ -type f -mtime +30 | wc -l
# 6234567(30 天前的文件占绝大多数)
sudo find /var/lib/php/sessions/ -type f -mtime -1 | wc -l
# 12345(最近 1 天的文件)
# 5. 批量清理(分批执行,避免 I/O 风暴影响业务)
# 第一批:删除 90 天前的文件
sudo find /var/lib/php/sessions/ -type f -mtime +90 -print0 | \
xargs -0 -r -P 4 -n 5000 rm -f
# 等待 I/O 恢复后继续
# 第二批:删除 30 天前的文件
sudo find /var/lib/php/sessions/ -type f -mtime +30 -print0 | \
xargs -0 -r -P 4 -n 5000 rm -f
# 6. 验证 inode 释放
df -i /
# Filesystem Inodes IUsed IFree IUse% Mounted on
# /dev/sda3 6553600 323456 6230144 5% /
长效措施:
# 配置 PHP session 自动清理
# 编辑 php.ini
sudo grep -n "session.gc" /etc/php/8.3/fpm/php.ini
# session.gc_probability = 1
# session.gc_divisor = 1000
# session.gc_maxlifetime = 1440
# 更激进的清理策略
sudo sed -i 's/session.gc_probability = 1/session.gc_probability = 1/' /etc/php/8.3/fpm/php.ini
sudo sed -i 's/session.gc_divisor = 1000/session.gc_divisor = 100/' /etc/php/8.3/fpm/php.ini
# 添加 cron 任务定期清理过期 session
cat > /etc/cron.d/php-session-cleanup << 'CRONEOF'
# 每 30 分钟清理超过 24 小时的 session 文件
*/30 * * * * root find /var/lib/php/sessions/ -type f -mmin +1440 -delete 2>/dev/null
CRONEOF
# 考虑将 session 存储迁移到 Redis
# redis-server 提供自动过期机制,彻底解决 session 文件堆积问题
3.2.1 自动化磁盘巡检脚本
#!/bin/bash
# 文件名:/opt/scripts/disk_patrol.sh
# 功能:自动化磁盘空间巡检,生成报告并发送告警
# 用法:通过 cron 每日执行
# 依赖:mailutils 或配置好的 SMTP
set -euo pipefail
# === 配置区 ===
ALERT_THRESHOLD_BLOCK=85 # block 使用率告警阈值(%)
ALERT_THRESHOLD_INODE=80 # inode 使用率告警阈值(%)
ALERT_THRESHOLD_DELETED=5 # 已删除未释放空间告警阈值(GB)
REPORT_DIR="/var/log/disk_patrol"
ALERT_EMAIL="ops-team@company.com"
HOSTNAME=$(hostname -f)
DATE=$(date '+%Y-%m-%d %H:%M:%S')
REPORT_FILE="${REPORT_DIR}/report_$(date '+%Y%m%d').txt"
mkdir -p "$REPORT_DIR"
# === 函数定义 ===
log() {
echo"[$(date '+%H:%M:%S')] $*" | tee -a "$REPORT_FILE"
}
alert() {
local level="$1"
local message="$2"
log"[${level}] ${message}"
# 写入告警文件
echo"${DATE}${level}${message}" >> "${REPORT_DIR}/alerts.log"
}
# === 主检查逻辑 ===
log"========================================="
log"磁盘巡检报告 - ${HOSTNAME}"
log"时间: ${DATE}"
log"========================================="
# 1. Block 空间检查
log""
log"--- 1. Block 空间使用 ---"
ALERT_FLAG=0
while IFS= read -r line; do
fs=$(echo"$line" | awk '{print $1}')
mount=$(echo"$line" | awk '{print $6}')
use_pct=$(echo"$line" | awk '{print $5}' | tr -d '%')
size=$(echo"$line" | awk '{print $2}')
used=$(echo"$line" | awk '{print $3}')
avail=$(echo"$line" | awk '{print $4}')
log"$fs ($mount): ${use_pct}% 已用 (总计: $size, 已用: $used, 可用: $avail)"
if [ "$use_pct" -ge "$ALERT_THRESHOLD_BLOCK" ]; then
alert "WARN""Block 空间告警: $mount 使用率 ${use_pct}% (阈值 ${ALERT_THRESHOLD_BLOCK}%)"
ALERT_FLAG=1
fi
done < <(df -Th | grep -v tmpfs | grep -v devtmpfs | tail -n +2)
# 2. Inode 检查
log""
log"--- 2. Inode 使用 ---"
while IFS= read -r line; do
fs=$(echo"$line" | awk '{print $1}')
mount=$(echo"$line" | awk '{print $5}')
use_pct=$(echo"$line" | awk '{print $4}' | tr -d '%')
# 跳过无 inode 信息的文件系统
[ "$use_pct" = "-" ] && continue
log"$fs ($mount): Inode 使用率 ${use_pct}%"
if [ "$use_pct" -ge "$ALERT_THRESHOLD_INODE" ]; then
alert "WARN""Inode 告警: $mount 使用率 ${use_pct}% (阈值 ${ALERT_THRESHOLD_INODE}%)"
ALERT_FLAG=1
fi
done < <(df -i | grep -v tmpfs | grep -v devtmpfs | tail -n +2)
# 3. 已删除未释放文件检查
log""
log"--- 3. 已删除未释放文件 ---"
DELETED_TOTAL=0
while IFS= read -r line; do
size_bytes=$(echo"$line" | awk '{print $7}')
proc_name=$(echo"$line" | awk '{print $1}')
pid=$(echo"$line" | awk '{print $2}')
filename=$(echo"$line" | awk '{print $9}')
if [ -n "$size_bytes" ] && [ "$size_bytes" -gt 104857600 ] 2>/dev/null; then
size_mb=$((size_bytes / 1024 / 1024))
log" 进程 $proc_name (PID $pid): ${size_mb} MB - $filename"
DELETED_TOTAL=$((DELETED_TOTAL + size_bytes))
fi
done < <(sudo lsof +L1 2>/dev/null | grep -i deleted)
DELETED_GB=$((DELETED_TOTAL / 1024 / 1024 / 1024))
log"已删除未释放文件总计: ${DELETED_GB} GB"
if [ "$DELETED_GB" -ge "$ALERT_THRESHOLD_DELETED" ]; then
alert "WARN""已删除未释放空间: ${DELETED_GB} GB (阈值 ${ALERT_THRESHOLD_DELETED} GB)"
ALERT_FLAG=1
fi
# 4. 大文件 TOP10
log""
log"--- 4. 大文件 TOP10 (>500MB) ---"
sudo find / -xdev -type f -size +500M -printf'%s %u %g %p\n' 2>/dev/null | \
sort -rn | head -10 | whileread size user group path; do
size_mb=$((size / 1024 / 1024))
log" ${size_mb} MB ${user}:${group}${path}"
done
# 5. 最近 24 小时增长最快的文件
log""
log"--- 5. 最近 24 小时修改的大文件 (>100MB) ---"
sudo find / -xdev -type f -size +100M -mtime -1 -printf'%s %T+ %p\n' 2>/dev/null | \
sort -rn | head -10 | whileread size mtime path; do
size_mb=$((size / 1024 / 1024))
log" ${size_mb} MB ${mtime}${path}"
done
# 6. ext4 预留空间检查
log""
log"--- 6. ext4 预留空间 ---"
for dev in $(df --output=source -t ext4 2>/dev/null | tail -n +2); do
reserved=$(sudo tune2fs -l "$dev" 2>/dev/null | grep "Reserved block count" | awk '{print $NF}')
total=$(sudo tune2fs -l "$dev" 2>/dev/null | grep "^Block count" | awk '{print $NF}')
bsize=$(sudo tune2fs -l "$dev" 2>/dev/null | grep "Block size" | awk '{print $NF}')
if [ -n "$reserved" ] && [ -n "$total" ] && [ -n "$bsize" ]; then
pct=$(echo"scale=1; $reserved * 100 / $total" | bc)
reserved_gb=$(echo"scale=1; $reserved * $bsize / 1024 / 1024 / 1024" | bc)
log" $dev: 预留 ${pct}% (${reserved_gb} GB)"
fi
done
# === 报告完成 ===
log""
log"========================================="
log"巡检完成"
log"========================================="
# 发送告警邮件(如果有告警)
if [ "$ALERT_FLAG" -eq 1 ]; then
ifcommand -v mail &>/dev/null; then
mail -s "[磁盘告警] ${HOSTNAME}$(date '+%Y-%m-%d')""$ALERT_EMAIL" < "$REPORT_FILE"
fi
fi
# 清理 30 天前的巡检报告
find "$REPORT_DIR" -name "report_*.txt" -mtime +30 -delete 2>/dev/null
exit 0
# 配置 cron 定时执行
cat > /etc/cron.d/disk-patrol << 'EOF'
# 每天早上 8 点和下午 4 点执行磁盘巡检
0 8,16 * * * root /opt/scripts/disk_patrol.sh
EOF
chmod +x /opt/scripts/disk_patrol.sh
四、最佳实践和注意事项
4.1 最佳实践
4.1.1 性能优化
磁盘 I/O 调度器选择:
Linux 内核 6.12 默认使用 mq-deadline 或 bfq 调度器。对于 NVMe SSD,推荐 none(直通模式);对于 SATA SSD 和 HDD,推荐 mq-deadline。
# 查看当前 I/O 调度器
cat /sys/block/sda/queue/scheduler
# [mq-deadline] kyber bfq none
# 临时切换(重启失效)
echo"none" | sudo tee /sys/block/nvme0n1/queue/scheduler
# 永久生效:通过 udev 规则
cat > /etc/udev/rules.d/60-io-scheduler.rules << 'EOF'
# NVMe SSD 使用 none
ACTION=="add|change", KERNEL=="nvme[0-9]*", ATTR{queue/scheduler}="none"
# SATA SSD 使用 mq-deadline
ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="mq-deadline"
# HDD 使用 bfq
ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="1", ATTR{queue/scheduler}="bfq"
EOF
sudo udevadm control --reload-rules
文件系统挂载优化:
# ext4 优化挂载选项(/etc/fstab)
# noatime: 不更新访问时间,减少写入
# commit=60: 每 60 秒提交一次 journal(默认 5 秒)
# barrier=0: 关闭写屏障(仅在有 UPS 或电池保护的 RAID 卡时使用)
/dev/sdb1 /data ext4 defaults,noatime,commit=60 0 2
# xfs 优化挂载选项
/dev/sdc1 /storage xfs defaults,noatime,logbufs=8,logbsize=256k 0 2
定期 fstrim(SSD 必须):
# 启用定时 TRIM
sudo systemctl enable fstrim.timer
sudo systemctl start fstrim.timer
# 查看定时器状态
systemctl status fstrim.timer
# 手动执行一次 TRIM
sudo fstrim -av
4.1.2 安全加固
限制用户磁盘配额:
# 安装 quota 工具
sudo apt install -y quota
# 编辑 /etc/fstab,在挂载选项中添加 usrquota,grpquota
# /dev/sdb1 /data ext4 defaults,noatime,usrquota,grpquota 0 2
# 重新挂载
sudo mount -o remount /data
# 初始化 quota 数据库
sudo quotacheck -cugm /data
# 启用 quota
sudo quotaon /data
# 为用户设置配额(soft: 50GB, hard: 60GB)
sudo setquota -u appuser 50G 60G 0 0 /data
# 查看配额使用情况
sudo repquota -as /data
配置 tmpwatch/systemd-tmpfiles 自动清理:
# 创建自定义 tmpfiles 规则
cat > /etc/tmpfiles.d/cleanup.conf << 'EOF'
# 清理 /tmp 中超过 3 天的文件
q /tmp 1777 root root 3d
# 清理应用临时文件目录中超过 1 天的文件
e /var/tmp/myapp - - - 1d
# 清理 core dump 文件,超过 7 天的删除
e /var/crash - - - 7d
EOF
# 验证配置
sudo systemd-tmpfiles --dry-run --clean
4.1.3 高可用配置
LVM 薄置备(Thin Provisioning):
# 创建薄池(在 VG 中分配 500GB 作为薄池)
sudo lvcreate -L 500G --thinpool thinpool vg_data
# 创建薄卷(虚拟大小 1TB,实际按需分配)
sudo lvcreate -V 1T --thin -n app_data vg_data/thinpool
# 查看薄池使用率
sudo lvs -o+lv_layout,pool_lv,data_percent,metadata_percent vg_data
# 薄池使用率超过 80% 时自动扩展
# 编辑 /etc/lvm/lvm.conf
# thin_pool_autoextend_threshold = 80
# thin_pool_autoextend_percent = 20
自动扩容方案(LVM + 监控告警联动):
#!/bin/bash
# 文件名:/opt/scripts/auto_extend_lv.sh
# 功能:当逻辑卷使用率超过阈值时自动扩容
# 注意:仅适用于 VG 中有剩余空间的场景
THRESHOLD=90
LV_PATH="/dev/vg_data/lv_data"
MOUNT_POINT="/data"
EXTEND_SIZE="10G"
CURRENT_USE=$(df --output=pcent "$MOUNT_POINT" | tail -1 | tr -d ' %')
if [ "$CURRENT_USE" -ge "$THRESHOLD" ]; then
# 检查 VG 剩余空间
VG_FREE=$(sudo vgs --noheadings -o vg_free --units g vg_data | tr -d ' ')
if [ "$(echo "$VG_FREE" | tr -d 'g' | cut -d. -f1)" -ge 10 ]; then
echo"$(date): 使用率 ${CURRENT_USE}%,自动扩容 ${EXTEND_SIZE}"
# 扩展 LV
sudo lvextend -L +${EXTEND_SIZE}${LV_PATH}
# 扩展文件系统
FS_TYPE=$(df -T "$MOUNT_POINT" | tail -1 | awk '{print $2}')
if [ "$FS_TYPE" = "ext4" ]; then
sudo resize2fs ${LV_PATH}
elif [ "$FS_TYPE" = "xfs" ]; then
sudo xfs_growfs ${MOUNT_POINT}
fi
echo"扩容完成,当前状态:"
df -Th "$MOUNT_POINT"
else
echo"$(date): VG 剩余空间不足 (${VG_FREE}),无法自动扩容"
fi
fi
4.2 注意事项
4.2.1 配置注意事项
在处理磁盘空间问题时,以下几点必须牢记:
永远不要在根分区满的时候执行 apt/dnf 更新操作。 包管理器需要临时空间来下载和解压包,磁盘满的情况下更新可能导致系统包损坏。
truncate 和重定向的区别很关键。truncate -s 0 file 是原子操作,不会创建新的 inode;而 echo "" > file 可能在高并发写入时出问题。
*删除大量小文件时必须用 xargs 而不是直接 rm 。 当文件数量超过 shell 的 ARG_MAX 限制(通常 2097152 字节)时,rm * 会报 "Argument list too long" 错误。
LVM 快照会消耗原始卷的空间。 创建 LVM 快照后,原始卷的每次写入都会触发 CoW(Copy on Write),如果快照空间不足会导致快照失效甚至原始卷只读。
btrfs 的 df 输出具有欺骗性。 btrfs 的 CoW 机制导致 du 和 df 的数据可能与实际使用量差距很大,需要使用 btrfs filesystem usage /mount 查看真实使用情况。
Docker overlay2 驱动的空间计算。 容器删除后,overlay2 层可能仍然存在。需要使用 docker system df -v 而不是 du 来准确评估 Docker 的空间占用。
4.2.2 常见错误
| | |
|---|
| | 使用 lsof +L1 找到持有进程,truncate fd 或重启进程 |
| | |
| | 检查 postrotate 脚本,确保发送了正确的信号 |
| resize2fs 报错 "Nothing to do" | | 使用 partprobe 或 kpartx 刷新分区表 |
| | 改用 truncate -s 0 再 rm,或在低峰期操作 |
| | |
| | 使用 df 的 --sync 选项或在 NFS 服务端查看 |
4.2.3 兼容性问题
ext4 vs xfs 的 reserved blocks: ext4 有 5% 预留空间,xfs 没有。迁移文件系统时需要考虑这个差异对可用空间的影响。
Ubuntu vs Rocky Linux 的 logrotate 默认配置: Ubuntu 默认对 /var/log/syslog 配置了 logrotate,Rocky Linux 使用 rsyslog 的内建切割。两者的配置路径和参数格式有细微差异。
systemd 256+ 的 tmpfiles 变化: systemd 256 引入了 tmpfiles 的新指令 C+(递归复制)和 R(递归删除),在编写清理规则时需要参考新版文档。
ncdu 2.x 与 1.x 的差异: ncdu 2.x(Rust 版)移除了 -x 参数(已改为默认行为),使用 --cross-file-system 启用跨文件系统扫描。旧版 shell 脚本中的 ncdu -x 需要更新。
内核 6.12 的 FUSE 性能改进: 使用 FUSE 挂载的文件系统(如 sshfs、s3fs)在新内核上性能显著提升,但 df/du 的行为可能与本地文件系统有差异。
五、故障排查和监控
5.1 故障排查
5.1.1 日志查看
# 查看系统磁盘相关日志
sudo journalctl -k | grep -iE "disk|storage|ext4|xfs|block|full"
# 查看特定时间段的 I/O 错误
sudo journalctl --since "2026-03-12" --until "2026-03-13" | grep -i "I/O error"
# 查看文件系统挂载日志
sudo dmesg | grep -iE "mount|ext4|xfs|filesystem"
# 查看 OOM killer 日志(磁盘满可能导致 OOM)
sudo journalctl | grep -i "out of memory"
# 查看 logrotate 执行日志
cat /var/lib/logrotate/status
sudo journalctl -u logrotate.service
5.1.2 常见问题排查
问题一:磁盘使用率 100% 但找不到大文件
# 诊断步骤
# 1. 检查所有挂载点(包括绑定挂载)
findmnt -t ext4,xfs,btrfs
# 2. 检查是否有目录被其他文件系统覆盖挂载
# 例如:/data 原本有数据,后来挂载了新分区覆盖了原目录
mount | grep "/data"
# 3. 临时卸载覆盖的挂载点查看底层数据
sudo mkdir /tmp/check_mount
sudo mount --bind / /tmp/check_mount
ls -la /tmp/check_mount/data/
du -sh /tmp/check_mount/data/
sudo umount /tmp/check_mount
# 4. 检查挂载命名空间(容器环境)
sudo nsenter -t 1 -m -- df -Th
问题二:logrotate 不生效
# 诊断步骤
# 1. 手动执行并查看详细输出
sudo logrotate -dv /etc/logrotate.d/app-service 2>&1
# 2. 检查日志文件权限
ls -la /var/log/app/
# 确认 logrotate 运行用户是否有读写权限
# 3. 检查 SELinux 上下文(Rocky Linux)
ls -Z /var/log/app/
# 确认文件的 SELinux 类型是否正确
sudo restorecon -Rv /var/log/app/
# 4. 检查 logrotate 状态文件
cat /var/lib/logrotate/status | grep app
# 确认上次切割时间是否正常
# 5. 检查 logrotate 定时器
systemctl status logrotate.timer
systemctl list-timers | grep logrotate
# 6. 常见原因:配置文件中有语法错误
sudo logrotate -d /etc/logrotate.conf 2>&1 | grep -i error
问题三:容器环境中宿主机磁盘被耗尽
# 1. 查看 Docker 整体空间占用
sudo docker system df -v
# 输出示例:
# TYPE TOTAL ACTIVE SIZE RECLAIMABLE
# Images 45 12 18.23GB 12.45GB (68%)
# Containers 23 8 5.67GB 3.21GB (56%)
# Local Volumes 15 5 45.67GB 38.90GB (85%)
# Build Cache 0 0 0B 0B
# 2. 找到占用空间最大的容器
sudo docker ps -a --size --format "table {{.ID}}\t{{.Names}}\t{{.Size}}" | sort -k3 -rh
# 3. 查看容器日志大小
for cid in $(sudo docker ps -q); do
name=$(sudo docker inspect --format '{{.Name}}'"$cid" | sed 's/\///')
logfile=$(sudo docker inspect --format '{{.LogPath}}'"$cid")
if [ -f "$logfile" ]; then
size=$(du -sh "$logfile" | awk '{print $1}')
echo"$size$name$logfile"
fi
done | sort -rh
# 4. 限制容器日志大小(daemon.json)
cat > /etc/docker/daemon.json << 'EOF'
{
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "3"
},
"storage-driver": "overlay2"
}
EOF
sudo systemctl reload docker
# 5. 清理未使用的 Docker 资源
sudo docker system prune -a --volumes --filter "until=168h"
5.1.3 调试模式
# ext4 文件系统调试
sudo debugfs /dev/sda3
# 进入 debugfs 交互模式后:
# stats - 查看文件系统统计信息
# ls -l / - 列出根目录
# stat <inode号> - 查看特定 inode 的信息
# quit - 退出
# xfs 文件系统调试
sudo xfs_db /dev/sdb1
# freesp - 查看空闲空间分布
# sb 0 - 查看超级块
# quit - 退出
# 查看文件系统的详细信息
sudo dumpe2fs /dev/sda3 | head -50
sudo xfs_info /data
# strace 跟踪磁盘写入(定位哪个进程在大量写入)
# 找到可疑进程后:
sudo strace -p <PID> -e write -c
# 统计 write 系统调用的频率和数据量
# iotop 实时监控磁盘 I/O
sudo iotop -oP
# -o: 只显示有 I/O 的进程
# -P: 显示进程而不是线程
5.2 性能监控
5.2.1 关键指标监控
# 使用 node_exporter 采集磁盘指标
# node_exporter 默认采集以下文件系统指标:
# node_filesystem_size_bytes - 分区总大小
# node_filesystem_avail_bytes - 可用空间
# node_filesystem_files - inode 总数
# node_filesystem_files_free - 可用 inode 数
# node_filesystem_readonly - 是否只读
# 安装 node_exporter
wget https://github.com/prometheus/node_exporter/releases/download/v1.9.0/node_exporter-1.9.0.linux-amd64.tar.gz
tar xzf node_exporter-1.9.0.linux-amd64.tar.gz
sudo cp node_exporter-1.9.0.linux-amd64/node_exporter /usr/local/bin/
# 创建 systemd service
sudo tee /etc/systemd/system/node_exporter.service << 'EOF'
[Unit]
Description=Prometheus Node Exporter
After=network-online.target
[Service]
Type=simple
User=node_exporter
Group=node_exporter
ExecStart=/usr/local/bin/node_exporter \
--collector.filesystem.mount-points-exclude="^/(sys|proc|dev|host|etc)($$|/)" \
--collector.diskstats.device-exclude="^(ram|loop|fd|dm-)\d+$$"
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
sudo useradd -rs /sbin/nologin node_exporter
sudo systemctl daemon-reload
sudo systemctl enable --now node_exporter
# 验证指标采集
curl -s http://localhost:9100/metrics | grep node_filesystem_avail_bytes
| | | |
|---|
| | Warning: 80%, Critical: 90% | 基于 node_filesystem_avail_bytes 计算 |
| | Warning: 80%, Critical: 95% | 基于 node_filesystem_files_free 计算 |
| | Warning: 80%, Critical: 95% | 基于 node_disk_io_time_seconds_total |
| | | 基于 node_disk_written_bytes_total |
| | | 基于 node_disk_io_time_weighted_seconds_total |
| | | |
5.2.2 Prometheus 告警规则
# 文件路径:/etc/prometheus/rules/disk_alerts.yml
groups:
-name:disk_space_alerts
interval:60s
rules:
# 磁盘空间使用率超过 80%(Warning)
-alert:DiskSpaceWarning
expr:|
100 - (
node_filesystem_avail_bytes{fstype=~"ext4|xfs|btrfs"}
/ node_filesystem_size_bytes{fstype=~"ext4|xfs|btrfs"}
* 100
) > 80
for:10m
labels:
severity:warning
annotations:
summary:"磁盘空间告警 ({{ $labels.instance }})"
description:>
挂载点 {{ $labels.mountpoint }} 使用率
{{ printf "%.1f" $value }}%,超过 80% 阈值。
# 磁盘空间使用率超过 90%(Critical)
-alert:DiskSpaceCritical
expr:|
100 - (
node_filesystem_avail_bytes{fstype=~"ext4|xfs|btrfs"}
/ node_filesystem_size_bytes{fstype=~"ext4|xfs|btrfs"}
* 100
) > 90
for:5m
labels:
severity:critical
annotations:
summary:"磁盘空间严重不足 ({{ $labels.instance }})"
description:>
挂载点 {{ $labels.mountpoint }} 使用率
{{ printf "%.1f" $value }}%,需要立即处理。
# Inode 使用率超过 80%
-alert:InodeUsageHigh
expr:|
100 - (
node_filesystem_files_free{fstype=~"ext4|xfs|btrfs"}
/ node_filesystem_files{fstype=~"ext4|xfs|btrfs"}
* 100
) > 80
for:15m
labels:
severity:warning
annotations:
summary:"Inode 使用率告警 ({{ $labels.instance }})"
description:>
挂载点 {{ $labels.mountpoint }} Inode 使用率
{{ printf "%.1f" $value }}%。
# 预测磁盘 7 天内将满
-alert:DiskWillFillIn7Days
expr:|
predict_linear(
node_filesystem_avail_bytes{fstype=~"ext4|xfs|btrfs"}[7d],
7 * 24 * 3600
) < 0
for:1h
labels:
severity:warning
annotations:
summary:"磁盘空间预计 7 天内耗尽 ({{ $labels.instance }})"
description:>
基于过去 7 天的增长趋势,挂载点 {{ $labels.mountpoint }}
预计将在 7 天内耗尽空间。
# 磁盘 I/O 利用率过高
-alert:DiskIOHigh
expr:|
rate(node_disk_io_time_seconds_total[5m]) * 100 > 80
for:15m
labels:
severity:warning
annotations:
summary:"磁盘 I/O 利用率过高 ({{ $labels.instance }})"
description:>
设备 {{ $labels.device }} I/O 利用率
{{ printf "%.1f" $value }}%,持续 15 分钟。
5.2.3 Grafana 面板配置
{
"dashboard": {
"title": "磁盘空间监控",
"panels": [
{
"title": "各分区使用率",
"type": "gauge",
"targets": [
{
"expr": "100 - (node_filesystem_avail_bytes{fstype=~\"ext4|xfs|btrfs\"} / node_filesystem_size_bytes{fstype=~\"ext4|xfs|btrfs\"} * 100)",
"legendFormat": "{{ instance }} - {{ mountpoint }}"
}
],
"fieldConfig": {
"defaults": {
"thresholds": {
"steps": [
{"color": "green", "value": 0},
{"color": "yellow", "value": 75},
{"color": "orange", "value": 85},
{"color": "red", "value": 95}
]
},
"unit": "percent",
"max": 100
}
}
},
{
"title": "磁盘空间趋势(7天)",
"type": "timeseries",
"targets": [
{
"expr": "node_filesystem_avail_bytes{fstype=~\"ext4|xfs|btrfs\"} / 1024 / 1024 / 1024",
"legendFormat": "{{ instance }} - {{ mountpoint }}"
}
],
"fieldConfig": {
"defaults": {
"unit": "decgbytes",
"custom": {
"drawStyle": "line",
"fillOpacity": 10
}
}
}
},
{
"title": "Inode 使用率",
"type": "stat",
"targets": [
{
"expr": "100 - (node_filesystem_files_free{fstype=~\"ext4|xfs\"} / node_filesystem_files{fstype=~\"ext4|xfs\"} * 100)",
"legendFormat": "{{ mountpoint }}"
}
]
},
{
"title": "磁盘 I/O 利用率",
"type": "timeseries",
"targets": [
{
"expr": "rate(node_disk_io_time_seconds_total[5m]) * 100",
"legendFormat": "{{ device }}"
}
],
"fieldConfig": {
"defaults": {
"unit": "percent",
"max": 100
}
}
}
]
}
}
5.3 备份与恢复
5.3.1 备份策略
#!/bin/bash
# 文件名:/opt/scripts/disk_config_backup.sh
# 功能:备份磁盘分区和文件系统配置信息
# 用途:在磁盘故障或误操作后可以参考恢复
BACKUP_DIR="/var/backup/disk_config"
DATE=$(date '+%Y%m%d')
mkdir -p "${BACKUP_DIR}/${DATE}"
# 备份分区表
for disk in $(lsblk -dno NAME | grep -v loop); do
sudo sfdisk -d "/dev/$disk" > "${BACKUP_DIR}/${DATE}/${disk}_partition.dump" 2>/dev/null
sudo sgdisk --backup="${BACKUP_DIR}/${DATE}/${disk}_gpt.bin""/dev/$disk" 2>/dev/null
done
# 备份 LVM 配置
sudo vgcfgbackup -f "${BACKUP_DIR}/${DATE}/lvm_vg_%s.conf" 2>/dev/null
sudo pvs > "${BACKUP_DIR}/${DATE}/pvs.txt" 2>/dev/null
sudo vgs > "${BACKUP_DIR}/${DATE}/vgs.txt" 2>/dev/null
sudo lvs > "${BACKUP_DIR}/${DATE}/lvs.txt" 2>/dev/null
# 备份 fstab
cp /etc/fstab "${BACKUP_DIR}/${DATE}/fstab.bak"
# 备份文件系统参数
for dev in $(df --output=source | tail -n +2 | grep "^/dev"); do
devname=$(basename "$dev")
fstype=$(df -T "$dev" | tail -1 | awk '{print $2}')
if [ "$fstype" = "ext4" ]; then
sudo tune2fs -l "$dev" > "${BACKUP_DIR}/${DATE}/${devname}_tune2fs.txt" 2>/dev/null
elif [ "$fstype" = "xfs" ]; then
sudo xfs_info "$(df --output=target "$dev" | tail -1)" > "${BACKUP_DIR}/${DATE}/${devname}_xfs_info.txt" 2>/dev/null
fi
done
# 备份 logrotate 配置
cp -r /etc/logrotate.d/ "${BACKUP_DIR}/${DATE}/logrotate.d/"
cp /etc/logrotate.conf "${BACKUP_DIR}/${DATE}/logrotate.conf"
# 保留 90 天的备份
find "$BACKUP_DIR" -maxdepth 1 -type d -mtime +90 -exec rm -rf {} \;
echo"备份完成: ${BACKUP_DIR}/${DATE}/"
ls -la "${BACKUP_DIR}/${DATE}/"
5.3.2 恢复流程
# 从备份恢复 MBR 分区表
sudo sfdisk /dev/sda < /var/backup/disk_config/20260313/sda_partition.dump
# 从备份恢复 GPT 分区表
sudo sgdisk --load-backup=/var/backup/disk_config/20260313/sda_gpt.bin /dev/sda
# 通知内核重新读取分区表
sudo partprobe /dev/sda
# 恢复 VG 配置
sudo vgcfgrestore -f /var/backup/disk_config/20260313/lvm_vg_vg_data.conf vg_data
# 激活 VG
sudo vgchange -ay vg_data
sudo cp /var/backup/disk_config/20260313/fstab.bak /etc/fstab
sudo mount -a
lsblk -f
df -Th
mount | grep -v cgroup
六、总结
6.1 技术要点回顾
- 磁盘空间问题分为五大类:block 耗尽、inode 耗尽、已删除未释放、预留空间占用、文件系统异常。排查前先用快速诊断脚本判断类型,再针对性处理。
- df/du/lsof 三板斧是基础。df 看全局,du 逐层下钻,lsof +L1 抓已删除未释放文件。三个工具组合使用覆盖 95% 以上的磁盘空间问题。
- logrotate 是日志文件膨胀的长效解决方案。核心参数包括切割频率、保留数量、压缩方式、maxsize 限制。postrotate 脚本必须正确通知应用重新打开文件描述符。
- ext4 的 5% 预留空间在大容量数据分区上造成浪费,可通过 tune2fs -m 调整。xfs 无此问题。
- 监控先行:通过 node_exporter + Prometheus 的 predict_linear 函数预测磁盘增长趋势,在空间耗尽前 7 天发出预警,变被动救火为主动预防。
6.2 进阶学习方向
btrfs 和 ZFS 的空间管理: 这两个现代文件系统支持透明压缩、去重、快照等高级特性,空间管理机制与 ext4/xfs 完全不同。btrfs 在 SUSE 系发行版中已是默认文件系统,ZFS 在存储服务器上广泛使用。
Ceph 分布式存储的空间治理: 大规模集群环境中,单机磁盘管理只是基础。Ceph 的 PG(Placement Group)分布、OSD 空间均衡、CRUSH 规则优化等是进阶主题。
eBPF 文件系统追踪: 使用 bcc/bpftrace 工具可以在内核层面追踪文件写入行为,精确到进程级别的 I/O 分析,定位"谁在持续写入大量数据"的问题。
6.3 参考资料
- Linux 内核文档 - ext4 文件系统:https://www.kernel.org/doc/html/latest/filesystems/ext4/
- Linux 内核文档 - xfs 文件系统:https://www.kernel.org/doc/html/latest/admin-guide/xfs.html
- logrotate 手册页:https://man7.org/linux/man-pages/man8/logrotate.8.html
- Prometheus node_exporter GitHub:https://github.com/prometheus/node_exporter
- ncdu 2.x 文档:https://dev.yorhel.nl/ncdu
附录
A. 命令速查表
# === 空间查看 ===
df -Th # 查看所有分区使用率(含文件系统类型)
df -i # 查看 inode 使用率
du -sh /path/* | sort -rh | head -10 # 查看目录大小排名
ncdu /path # 交互式空间分析
lsblk -f # 查看块设备和文件系统信息
# === 大文件定位 ===
find / -xdev -type f -size +1G -ls # 查找超过 1GB 的文件
find / -xdev -type f -mtime -1 -size +100M # 最近 24 小时修改的大文件
# === 已删除文件 ===
lsof +L1 # 列出已删除但未释放的文件
truncate -s 0 /proc/<PID>/fd/<FD> # 截断进程持有的已删除文件
# === 清理操作 ===
truncate -s 0 /path/to/logfile # 截断日志文件(原子操作)
journalctl --vacuum-time=7d # 清理 7 天前的 journal 日志
docker system prune -a --volumes # 清理未使用的 Docker 资源
apt clean / dnf clean all # 清理包管理器缓存
# === 文件系统管理 ===
tune2fs -m 1 /dev/sdb1 # 调整 ext4 预留空间为 1%
tune2fs -l /dev/sdb1 # 查看 ext4 文件系统参数
xfs_info /mount_point # 查看 xfs 文件系统信息
resize2fs /dev/mapper/vg-lv # 在线扩展 ext4 文件系统
xfs_growfs /mount_point # 在线扩展 xfs 文件系统
# === logrotate ===
logrotate -d /etc/logrotate.d/app # dry-run 测试配置
logrotate -f /etc/logrotate.d/app # 强制执行切割
cat /var/lib/logrotate/status # 查看切割状态记录
B. 配置参数详解
logrotate 参数速查:
tune2fs 常用参数:
| | |
|---|
| | |
| | |
| | |
| | |
| | tune2fs -L "data" /dev/sdb1 |
| | tune2fs -U random /dev/sdb1 |
C. 术语表
| | |
|---|
| | 文件系统中存储文件元数据的数据结构,包含权限、时间戳、数据块指针等 |
| | 文件系统中存储实际数据的最小分配单元,通常为 4KB |
| | 进程用于访问文件的整数标识,通过 /proc/pid/fd/ 可查看 |
| | xfs 文件系统的并行管理单元,每个 AG 独立管理空间分配 |
| | ext4 用于管理文件数据块映射的 B 树结构,替代传统间接块指针 |
| | btrfs/ZFS 的核心机制,修改数据时写入新位置而不覆盖原数据 |
| | ext4 为 root 用户保留的磁盘空间,默认 5% |
| | |
| | |
| | 以固定大小块为单位进行 I/O 操作的设备,如硬盘、SSD |