Linux磁盘空间满了怎么办?这套排查流程救了我无数次
一、概述
1.1 背景介绍
磁盘空间问题是Linux服务器运维中最常见的问题之一,没有之一。根据我这些年的统计,在所有生产环境告警中,磁盘相关的告警占比大概在15%-20%左右。而且这类问题往往来得很突然——前一天还好好的,第二天早上磁盘就满了。
为什么磁盘问题这么常见?原因很简单:
- 应用程序可能存在内存泄漏,产生大量core dump
更要命的是,磁盘空间不足会引发连锁反应。数据库写不进去数据,应用程序抛异常,缓存服务挂掉,最后整个业务系统瘫痪。我见过太多P0级别的故障,根因追溯下来就是磁盘满了。
1.2 技术特点
磁盘空间排查看似简单,实际上涉及到Linux文件系统的很多底层知识:
存储空间 vs inode数量
这是很多新手容易忽略的点。磁盘空间有两个维度:一个是存储空间(blocks),另一个是inode数量。即使存储空间还有剩余,如果inode用完了,也无法创建新文件。我就遇到过这种情况:df -h显示还有30%的空间,但系统报"No space left on device"。查了半天才发现是inode耗尽了。
已删除但未释放的文件
这是另一个经典坑。用rm删除了一个大文件,但df显示空间没有释放。原因是还有进程在持有这个文件的文件描述符。文件虽然从目录项中删除了,但数据块并没有真正释放。
预留空间机制
ext4文件系统默认会预留5%的空间给root用户。这个设计是为了防止普通用户把磁盘写满后,root用户连登录都登不了。但在某些场景下,这5%的空间也是可以利用的。
1.3 适用场景
这套排查流程适用于以下场景:
- 应用程序报"No space left on device"错误
1.4 环境要求
本文的命令和配置基于以下环境测试:
操作系统: Ubuntu 22.04 LTS / CentOS 7.9 / Rocky Linux 9
文件系统: ext4 / xfs
内核版本: 5.15+
容器运行时: Docker 24.x / containerd 1.7.x
监控系统: Prometheus 2.47+ / Grafana 10.x
大部分命令在主流Linux发行版上都能正常使用,个别工具可能需要额外安装。
二、详细步骤
2.1 第一步:快速确认磁盘状态
收到告警后,第一件事是登录服务器,用df命令确认磁盘的整体使用情况:
df -h
输出示例:
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 100G 95G 5.0G 95% /
/dev/sdb1 500G 120G 380G 24% /data
tmpfs 7.8G 1.2M 7.8G 1% /run
/dev/sdc1 200G 200G 0 100% /var/log
看到这个输出,问题就很明显了:根分区使用了95%,/var/log分区已经100%满了。
但光看存储空间还不够,还要检查inode的使用情况:
df -i
输出示例:
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/sda1 6553600 6553598 2 100% /
/dev/sdb1 32768000 125000 32643000 1% /data
这个输出就更严重了——根分区的inode几乎用完了,只剩2个。这种情况下,即使还有5G的存储空间,也无法创建新文件。
2.2 第二步:定位大文件和目录
确认磁盘状态后,接下来要找出是什么东西占用了这么多空间。这里有几个工具可以用,我按实用程度排序介绍。
2.2.1 du命令(基础款)
du是Linux自带的命令,不需要额外安装。最常用的用法:
# 查看当前目录下各子目录的大小,按大小排序
du -sh /* 2>/dev/null | sort -rh | head -20
输出示例:
45G /var
32G /home
15G /opt
8.5G /usr
2.1G /root
发现/var目录占用最多,继续往下钻:
du -sh /var/* 2>/dev/null | sort -rh | head -10
输出:
38G /var/log
4.2G /var/lib
1.8G /var/cache
再往下:
du -sh /var/log/* 2>/dev/null | sort -rh | head -10
输出:
25G /var/log/nginx
8.5G /var/log/app
3.2G /var/log/syslog
1.1G /var/log/auth.log
找到了!nginx的日志占了25G。
du命令的问题
du命令有个很大的缺点:慢。在大目录下执行du,可能要等几分钟才能出结果。而且它没有交互式界面,每次都要重新敲命令,效率很低。
2.2.2 ncdu(进阶款,强烈推荐)
ncdu是我日常用得最多的磁盘分析工具,全称是NCurses Disk Usage。它有一个交互式的终端界面,可以快速浏览目录结构,支持排序和删除操作。
安装方式:
# Ubuntu/Debian
apt install ncdu
# CentOS/RHEL
yum install ncdu
# Rocky/Alma Linux
dnf install ncdu
使用方法:
# 分析根目录
ncdu /
# 分析指定目录
ncdu /var/log
# 排除某些目录(比如挂载的网络文件系统)
ncdu / --exclude /mnt --exclude /media
执行后会进入一个交互式界面:
ncdu 1.18 ~ Use the arrow keys to navigate, press ? forhelp
--- /var/log -------------------------------------------------------
25.1 GiB [##########] /nginx
8.5 GiB [### ] /app
3.2 GiB [# ] syslog
1.1 GiB [ ] auth.log
512.0 MiB [ ] /journal
128.0 MiB [ ] kern.log
Total disk usage: 38.4 GiB Apparent size: 38.2 GiB Items: 15234
在这个界面里,你可以用方向键上下移动,按回车进入子目录,按d删除文件或目录(会有确认提示),按q退出。
ncdu还有个很实用的功能:可以先扫描再查看。在生产环境,我一般会先把扫描结果导出到文件,然后慢慢分析:
# 扫描并导出结果
ncdu -o /tmp/ncdu-root.json /
# 后续直接加载结果,秒开
ncdu -f /tmp/ncdu-root.json
2.2.3 dust(现代款)
dust是用Rust写的du替代品,速度快,输出美观。它会用树状图直观显示目录大小占比。
安装方式:
# Ubuntu 22.04+
apt install dust
# 其他系统用cargo安装
cargo install du-dust
# 或者下载预编译二进制
wget https://github.com/bootandy/dust/releases/download/v1.0.0/dust-v1.0.0-x86_64-unknown-linux-gnu.tar.gz
tar xzf dust-v1.0.0-x86_64-unknown-linux-gnu.tar.gz
mv dust /usr/local/bin/
使用示例:
dust -d 2 /var
输出:
38G ┌── log │ ████████████ │ 82%
4.2G │ ┌── lib │ ██ │ 9%
1.8G │ ├── cache │ █ │ 4%
2.1G ├─┴ var │ ██ │ 5%
46G ┴ . │████████████████████████████████████████████│ 100%
dust的优点是输出很直观,一眼就能看出哪个目录占比最大。但它不支持交互式操作,所以我一般是用dust快速定位,然后用ncdu深入分析。
2.2.4 三款工具对比总结
我的使用习惯是:紧急情况用du快速定位,日常巡检用ncdu,给领导汇报用dust(因为图好看)。
2.3 第三步:检查已删除但未释放的文件
这是一个非常容易被忽略的问题。有时候你明明删了很多文件,但df显示空间没变化。这时候就要检查是否有进程还在持有已删除文件的句柄。
用lsof命令查找:
lsof +L1 2>/dev/null | head -20
输出示例:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NLINK NODE NAME
java 12345 app 25w REG 253,1 21474836480 0 12345 /var/log/app/debug.log (deleted)
nginx 23456 www 12w REG 253,1 5368709120 0 23456 /var/log/nginx/access.log (deleted)
看到了吧,有两个进程还在写已删除的文件。java进程持有一个20G的debug.log,nginx持有一个5G的access.log。这25G的空间虽然文件被删了,但实际并没有释放。
解决方法有两种:
方法一:重启进程(推荐)
# 重启应用
systemctl restart app
# 重载nginx(不需要完全重启)
nginx -s reopen
方法二:清空文件而不是删除
如果不能重启进程,可以用truncate清空文件内容:
# 进程还在运行时,用这种方式清空日志
truncate -s 0 /var/log/app/debug.log
# 或者用这种方式
> /var/log/app/debug.log
# 或者
cat /dev/null > /var/log/app/debug.log
这三种写法效果一样,都是把文件内容清空但保留文件本身。这样进程的文件句柄依然有效,空间立即释放。
方法三:强制释放(黑科技,慎用)
如果实在不能重启进程,还有一个黑科技——通过/proc文件系统直接truncate文件描述符:
# 假设进程PID是12345,文件描述符是25
: > /proc/12345/fd/25
这个方法我只在紧急情况下用过几次。原理是直接操作进程的文件描述符,把对应的文件内容清空。但这种操作有风险,可能导致进程行为异常,所以要谨慎。
2.4 第四步:处理inode耗尽问题
如果df -i显示inode使用率很高,说明系统中存在大量小文件。这种情况通常是以下原因导致的:
- 邮件队列堆积(/var/spool/postfix)
- Session文件堆积(/var/lib/php/sessions)
- 缓存文件碎片化(/tmp, /var/cache)
找出哪个目录的文件数量最多:
# 统计各目录的文件数量
for dir in /*; do
echo -n "$dir: "
find "$dir" -xdev -type f 2>/dev/null | wc -l
done | sort -t: -k2 -rn | head -10
输出示例:
/var: 1523456
/home: 234567
/usr: 123456
/opt: 45678
继续细查/var目录:
for dir in /var/*; do
echo -n "$dir: "
find "$dir" -xdev -type f 2>/dev/null | wc -l
done | sort -t: -k2 -rn | head -10
输出:
/var/spool: 1234567
/var/log: 156789
/var/lib: 89012
找到了,/var/spool目录下有120多万个文件。再往下查:
ls -la /var/spool/postfix/deferred/ | head -20
果然是邮件队列堆积了。清理方法:
# 查看邮件队列状态
postqueue -p
# 清空所有队列
postsuper -d ALL
# 如果是Sendmail
rm -rf /var/spool/mqueue/*
另一个常见的inode杀手是PHP session文件:
# 统计session文件数量
ls /var/lib/php/sessions | wc -l
# 清理过期的session
find /var/lib/php/sessions -type f -mtime +7 -delete
2.5 第五步:释放预留空间(救急用)
ext4文件系统默认预留5%的空间给root用户。对于大容量磁盘,这个比例可能太高了。比如1TB的磁盘,5%就是50GB,这个空间平时用不到有点浪费。
查看当前预留比例:
tune2fs -l /dev/sda1 | grep "Reserved block count"
修改预留比例:
# 将预留空间改为1%
tune2fs -m 1 /dev/sda1
# 或者直接设置预留的块数量
tune2fs -r 100000 /dev/sda1
注意:这个操作可以在文件系统挂载状态下执行,不需要卸载。但建议在业务低峰期操作。
对于XFS文件系统,预留空间的机制不太一样,一般不需要调整。
三、示例代码和配置
3.1 常见磁盘杀手处理
3.1.1 日志文件清理
日志文件是最常见的磁盘空间杀手。在清理之前,先确认哪些日志可以安全删除:
# 查找大于100MB的日志文件
find /var/log -type f -size +100M -exec ls -lh {} \; 2>/dev/null
# 查找超过7天的日志文件
find /var/log -type f -mtime +7 -name "*.log" -exec ls -lh {} \;
# 查找并删除超过30天的压缩日志
find /var/log -type f -mtime +30 -name "*.gz" -delete
对于正在被进程写入的日志,不要直接删除,用truncate清空:
# 清空nginx日志
truncate -s 0 /var/log/nginx/access.log
truncate -s 0 /var/log/nginx/error.log
# 通知nginx重新打开日志文件
nginx -s reopen
对于systemd journal日志:
# 查看journal占用空间
journalctl --disk-usage
# 只保留最近7天的日志
journalctl --vacuum-time=7d
# 只保留最近500MB的日志
journalctl --vacuum-size=500M
3.1.2 Docker镜像和容器清理
Docker是另一个磁盘空间大户。我见过很多服务器,/var/lib/docker目录占用上百GB。
# 查看Docker占用的磁盘空间
docker system df
# 输出示例:
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 45 12 12.5GB 8.2GB (65%)
Containers 28 8 2.3GB 1.8GB (78%)
Local Volumes 15 8 45.6GB 23.4GB (51%)
Build Cache 0 0 0B 0B
清理命令:
# 删除所有停止的容器
docker container prune -f
# 删除所有未使用的镜像
docker image prune -a -f
# 删除所有未使用的volume(危险,可能丢数据)
docker volume prune -f
# 一键清理所有未使用的资源
docker system prune -a -f --volumes
# 删除指定时间之前创建的镜像
docker image prune -a -f --filter "until=720h"
对于Kubernetes环境,还要清理containerd的数据:
# 查看crictl占用空间(containerd)
crictl images
crictl ps -a
# 清理未使用的镜像
crictl rmi --prune
# 清理停止的容器
crictl rm $(crictl ps -a -q --state exited)
3.1.3 临时文件清理
临时目录如果长期不清理,也会堆积大量垃圾:
# 查看/tmp目录大小
du -sh /tmp
# 删除超过7天的临时文件
find /tmp -type f -atime +7 -delete
# 删除超过7天的空目录
find /tmp -type d -empty -mtime +7 -delete
# 清理pip缓存
pip cache purge
rm -rf ~/.cache/pip
# 清理npm缓存
npm cache clean --force
# 清理apt缓存
apt clean
apt autoclean
# 清理yum缓存
yum clean all
rm -rf /var/cache/yum
3.1.4 旧内核清理
升级系统后,旧内核文件会保留在/boot分区。如果/boot分区比较小,很容易被撑满:
# 查看已安装的内核
dpkg --list | grep linux-image # Debian/Ubuntu
rpm -qa | grep kernel # RHEL/CentOS
# 查看当前使用的内核
uname -r
# Ubuntu自动清理旧内核
apt autoremove --purge
# CentOS保留最近2个内核,删除其他
package-cleanup --oldkernels --count=2
3.2 logrotate配置详解
logrotate是Linux下管理日志文件的标准工具。配置得当可以避免90%的日志撑爆磁盘问题。
主配置文件:/etc/logrotate.conf
# 每周轮转一次
weekly
# 保留4份历史日志
rotate 4
# 轮转后创建新的日志文件
create
# 使用日期作为后缀
dateext
# 压缩历史日志
compress
# 延迟压缩,保留最近一份不压缩(便于查看)
delaycompress
# 日志为空时不轮转
notifempty
# 包含其他配置文件
include /etc/logrotate.d
为应用创建专门的轮转配置,以nginx为例:
# /etc/logrotate.d/nginx
/var/log/nginx/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 0640 www-data adm
sharedscripts
postrotate
[ -f /var/run/nginx.pid ] && kill -USR1 `cat /var/run/nginx.pid`
endscript
}
关键参数解释:
- create 0640 www-data adm:创建新文件的权限和属主
- sharedscripts:多个日志文件共享postrotate脚本
- postrotate/endscript:轮转后执行的命令
按大小轮转(适合增长不规律的日志):
# /etc/logrotate.d/app
/var/log/app/*.log {
size 100M
rotate 10
compress
missingok
notifempty
copytruncate
}
copytruncate参数很重要:它会先复制日志文件,然后清空原文件。这样即使应用没有处理SIGHUP信号的能力,也能正常轮转。缺点是在复制和清空之间可能丢失几行日志。
手动测试logrotate配置:
# 测试配置是否正确(不实际执行)
logrotate -d /etc/logrotate.d/nginx
# 强制执行轮转
logrotate -f /etc/logrotate.d/nginx
# 查看轮转状态
cat /var/lib/logrotate/status
3.3 tmpwatch/tmpreaper配置
tmpwatch(RHEL系)和tmpreaper(Debian系)用于自动清理临时文件。
CentOS/RHEL安装配置:
# 安装
yum install tmpwatch
# 配置cron任务(/etc/cron.daily/tmpwatch)
/usr/sbin/tmpwatch -umc 720 /tmp
/usr/sbin/tmpwatch -umc 720 /var/tmp
参数说明:
Ubuntu/Debian安装配置:
# 安装
apt install tmpreaper
# 配置文件:/etc/tmpreaper.conf
TMPREAPER_TIME=7d
TMPREAPER_PROTECT_EXTRA=''
TMPREAPER_DIRS='/tmp/. /var/tmp/.'
TMPREAPER_DELAY='256'
TMPREAPER_ADDITIONALOPTIONS=''
systemd-tmpfiles(现代方案):
现代Linux发行版更推荐使用systemd-tmpfiles来管理临时文件:
# 查看当前配置
systemd-tmpfiles --cat-config
# 配置文件位置
/etc/tmpfiles.d/ # 管理员自定义配置
/usr/lib/tmpfiles.d/ # 软件包默认配置
/run/tmpfiles.d/ # 运行时配置
创建自定义清理规则:
# /etc/tmpfiles.d/cleanup.conf
# 类型 路径 模式 用户 用户组 生命周期 参数
# 清理超过10天的/tmp文件
d /tmp 1777 root root 10d
# 清理超过30天的/var/tmp文件
d /var/tmp 1777 root root 30d
# 清理超过7天的应用临时文件
D /var/app/tmp 0755 app app 7d
# 每次启动时清空指定目录
D /run/app 0755 app app -
手动执行清理:
# 清理临时文件
systemd-tmpfiles --clean
# 创建目录和文件
systemd-tmpfiles --create
3.4 完整的磁盘清理脚本
下面是我在生产环境使用的一个磁盘清理脚本,包含了常见的清理项目和安全检查:
#!/bin/bash
# disk_cleanup.sh - 生产环境磁盘清理脚本
# Author: SRE Team
# Version: 2.1
# Last Modified: 2025-01-05
set -euo pipefail
# 配置参数
LOG_RETAIN_DAYS=30
TMP_RETAIN_DAYS=7
DOCKER_IMAGE_AGE="720h"# 30天
MIN_FREE_PERCENT=10
DRY_RUN=${DRY_RUN:-false}
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log_info() {
echo -e "${GREEN}[INFO]${NC}$(date '+%Y-%m-%d %H:%M:%S')$1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC}$(date '+%Y-%m-%d %H:%M:%S')$1"
}
log_error() {
echo -e "${RED}[ERROR]${NC}$(date '+%Y-%m-%d %H:%M:%S')$1"
}
# 获取磁盘使用率
get_disk_usage() {
local mount_point=${1:-/}
df -h "$mount_point" | awk 'NR==2 {gsub(/%/,""); print $5}'
}
# 获取可用空间(GB)
get_free_space() {
local mount_point=${1:-/}
df -BG "$mount_point" | awk 'NR==2 {gsub(/G/,""); print $4}'
}
# 清理前后对比
show_cleanup_result() {
local before=$1
local after=$2
local item=$3
local freed=$((before - after))
if [ $freed -gt 0 ]; then
log_info "$item: 释放了 ${freed}GB 空间"
else
log_info "$item: 无可清理空间"
fi
}
# 清理系统日志
cleanup_system_logs() {
log_info "开始清理系统日志..."
local before=$(get_free_space)
if [ "$DRY_RUN" = "true" ]; then
log_info "[DRY-RUN] 将清理 $LOG_RETAIN_DAYS 天前的日志"
find /var/log -type f -name "*.log" -mtime +$LOG_RETAIN_DAYS 2>/dev/null | head -20
find /var/log -type f -name "*.gz" -mtime +$LOG_RETAIN_DAYS 2>/dev/null | head -20
else
# 清理压缩的旧日志
find /var/log -type f -name "*.gz" -mtime +$LOG_RETAIN_DAYS -delete 2>/dev/null || true
find /var/log -type f -name "*.old" -mtime +$LOG_RETAIN_DAYS -delete 2>/dev/null || true
find /var/log -type f -name "*.[0-9]" -mtime +$LOG_RETAIN_DAYS -delete 2>/dev/null || true
# 清理journal日志
ifcommand -v journalctl &>/dev/null; then
journalctl --vacuum-time=${LOG_RETAIN_DAYS}d 2>/dev/null || true
fi
fi
local after=$(get_free_space)
show_cleanup_result $before$after"系统日志"
}
# 清理临时文件
cleanup_temp_files() {
log_info "开始清理临时文件..."
local before=$(get_free_space)
if [ "$DRY_RUN" = "true" ]; then
log_info "[DRY-RUN] 将清理 $TMP_RETAIN_DAYS 天前的临时文件"
find /tmp -type f -atime +$TMP_RETAIN_DAYS 2>/dev/null | wc -l
find /var/tmp -type f -atime +$TMP_RETAIN_DAYS 2>/dev/null | wc -l
else
find /tmp -type f -atime +$TMP_RETAIN_DAYS -delete 2>/dev/null || true
find /var/tmp -type f -atime +$TMP_RETAIN_DAYS -delete 2>/dev/null || true
find /tmp -type d -empty -delete 2>/dev/null || true
find /var/tmp -type d -empty -delete 2>/dev/null || true
fi
local after=$(get_free_space)
show_cleanup_result $before$after"临时文件"
}
# 清理包管理器缓存
cleanup_package_cache() {
log_info "开始清理包管理器缓存..."
local before=$(get_free_space)
if [ "$DRY_RUN" = "true" ]; then
log_info "[DRY-RUN] 将清理包管理器缓存"
else
# APT (Debian/Ubuntu)
ifcommand -v apt &>/dev/null; then
apt clean 2>/dev/null || true
apt autoclean 2>/dev/null || true
fi
# YUM/DNF (RHEL/CentOS)
ifcommand -v yum &>/dev/null; then
yum clean all 2>/dev/null || true
fi
ifcommand -v dnf &>/dev/null; then
dnf clean all 2>/dev/null || true
fi
fi
local after=$(get_free_space)
show_cleanup_result $before$after"包管理器缓存"
}
# 清理Docker资源
cleanup_docker() {
if ! command -v docker &>/dev/null; then
log_info "Docker未安装,跳过"
return
fi
log_info "开始清理Docker资源..."
local before=$(get_free_space)
if [ "$DRY_RUN" = "true" ]; then
log_info "[DRY-RUN] Docker资源占用情况:"
docker system df
else
# 清理停止的容器
docker container prune -f 2>/dev/null || true
# 清理悬空镜像
docker image prune -f 2>/dev/null || true
# 清理指定时间前的未使用镜像
docker image prune -a -f --filter "until=$DOCKER_IMAGE_AGE" 2>/dev/null || true
# 清理未使用的网络
docker network prune -f 2>/dev/null || true
# 清理构建缓存
docker builder prune -f 2>/dev/null || true
fi
local after=$(get_free_space)
show_cleanup_result $before$after"Docker资源"
}
# 检查已删除但未释放的文件
check_deleted_files() {
log_info "检查已删除但未释放的文件..."
local deleted_size=$(lsof +L1 2>/dev/null | awk '{sum+=$7} END {print int(sum/1024/1024/1024)}')
if [ "$deleted_size" -gt 0 ]; then
log_warn "发现 ${deleted_size}GB 已删除但未释放的文件:"
lsof +L1 2>/dev/null | awk '$7 > 104857600 {print $1, $2, $7/1024/1024/1024"GB", $NF}'
log_warn "建议重启相关进程以释放空间"
else
log_info "无已删除但未释放的文件"
fi
}
# 主函数
main() {
log_info "========== 磁盘清理开始 =========="
log_info "当前磁盘使用率: $(get_disk_usage)%"
log_info "当前可用空间: $(get_free_space)GB"
if [ "$DRY_RUN" = "true" ]; then
log_warn "当前为DRY-RUN模式,不会实际删除文件"
fi
cleanup_system_logs
cleanup_temp_files
cleanup_package_cache
cleanup_docker
check_deleted_files
log_info "========== 磁盘清理完成 =========="
log_info "清理后磁盘使用率: $(get_disk_usage)%"
log_info "清理后可用空间: $(get_free_space)GB"
}
# 显示帮助信息
show_help() {
echo"Usage: $0 [options]"
echo""
echo"Options:"
echo" -d, --dry-run 模拟运行,不实际删除文件"
echo" -h, --help 显示帮助信息"
echo""
echo"Environment Variables:"
echo" LOG_RETAIN_DAYS 日志保留天数 (默认: 30)"
echo" TMP_RETAIN_DAYS 临时文件保留天数 (默认: 7)"
echo" DOCKER_IMAGE_AGE Docker镜像保留时间 (默认: 720h)"
}
# 解析参数
while [[ $# -gt 0 ]]; do
case$1in
-d|--dry-run)
DRY_RUN=true
shift
;;
-h|--help)
show_help
exit 0
;;
*)
log_error "未知参数: $1"
show_help
exit 1
;;
esac
done
# 检查root权限
if [ "$EUID" -ne 0 ]; then
log_error "请使用root权限运行此脚本"
exit 1
fi
main
使用方法:
# 模拟运行
./disk_cleanup.sh --dry-run
# 实际执行
./disk_cleanup.sh
# 自定义参数
LOG_RETAIN_DAYS=7 TMP_RETAIN_DAYS=3 ./disk_cleanup.sh
四、最佳实践和注意事项
4.1 性能优化
4.1.1 分区规划最佳实践
一个好的分区规划可以从源头避免磁盘空间问题。我推荐的分区方案:
/ 20-50GB 系统根分区,只装系统和核心软件
/boot 1GB 启动分区,放内核和引导文件
/var 50-100GB 日志和变动数据,根据业务量调整
/var/log 20-50GB 单独的日志分区,防止日志撑爆系统
/home 按需 用户数据
/data 剩余空间 业务数据
swap 内存的1-2倍 交换分区
把/var/log单独分区是一个很重要的经验。这样即使日志文件失控,也不会影响系统的正常运行。
4.1.2 日志级别控制
生产环境不要开DEBUG级别的日志!这是血的教训。
我曾经接手过一个项目,前任运维在生产环境开着DEBUG日志,每天产生50GB的日志。改成INFO级别后,日志量降到了每天200MB。
各应用的日志级别配置:
# Nginx: 注释掉access_log或者改成warn级别
access_log off;
error_log /var/log/nginx/error.log warn;
# Java应用(logback.xml)
<root level="INFO">
<appender-ref ref="FILE"/>
</root>
# Python应用
logging.basicConfig(level=logging.INFO)
4.1.3 日志采样
对于高QPS的服务,可以考虑日志采样:
# Nginx: 只记录1%的access日志
map $request_uri $loggable {
default 0;
"~^/health" 0; # 健康检查不记录
}
map $msec $sample {
default 0;
"~[0-9]\.01" 1; # 1%采样
}
access_log /var/log/nginx/access.log combined if=$sample;
4.2 安全加固
4.2.1 设置磁盘配额
对于多用户系统,设置磁盘配额可以防止单个用户占用过多空间:
# 启用配额支持(编辑/etc/fstab)
# /dev/sda1 /home ext4 defaults,usrquota,grpquota 0 2
# 重新挂载
mount -o remount /home
# 创建配额文件
quotacheck -cum /home
quotaon /home
# 设置用户配额(软限制10GB,硬限制12GB)
setquota -u username 10485760 12582912 0 0 /home
# 查看配额使用情况
repquota -a
4.2.2 目录大小限制
使用tmpfs或者项目级配额限制特定目录的大小:
# 使用tmpfs限制/tmp大小为2GB
mount -t tmpfs -o size=2G tmpfs /tmp
# 写入fstab持久化
# tmpfs /tmp tmpfs size=2G,mode=1777 0 0
4.2.3 防止日志炸弹攻击
有些攻击会故意触发大量错误日志,试图撑满磁盘。防护措施:
# 限制单个日志文件大小(rsyslog配置)
$outchannel log_rotation,/var/log/messages,52428800,/var/log/rotate_script.sh
*.* :omfile:$log_rotation
# fail2ban防护日志爆破
[nginx-limit]
enabled = true
filter = nginx-limit-req
action = iptables-multiport[name=nginx-limit, port="http,https"]
logpath = /var/log/nginx/error.log
maxretry = 10
findtime = 60
bantime = 3600
4.3 常见错误及解决方案
错误1:rm删除大文件后空间不释放
原因:文件被进程占用
解决:
# 方法1:找到占用进程并重启
lsof +L1 | grep deleted
systemctl restart <service>
# 方法2:清空而不是删除
truncate -s 0 /path/to/file
# 方法3:通过/proc释放
: > /proc/<pid>/fd/<fd_number>
错误2:df和du显示的大小不一致
原因:已删除但未释放的文件、稀疏文件、预留空间
排查:
# 检查已删除文件
lsof +L1
# 检查预留空间
tune2fs -l /dev/sda1 | grep Reserved
# du和df的区别
# du统计的是文件实际占用的块大小
# df统计的是文件系统使用的总块数
错误3:空间充足但无法创建文件
原因:inode耗尽
排查:
df -i
# 如果IUse%接近100%,就是inode问题
# 找出哪个目录文件最多
find / -xdev -type f | cut -d "/" -f 2-3 | sort | uniq -c | sort -rn | head -10
错误4:logrotate不生效
常见原因和解决方案:
# 1. 配置文件语法错误
logrotate -d /etc/logrotate.d/nginx # 用-d参数调试
# 2. 权限问题
ls -la /var/log/nginx/
# 确保logrotate有权限读取和写入
# 3. selinux阻止
audit2why < /var/log/audit/audit.log
# 4. 日志文件被锁定
lsattr /var/log/nginx/access.log
# 如果显示i属性,用chattr解除
chattr -i /var/log/nginx/access.log
五、故障排查和监控
5.1 日志查看
磁盘相关问题的排查,可以从以下日志入手:
# 系统日志(查找磁盘相关错误)
grep -i "disk\|space\|full\|no space" /var/log/syslog
grep -i "disk\|space\|full\|no space" /var/log/messages
# dmesg(内核级别的磁盘错误)
dmesg | grep -i "error\|fail\|disk\|sda"
# systemd journal
journalctl -p err -b | grep -i disk
5.2 实时监控命令
5.2.1 iostat查看磁盘IO
# 每2秒刷新一次,显示详细信息
iostat -x 2
# 输出示例:
Device r/s w/s rkB/s wkB/s await svctm %util
sda 0.50 45.00 2.00 180.00 0.50 0.10 0.45
sdb 120.00 30.00 15360.00 3840.00 8.50 2.00 30.00
关键指标:
5.2.2 iotop查看进程IO
# 安装
apt install iotop # Debian/Ubuntu
yum install iotop # CentOS/RHEL
# 使用
iotop -ao # 只显示有IO的进程,累计模式
# 输出示例:
Total DISK READ: 15.00 M/s | Total DISK WRITE: 3.00 M/s
PID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
12345 be/4 mysql 14.50 M/s 2.80 M/s 0.00 % 15.00 % mysqld
23456 be/4 app 0.50 M/s 0.20 M/s 0.00 % 2.00 % java
5.3 Prometheus磁盘监控告警配置
这是我在生产环境使用的完整Prometheus监控配置:
5.3.1 Node Exporter配置
首先确保node_exporter正常运行,它会自动采集磁盘相关指标:
# 安装node_exporter
wget https://github.com/prometheus/node_exporter/releases/download/v1.7.0/node_exporter-1.7.0.linux-amd64.tar.gz
tar xzf node_exporter-1.7.0.linux-amd64.tar.gz
mv node_exporter-1.7.0.linux-amd64/node_exporter /usr/local/bin/
# 创建systemd服务
cat > /etc/systemd/system/node_exporter.service << 'EOF'
[Unit]
Description=Node Exporter
After=network.target
[Service]
Type=simple
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
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now node_exporter
5.3.2 Prometheus告警规则
# /etc/prometheus/rules/disk_alerts.yml
groups:
-name:disk_alerts
rules:
# 磁盘空间告警
-alert:DiskSpaceWarning
expr:|
(node_filesystem_avail_bytes{fstype=~"ext4|xfs|btrfs"}
/ node_filesystem_size_bytes{fstype=~"ext4|xfs|btrfs"}) * 100 < 20
for:5m
labels:
severity:warning
annotations:
summary:"磁盘空间不足警告 - {{ $labels.instance }}"
description:"{{ $labels.mountpoint }} 可用空间低于20%,当前: {{ $value | printf \"%.1f\" }}%"
-alert:DiskSpaceCritical
expr:|
(node_filesystem_avail_bytes{fstype=~"ext4|xfs|btrfs"}
/ node_filesystem_size_bytes{fstype=~"ext4|xfs|btrfs"}) * 100 < 10
for:2m
labels:
severity:critical
annotations:
summary:"磁盘空间严重不足 - {{ $labels.instance }}"
description:"{{ $labels.mountpoint }} 可用空间低于10%,当前: {{ $value | printf \"%.1f\" }}%,需要立即处理!"
-alert:DiskSpaceEmergency
expr:|
(node_filesystem_avail_bytes{fstype=~"ext4|xfs|btrfs"}
/ node_filesystem_size_bytes{fstype=~"ext4|xfs|btrfs"}) * 100 < 5
for:1m
labels:
severity:emergency
annotations:
summary:"磁盘空间即将耗尽 - {{ $labels.instance }}"
description:"{{ $labels.mountpoint }} 可用空间低于5%,当前: {{ $value | printf \"%.1f\" }}%,紧急!"
# inode告警
-alert:InodeWarning
expr:|
(node_filesystem_files_free{fstype=~"ext4|xfs"}
/ node_filesystem_files{fstype=~"ext4|xfs"}) * 100 < 20
for:5m
labels:
severity:warning
annotations:
summary:"inode不足警告 - {{ $labels.instance }}"
description:"{{ $labels.mountpoint }} 可用inode低于20%,当前: {{ $value | printf \"%.1f\" }}%"
-alert:InodeCritical
expr:|
(node_filesystem_files_free{fstype=~"ext4|xfs"}
/ node_filesystem_files{fstype=~"ext4|xfs"}) * 100 < 10
for:2m
labels:
severity:critical
annotations:
summary:"inode严重不足 - {{ $labels.instance }}"
description:"{{ $labels.mountpoint }} 可用inode低于10%,当前: {{ $value | printf \"%.1f\" }}%"
# 磁盘空间增长预测
-alert:DiskWillFillIn24Hours
expr:|
predict_linear(node_filesystem_avail_bytes{fstype=~"ext4|xfs|btrfs"}[6h], 24*3600) < 0
for:30m
labels:
severity:warning
annotations:
summary:"磁盘空间预计24小时内耗尽 - {{ $labels.instance }}"
description:"{{ $labels.mountpoint }} 按当前增长速度,预计24小时内将耗尽空间"
-alert:DiskWillFillIn4Hours
expr:|
predict_linear(node_filesystem_avail_bytes{fstype=~"ext4|xfs|btrfs"}[1h], 4*3600) < 0
for:10m
labels:
severity:critical
annotations:
summary:"磁盘空间预计4小时内耗尽 - {{ $labels.instance }}"
description:"{{ $labels.mountpoint }} 按当前增长速度,预计4小时内将耗尽空间,需要立即处理!"
# 磁盘IO告警
-alert:DiskIOHigh
expr:|
rate(node_disk_io_time_seconds_total[5m]) * 100 > 80
for:10m
labels:
severity:warning
annotations:
summary:"磁盘IO过高 - {{ $labels.instance }}"
description:"{{ $labels.device }} IO利用率超过80%,当前: {{ $value | printf \"%.1f\" }}%"
# 磁盘读写延迟告警
-alert:DiskLatencyHigh
expr:|
rate(node_disk_read_time_seconds_total[5m])
/ rate(node_disk_reads_completed_total[5m]) * 1000 > 20
for:5m
labels:
severity:warning
annotations:
summary:"磁盘读延迟过高 - {{ $labels.instance }}"
description:"{{ $labels.device }} 平均读延迟超过20ms,当前: {{ $value | printf \"%.1f\" }}ms"
5.3.3 Alertmanager配置
# /etc/alertmanager/alertmanager.yml
global:
resolve_timeout:5m
smtp_smarthost:'smtp.company.com:587'
smtp_from:'alertmanager@company.com'
smtp_auth_username:'alertmanager@company.com'
smtp_auth_password:'password'
route:
group_by:['alertname','instance']
group_wait:30s
group_interval:5m
repeat_interval:4h
receiver:'default-receiver'
routes:
# 紧急告警立即发送
-match:
severity:emergency
receiver:'emergency-receiver'
group_wait:10s
repeat_interval:30m
# 严重告警
-match:
severity:critical
receiver:'critical-receiver'
group_wait:30s
repeat_interval:1h
receivers:
-name:'default-receiver'
email_configs:
-to:'ops-team@company.com'
webhook_configs:
-url:'https://hooks.slack.com/services/xxx/yyy/zzz'
-name:'critical-receiver'
email_configs:
-to:'ops-team@company.com,sre-oncall@company.com'
webhook_configs:
-url:'https://hooks.slack.com/services/xxx/yyy/zzz'
-url:'https://api.pagerduty.com/generic/xxx'
-name:'emergency-receiver'
email_configs:
-to:'ops-team@company.com,sre-oncall@company.com,manager@company.com'
webhook_configs:
-url:'https://hooks.slack.com/services/xxx/yyy/zzz'
-url:'https://api.pagerduty.com/generic/xxx'
# 电话告警(通过第三方服务)
webhook_configs:
-url:'https://api.phonecall-service.com/alert'
5.3.4 Grafana Dashboard
创建一个磁盘监控的Dashboard,关键Panel配置:
{
"panels": [
{
"title": "磁盘使用率",
"type": "gauge",
"targets": [
{
"expr": "(1 - node_filesystem_avail_bytes{mountpoint=\"/\"} / node_filesystem_size_bytes{mountpoint=\"/\"}) * 100",
"legendFormat": "{{ mountpoint }}"
}
],
"fieldConfig": {
"defaults": {
"thresholds": {
"steps": [
{"color": "green", "value": null},
{"color": "yellow", "value": 70},
{"color": "orange", "value": 80},
{"color": "red", "value": 90}
]
},
"max": 100,
"min": 0,
"unit": "percent"
}
}
},
{
"title": "磁盘空间趋势(7天)",
"type": "timeseries",
"targets": [
{
"expr": "node_filesystem_avail_bytes{mountpoint=\"/\"} / 1024 / 1024 / 1024",
"legendFormat": "可用空间 (GB)"
},
{
"expr": "node_filesystem_size_bytes{mountpoint=\"/\"} / 1024 / 1024 / 1024",
"legendFormat": "总空间 (GB)"
}
]
},
{
"title": "空间耗尽预测",
"type": "stat",
"targets": [
{
"expr": "(node_filesystem_avail_bytes{mountpoint=\"/\"}) / (rate(node_filesystem_avail_bytes{mountpoint=\"/\"}[6h]) * -1) / 3600",
"legendFormat": "预计耗尽时间"
}
],
"fieldConfig": {
"defaults": {
"unit": "h",
"thresholds": {
"steps": [
{"color": "red", "value": null},
{"color": "orange", "value": 24},
{"color": "yellow", "value": 72},
{"color": "green", "value": 168}
]
}
}
}
},
{
"title": "Top 10 文件系统使用率",
"type": "table",
"targets": [
{
"expr": "topk(10, (1 - node_filesystem_avail_bytes / node_filesystem_size_bytes) * 100)",
"format": "table"
}
]
}
]
}
5.4 自动化告警响应
配合告警,可以设置一些自动化响应动作。但要注意,自动清理需要非常谨慎,我一般只自动清理那些绝对安全的内容:
# Prometheus Alertmanager webhook -> 自动清理服务
# cleanup-service.py (简化示例)
fromflaskimportFlask,request
importsubprocess
importlogging
app=Flask(__name__)
logging.basicConfig(level=logging.INFO)
# 安全的清理命令列表
SAFE_CLEANUP_COMMANDS={
'journal':'journalctl --vacuum-time=3d',
'apt_cache':'apt clean',
'docker_prune':'docker container prune -f',
'tmp_old':'find /tmp -type f -atime +3 -delete',
}
@app.route('/webhook',methods=['POST'])
defhandle_alert():
data=request.json
foralertindata.get('alerts',[]):
ifalert['status']=='firing':
mountpoint=alert['labels'].get('mountpoint','/')
severity=alert['labels'].get('severity','warning')
logging.info(f"收到告警:{mountpoint},严重程度:{severity}")
# 只有warning级别才自动清理,critical和emergency需要人工介入
ifseverity=='warning':
forname,cmdinSAFE_CLEANUP_COMMANDS.items():
logging.info(f"执行清理:{name}")
try:
subprocess.run(cmd,shell=True,timeout=300)
except Exception as e:
logging.error(f"清理失败:{name},错误:{e}")
return'OK',200
if__name__=='__main__':
app.run(host='0.0.0.0',port=5000)
六、总结
6.1 要点回顾
写到这里,把整个磁盘空间排查的流程梳理一下:
- 快速确认:df -h 看空间,df -i 看inode
- 检查陷阱:lsof +L1 检查已删除但未释放的文件
- 日志文件:logrotate + truncate
- Docker:docker system prune
记住一个原则:永远不要等到磁盘满了才去处理。设置好监控告警,在80%的时候就开始关注,90%的时候就要行动。
6.2 进阶方向
如果你想在磁盘管理方面更进一步,可以学习这些内容:
- Ceph/GlusterFS分布式存储:解决单机存储容量限制
- 对象存储(S3/MinIO):日志和备份的归档存储方案
- Kubernetes存储:PV/PVC/StorageClass的管理
- ZFS/Btrfs:高级文件系统特性,如快照、压缩、去重
6.3 参考资料
- Linux Filesystem Hierarchy Standard: https://refspecs.linuxfoundation.org/FHS_3.0/fhs/index.html
- Prometheus Node Exporter: https://github.com/prometheus/node_exporter
- ncdu官方文档: https://dev.yorhel.nl/ncdu
- logrotate手册: man logrotate
- systemd-tmpfiles手册: man tmpfiles.d
附录
附录A:命令速查表
# ========== 磁盘空间检查 ==========
df -h # 查看磁盘使用情况
df -i # 查看inode使用情况
df -Th # 显示文件系统类型
lsblk # 列出块设备
fdisk -l # 列出分区信息
# ========== 空间分析 ==========
du -sh /path # 目录总大小
du -sh /* | sort -rh | head # 根目录下各目录大小排序
ncdu / # 交互式空间分析
dust -d 2 / # 树状图展示
# ========== 文件查找 ==========
find / -type f -size +100M # 查找大于100MB的文件
find / -type f -mtime +30 # 查找30天前修改的文件
find / -type f -atime +30 # 查找30天未访问的文件
find /tmp -type f -delete # 删除/tmp下所有文件
# ========== 进程相关 ==========
lsof +L1 # 已删除但未释放的文件
lsof /var/log/syslog # 查看文件被哪些进程使用
fuser -v /var/log/syslog # 同上
# ========== 清理命令 ==========
truncate -s 0 /path/file # 清空文件内容
> /path/file # 清空文件(另一种写法)
journalctl --vacuum-size=500M # 清理journal日志
docker system prune -a -f # 清理Docker资源
apt clean # 清理APT缓存
yum clean all # 清理YUM缓存
# ========== 文件系统操作 ==========
tune2fs -l /dev/sda1 # 查看ext文件系统信息
tune2fs -m 1 /dev/sda1 # 修改预留空间比例
xfs_info /dev/sda1 # 查看XFS文件系统信息
resize2fs /dev/sda1 # 调整ext文件系统大小
xfs_growfs /mountpoint # 扩展XFS文件系统
# ========== 配额管理 ==========
quotacheck -cum /home # 创建配额数据库
quotaon /home # 启用配额
setquota -u user 10G 12G 0 0 /home # 设置用户配额
repquota -a # 查看所有配额使用情况
# ========== IO监控 ==========
iostat -x 2 # 磁盘IO统计
iotop -ao # 进程IO监控
pidstat -d 1 # 进程磁盘统计
附录B:关键参数详解
df命令参数
du命令参数
find命令常用参数
logrotate配置参数
附录C:术语表
| | |
|---|
| | 存储文件元数据的数据结构,包含权限、时间戳等,不包含文件名和内容 |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| Redundant Array of Independent Disks | |
| | 组织和管理存储设备上数据的方法,如ext4、xfs |
| | |
最后送大家一句话:磁盘空间就像房间,不定期清理,早晚会堆满杂物。养成良好的运维习惯,比什么都重要。