liuziyi

liuziyi

Linux 大文件审计脚本:从“找到大文件”到“智能给出清理建议”

一、引子:磁盘告警时,我们到底需要什么?

凌晨两点,监控告警:/ 分区使用率 92%。

你 SSH 上去,第一反应是 du -sh /* | sort -h 或者 find / -size +1G。命令敲完,屏幕上刷出一堆路径:

939M  /var/lib/docker/containerd/.../blobs/sha256/8d34...
931M  /u01/app/oracle/oradata/ORCL11G/system01.dbf
831M  /u01/app/oracle/oradata/ORCL11G/sysaux01.dbf
574M  /usr/local/mysql/lib/libmysqld.a
513M  /u01/app/oracle/oradata/ORCL11G/temp02.dbf

然后呢?知道哪些文件大,和知道哪些文件能删,是两件事。

如果手抖 rm 了 system01.dbf,数据库直接报废。如果 rm 了 containerd 的 blob 文件,Docker 镜像层会损坏。如果 rm 了正在写入的日志,df 空间还不释放——因为进程还持有 inode。

我最初写的第一版脚本,就是简单遍历文件系统找大文件,按大小排序列出来。用了一段时间,踩过坑之后发现:审计工具的价值不在于"发现大文件",而在于"告诉运维这个文件能不能动、该怎么动"。

于是就有了这个 v2.0 版本:一个会按路径智能识别文件类型、给出分级清理建议、生成安全清理命令清单的大文件审计脚本。


二、第一版脚本的问题

第一版脚本的核心逻辑很简单:

df -Pl -x tmpfs ... | awk '{print $6}' | while read mp; do
    find "$mp" -xdev -type f -size +500M -printf '%s\t%p\n' | sort -nr
done

能跑,但在我那台跑了 Oracle + MySQL + Docker 的机器上,输出是这样的:

[check-lsof]  931MB  /u01/app/oracle/oradata/ORCL11G/system01.dbf
               建议: 删除前务必用 lsof 检查是否被进程占用

问题很明显:

  1. Oracle 数据文件被标成"用 lsof 检查一下" —— 这是误导。system01.dbf 根本不该出现在"候选删除清单"里,它就不应该被考虑删除。正确提示应该是"禁止删除"。
  2. Docker 的 containerd blob 被建议 lsof —— 但 blob 文件不该被直接 rm,正确操作是 docker system prune。
  3. /usr/local/mysql/lib/libmysqld.a 被打成"系统路径,极度谨慎" —— 实际上它只是个编译产物静态库,没有编译需求时删了毫无影响。
  4. 没有概览 —— 只看到一堆文件,不知道总共有多少、哪个风险等级占多少、能回收多少空间。
  5. ./script.sh help 报"未知参数" —— 只认 -h,不认 help 子命令,不友好。
  6. 输出格式 bug —— [check-lsof] 的 [ 被 cut 掉了,成了 check-lsof]。

所以 v2.0 的目标很明确:从"列清单"升级为"做判断"。


三、v2.0 的设计思路

3.1 风险四级分类

把所有大文件按能不能删分成四档,颜色区分:

等级 标记 含义 例子
🚫 禁止删除 红色 删了直接出事 Oracle 数据文件、MySQL 表空间、PG WAL
⚠️ 谨慎处理 黄色 需结合业务判断 正在写入的日志、系统路径、binlog
✅ 可安全清理 绿色 删了有兜底/可重建 旧日志、缓存、containerd、coredump
🔄 按需清理 青色 确认后可删 归档包、备份、.a 静态库、ISO

判断规则放在 classify() 函数里,匹配顺序从具体到宽泛:

具体路径(Oracle/MySQL/PG 数据文件)> 容器/日志/缓存 > 扩展名(.tar/.bak/.a)> 系统路径兜底

3.2 针对性清理姿势,而不是一律 rm

每个分类不只是给个风险等级,还给出该类型文件的正确清理方式:

  • Oracle temp 表空间 → ALTER TABLESPACE TEMP SHRINK SPACE,别在 OS 层动
  • containerd blob → docker system prune -a,不是 rm
  • journald 日志 → journalctl --vacuum-size=200M,不是 rm /var/log/journal/*
  • 正在写入的日志 → truncate -s 0 FILE,不是 rm(rm 后空间不释放!)
  • 已轮转的旧日志 → 直接 rm 即可
  • MySQL binlog → PURGE BINARY LOGS BEFORE '...'

3.3 汇总 + 可回收空间估算

不要让人从上到下翻,一上来就告诉你全局:

共发现 6 个文件,总计 4.31 GB

【风险分级汇总】
   🚫 禁止删除   3 个    2.24 GB
   ✅ 可安全清理 1 个    939.00 MB
   🔄 按需清理   2 个    1.13 GB

⛔ 警告: 发现 3 个「禁止删除」的关键文件(数据库数据等)
💡 建议优先处理「可安全清理 + 按需清理」共 2.06 GB

3.4 --plan 生成安全清理命令清单

只对 SAFE 和 OPTIONAL 类型的文件生成命令,NO 和 CAUTION 一律不出现。生成的命令也按类型适配——Docker 生成 docker system prune,日志生成 truncate,旧日志生成 rm -f。

但这只是候选清单,必须人工复核后执行。 脚本的原则始终是"只审计、只建议、绝不自动删除"。

3.5 性能与稳健性

  • -t N 快速模式:只分析最大的 N 个,大文件很多时秒出结果
  • -d DIR 定向扫描:只看某个目录(比如 /var/log)
  • find -xdev:不跨挂载点,避免在一个扫描任务里同时检查根和一堆 tmpfs
  • printf '%s\t%p\0' + sort -z:用 NUL 分隔,路径带空格/特殊字符也不会错乱

四、完整脚本

#!/bin/bash
# ============================================================
# large-file-audit.sh v2.0  —— 智能大文件审计
#   - 按路径智能识别文件类型,给出分级清理建议
#   - 汇总统计 + 优先清理项 + 可回收空间估算
#   - 支持 help / -h / --help / -s SIZE / -t TOP / --plan
#   - 只审计、只建议,绝不自动删除
# ============================================================
set -uo pipefail

VERSION="2.0.0"
THRESHOLD="500M"
TOP=0
CHECK_LSOF=false
SUMMARY_ONLY=false
EMIT_PLAN=false
SCAN_DIRS=()
USE_COLOR="auto"
EMOJI=true

# ---------- 帮助 ----------
usage() {
cat <<'EOF'
large-file-audit.sh v2.0 —— 智能大文件审计工具

用法:
  large-file-audit.sh [选项]        支持 help / -h / --help

选项:
  -s, --size SIZE    大小阈值(默认 500M),支持 100M / 2G / 500K / 1T
  -t, --top N        只分析最大的 N 个文件(快速模式,如 -t 50)
  -d, --dir PATH     只扫描指定目录(可多次使用);默认扫描所有本地文件系统
  -c, --check-lsof   对结果逐个调用 lsof,标记被进程占用的文件(较慢)
      --summary-only 只输出概览统计,不输出详细列表
      --plan         输出建议执行的清理命令清单(务必人工复核后再执行)
      --no-color     禁用颜色
      --no-emoji     使用纯文本风险标记
  -h, --help         显示帮助
  -V, --version      显示版本
  help               同 --help

风险分级:
  🚫 禁止删除     数据库数据/控制/redo 等,删除将导致严重故障
  ⚠️ 谨慎处理     系统路径 / 正在写入的日志等,需结合业务判断
  ✅ 可安全清理   缓存、临时文件、旧日志、容器镜像等
  🔄 按需清理     备份包、安装镜像、开发产物等,确认后即可清理

示例:
  large-file-audit.sh help
  large-file-audit.sh -s 2G
  large-file-audit.sh -s 100M --plan
  large-file-audit.sh -d /var/log -s 50M -t 20
EOF
}

# ---------- 参数解析 ----------
while [[ $# -gt 0 ]]; do
    case "$1" in
        help|-h|--help) usage; exit 0;;
        -V|--version)   echo "large-file-audit.sh v$VERSION"; exit 0;;
        -s|--size)      [[ $# -ge 2 ]] || { echo "错误: -s 需要参数值" >&2; exit 1; }
                        THRESHOLD="$2"; shift 2;;
        --size=*)       THRESHOLD="${1#*=}"; shift;;
        -t|--top)       [[ $# -ge 2 ]] || { echo "错误: -t 需要参数值" >&2; exit 1; }
                        TOP="$2"; shift 2;;
        --top=*)        TOP="${1#*=}"; shift;;
        -d|--dir)       [[ $# -ge 2 ]] || { echo "错误: -d 需要参数值" >&2; exit 1; }
                        SCAN_DIRS+=("$2"); shift 2;;
        --dir=*)        SCAN_DIRS+=("${1#*=}"); shift;;
        -c|--check-lsof) CHECK_LSOF=true; shift;;
        --summary-only) SUMMARY_ONLY=true; shift;;
        --plan)         EMIT_PLAN=true; shift;;
        --no-color)     USE_COLOR="never"; shift;;
        --no-emoji)     EMOJI=false; shift;;
        *) echo "未知参数: $1(使用 'help' 查看帮助)" >&2; exit 1;;
    esac
done

# ---------- 参数校验 ----------
[[ "$THRESHOLD" =~ ^[0-9]+[kKmMgGtT]?$ ]] \
    || { echo "错误: 阈值格式应为 数字+K/M/G/T(如 100M)" >&2; exit 1; }
[[ "$TOP" =~ ^[0-9]+$ ]] || TOP=0
SIZE_FOR_FIND="${THRESHOLD^^}"

if $CHECK_LSOF && ! command -v lsof >/dev/null 2>&1; then
    echo "警告: --check-lsof 需要 lsof,未找到,已关闭该检查" >&2
    CHECK_LSOF=false
fi

# ---------- 颜色 ----------
if [[ "$USE_COLOR" == "never" || ! -t 1 ]]; then
    C_RED=; C_YEL=; C_GRN=; C_CYN=; C_BLD=; C_DIM=; C_RST=
else
    C_RED=$'\033[1;31m'; C_YEL=$'\033[1;33m'; C_GRN=$'\033[1;32m'
    C_CYN=$'\033[1;36m'; C_BLD=$'\033[1m'; C_DIM=$'\033[2m'; C_RST=$'\033[0m'
fi

# ---------- 工具函数 ----------
fmt_size() {
    local b=$1
    if   (( b >= 1099511627776 )); then
        printf "%d.%02d TB" $((b/1099511627776)) $(( (b%1099511627776)*100/1099511627776 ))
    elif (( b >= 1073741824 )); then
        printf "%d.%02d GB" $((b/1073741824)) $(( (b%1073741824)*100/1073741824 ))
    elif (( b >= 1048576 )); then
        printf "%d.%02d MB" $((b/1048576)) $(( (b%1048576)*100/1048576 ))
    elif (( b >= 1024 )); then
        printf "%d KB" $((b/1024))
    else
        printf "%d B" "$b"
    fi
}

risk_badge() {
    local c="" t=""
    case "$1" in
        NO)       c=$C_RED; $EMOJI && t="🚫 禁止删除"   || t="[禁止]";;
        CAUTION)  c=$C_YEL; $EMOJI && t="⚠️  谨慎处理" || t="[谨慎]";;
        SAFE)     c=$C_GRN; $EMOJI && t="✅ 可安全清理" || t="[可清]";;
        OPTIONAL) c=$C_CYN; $EMOJI && t="🔄 按需清理"   || t="[按需]";;
    esac
    printf "%s%s%s" "$c" "$t" "$C_RST"
}

# ---------- 智能分类核心 ----------
# 输出: RISK|CODE|ADVICE
classify() {
    local f="$1"
    case "$f" in
        # ===== Oracle(数据文件绝不能删) =====
        */oradata/*/temp*.dbf)
            echo "NO|oracle-temp|Oracle 临时表空间。正确清理:SQL> ALTER TABLESPACE TEMP SHRINK SPACE; 或 ALTER DATABASE TEMPFILE '...' DROP 后重建。严禁 OS 层 rm。";;
        */oradata/*/redo*.log)
            echo "NO|oracle-redo|Oracle 联机 redo 日志。删除会导致实例崩溃/数据丢失。禁止删除。";;
        */oradata/*/control*.ctl*)
            echo "NO|oracle-control|Oracle 控制文件。删除会导致数据库无法启动。禁止删除。";;
        */oradata/*/*.dbf)
            echo "NO|oracle-datafile|Oracle 数据文件。删除会导致数据丢失/数据库损坏。禁止删除。";;
        */fast_recovery_area/*|*/flash_recovery_area/*)
            echo "CAUTION|oracle-fra|Oracle 闪回恢复区(归档/备份)。用 RMAN DELETE ARCHIVELOG 或配置 FRA 保留策略,不要 rm。";;
        /u01/app/oracle/*|/opt/oracle/*)
            echo "CAUTION|oracle-home|Oracle 安装目录文件。确认不属于运行实例后再处理。";;
        # ===== MySQL =====
        /var/lib/mysql/ibdata*|/var/lib/mysql/ib_logfile*|/var/lib/mysql/ibtmp*)
            echo "NO|mysql-system|MySQL 系统表空间/redo/临时文件。删除将导致数据库损坏。禁止删除。";;
        /var/lib/mysql/*.ibd|/var/lib/mysql/*/*.ibd|/var/lib/mysql/*.MYD|/var/lib/mysql/*.MYI|/var/lib/mysql/*.frm)
            echo "NO|mysql-data|MySQL 表数据文件。删除将导致数据丢失。禁止删除。";;
        /var/lib/mysql/*-bin.[0-9]*|/var/lib/mysql/mysql-bin.[0-9]*)
            echo "CAUTION|mysql-binlog|MySQL binlog。正确做法:PURGE BINARY LOGS BEFORE '...'; 或配置 expire_logs_days。";;
        /var/lib/mysql/*)
            echo "CAUTION|mysql-other|MySQL 目录下文件,确认用途后再处理。";;
        # ===== PostgreSQL =====
        */pgsql/*/base/*|*/pgsql/*/pg_wal/*|*/postgresql/*/base/*|*/postgresql/*/pg_wal/*)
            echo "NO|pg-data|PostgreSQL 数据/WAL 文件。删除将导致数据库损坏。禁止删除。";;
        # ===== 容器 =====
        /var/lib/docker/*|/var/lib/containerd/*|/var/lib/containers/*|/var/lib/crio/*)
            echo "SAFE|container|容器运行时数据。优先用 docker system prune -a(或 crictl rmi --prune)清理,不要直接 rm 文件。";;
        # ===== journald =====
        /var/log/journal/*)
            echo "SAFE|journal|systemd 日志。用 journalctl --vacuum-size=200M 或 --vacuum-time=7d 安全清理,不要直接 rm。";;
        # ===== core/crash =====
        /var/crash/*|/var/lib/systemd/coredump/*)
            echo "SAFE|coredump|系统/程序 core dump。可安全清理(如无需分析)。";;
        */core|*/core.[0-9]*)
            echo "SAFE|coredump|程序 core dump 文件。可安全清理。";;
        # ===== 日志 =====
        /var/log/*.gz|/var/log/*.xz|/var/log/*.bz2|/var/log/*.zst|/var/log/*.[0-9]|/var/log/*.[0-9][0-9]|/var/log/*.[0-9][0-9][0-9])
            echo "SAFE|log-rotate|已轮转的旧日志。可删除。建议配置 logrotate maxage 自动清理。";;
        /var/log/*.log|/var/log/*.log.*|/var/log/syslog*|/var/log/messages*|/var/log/auth.log*|/var/log/secure*|/var/log/kern.log*|/var/log/daemon.log*|/var/log/cron*|/var/log/maillog*|/var/log/boot.log*)
            echo "CAUTION|log-active|可能正在写入的文本日志。用 truncate -s 0 FILE 清空而非 rm(rm 后空间不释放)。";;
        /var/log/*)
            echo "CAUTION|log-other|/var/log 下非文本日志,确认所属服务后处理。";;
        # ===== 归档/备份 =====
        *.tar|*.tar.gz|*.tgz|*.tar.xz|*.txz|*.tar.bz2|*.tbz2|*.zip|*.7z|*.rar|*.gz|*.xz|*.bz2|*.zst)
            echo "OPTIONAL|archive|归档/压缩包。确认是否为有效备份后再删除。";;
        *.bak|*.old|*.orig|*.backup|*~|*.swp|*.swo|*.tmp)
            echo "SAFE|tempfile|备份/交换/临时文件。可安全删除。";;
        *.dump|*.sql.gz|*.sql.bz2|*.sql.xz)
            echo "OPTIONAL|dbdump|数据库导出文件。确认已异地归档后再删除。";;
        # ===== 开发产物 =====
        *.a|*.o|*.lo|*.la|*.so.debug)
            echo "OPTIONAL|static-lib|静态库/编译中间产物。无编译/链接需求时可删除。";;
        */node_modules/*)
            echo "SAFE|node-modules|node_modules 依赖。可用 npm install 重建。";;
        */__pycache__/*|*.pyc|*.pyo)
            echo "SAFE|pycache|Python 字节码缓存。可安全删除。";;
        *.iso|*.img|*.vmdk|*.qcow2|*.vdi)
            echo "OPTIONAL|image|系统/虚拟机镜像。若已安装或不再需要可删除。";;
        # ===== 缓存/临时 =====
        /var/cache/*|*/.cache/*)
            echo "SAFE|cache|缓存文件。通常可安全清理(对应服务会重建)。";;
        /tmp/*|/var/tmp/*)
            echo "OPTIONAL|tmp|临时目录文件。确认无进程占用后可删。";;
        # ===== 系统(兜底在最后) =====
        /usr/*|/lib/*|/lib64/*|/bin/*|/sbin/*|/boot/*)
            echo "CAUTION|system|系统路径下的大文件,极度谨慎!确认用途(可能影响启动/运行)。";;
        /opt/*)
            echo "CAUTION|opt|/opt 下应用文件,确认应用不再使用后再处理。";;
        *)
            echo "CAUTION|unknown|未知用途的大文件。删除前用 lsof -- FILE 确认无进程占用,并确认文件来源。";;
    esac
}

# ---------- 扫描 ----------
TMPLIST=$(mktemp) || exit 1
DETAIL=$(mktemp)  || exit 1
PLAN=$(mktemp)    || exit 1
TOPFILE="${TMPLIST}.top"
trap 'rm -f "$TMPLIST" "$DETAIL" "$PLAN" "$TOPFILE"' EXIT

START=$(date +%s)

if [[ ${#SCAN_DIRS[@]} -gt 0 ]]; then
    for d in "${SCAN_DIRS[@]}"; do
        [[ -d "$d" ]] || { echo "警告: 目录不存在: $d" >&2; continue; }
        find "$d" -xdev -type f -size +"$SIZE_FOR_FIND" -printf '%s\t%p\0' 2>/dev/null >> "$TMPLIST"
    done
    SCAN_DESC="${SCAN_DIRS[*]}"
else
    mapfile -t MOUNTS < <(df -Pl -x tmpfs -x devtmpfs -x squashfs 2>/dev/null \
        | awk 'NR>1 && $1 ~ /^\/dev\// {print $6}' | sort -u)
    for mp in "${MOUNTS[@]}"; do
        [[ -d "$mp" ]] || continue
        find "$mp" -xdev -type f -size +"$SIZE_FOR_FIND" -printf '%s\t%p\0' 2>/dev/null >> "$TMPLIST"
    done
    SCAN_DESC=$(printf '%s ' "${MOUNTS[@]}")
fi

# 按大小降序(NUL 分隔,稳健处理特殊文件名)
LC_ALL=C sort -z -nr "$TMPLIST" -o "$TMPLIST"
SRC="$TMPLIST"
if [[ "$TOP" -gt 0 ]]; then
    head -z -n "$TOP" "$TMPLIST" > "$TOPFILE"
    SRC="$TOPFILE"
fi

END=$(date +%s); DURATION=$((END-START))

# ---------- 分类 + 统计 + 生成明细 ----------
declare -A CNT SZ
exec 3>"$DETAIL"
exec 4>"$PLAN"

while IFS=$'\t' read -r -d '' size path; do
    cls=$(classify "$path")
    risk="${cls%%|*}"; rest="${cls#*|}"
    code="${rest%%|*}";  advice="${rest#*|}"

    if $CHECK_LSOF; then
        if lsof -- "$path" >/dev/null 2>&1; then
            advice="$advice  ⚠ 当前被进程打开,不要直接删除。"
        fi
    fi

    CNT[$risk]=$(( ${CNT[$risk]:-0} + 1 ))
    SZ[$risk]=$(( ${SZ[$risk]:-0} + size ))

    printf "%s  %s\n" "$(risk_badge "$risk")" "$(fmt_size "$size")" >&3
    printf "   路径: %s\n" "$path" >&3
    printf "   原因: %s\n\n" "$advice" >&3

    if $EMIT_PLAN; then
        case "$code" in
            container)    printf "# [SAFE] 容器运行时数据\n%s\n\n" "docker system prune -a -f   # 或: crictl rmi --prune" >&4;;
            journal)      printf "# [SAFE] systemd 日志\n%s\n\n" "journalctl --vacuum-size=200M" >&4;;
            log-active)   printf "# [CAUTION] 正在使用的日志,确认可清空后:\ntruncate -s 0 '%s'\n\n" "$path" >&4;;
            log-rotate|cache|coredump|tempfile|pycache|node-modules)
                          printf "# [SAFE] %s\nrm -f '%s'\n\n" "$code" "$path" >&4;;
            tmp|archive|image|static-lib|dbdump)
                          printf "# [OPTIONAL] 确认后删除: %s\nrm -f '%s'\n\n" "$code" "$path" >&4;;
            *) ;;     # NO/CAUTION 不生成命令
        esac
    fi
done < "$SRC"

exec 3>&-
exec 4>&-

# ---------- 输出概览 ----------
TOTAL_FILES=0; TOTAL_SIZE=0
for r in NO CAUTION SAFE OPTIONAL; do
    TOTAL_FILES=$(( TOTAL_FILES + ${CNT[$r]:-0} ))
    TOTAL_SIZE=$((  TOTAL_SIZE + ${SZ[$r]:-0}  ))
done

echo "${C_BLD}============================================================${C_RST}"
echo "${C_BLD} 大文件审计报告 v$VERSION${C_RST}"
echo "${C_BLD}============================================================${C_RST}"
echo " 扫描范围 : ${SCAN_DESC:-(无匹配挂载点)}"
echo " 阈值     : > $THRESHOLD"
echo " 耗时     : ${DURATION}s"
echo "------------------------------------------------------------"
if (( TOTAL_FILES == 0 )); then
    echo " ✅ 未发现大于 $THRESHOLD 的文件。"
    echo "============================================================"
    exit 0
fi
echo " 共发现 ${C_BLD}$TOTAL_FILES${C_RST} 个文件,总计 ${C_BLD}$(fmt_size "$TOTAL_SIZE")${C_RST}"
echo ""
echo " 【风险分级汇总】"
for r in NO CAUTION SAFE OPTIONAL; do
    c=${CNT[$r]:-0}; [[ $c -eq 0 ]] && continue
    s=${SZ[$r]:-0}
    printf "   %s  %4d 个   %10s\n" "$(risk_badge "$r")" "$c" "$(fmt_size "$s")"
done
echo ""

if [[ ${CNT[NO]:-0} -gt 0 ]]; then
    echo " ${C_RED}⛔ 警告: 发现 ${CNT[NO]} 个「禁止删除」的关键文件(数据库数据等),"
    echo "    已在下方详细列表中标红。请勿直接删除!${C_RST}"
    echo ""
fi

RECLAIM=$(( ${SZ[SAFE]:-0} + ${SZ[OPTIONAL]:-0} ))
if (( RECLAIM > 0 )); then
    echo " ${C_GRN}💡 建议优先处理「可安全清理 + 按需清理」共 $(fmt_size "$RECLAIM")${C_RST}"
    echo ""
fi

# ---------- 详细列表 ----------
if ! $SUMMARY_ONLY; then
    echo "------------------------------------------------------------"
    echo " 【详细列表】(按大小降序)"
    echo "------------------------------------------------------------"
    cat "$DETAIL"
fi

# ---------- 清理计划 ----------
if $EMIT_PLAN; then
    echo "------------------------------------------------------------"
    echo " 【建议清理命令清单】(仅供参考,务必人工复核后再执行)"
    echo "------------------------------------------------------------"
    echo "# ⚠ 以下命令仅针对可安全/按需清理的文件,禁止删除类的文件不会在此出现。"
    echo "# ⚠ 执行前请再次确认文件用途。"
    echo ""
    if [[ -s "$PLAN" ]]; then
        awk '!seen[$0]++' "$PLAN"
    else
        echo "# 本次未生成需要自动清理的命令"
    fi
fi

echo "============================================================"
echo " 通用安全提示:"
echo "   1. 正在写入的日志用: truncate -s 0 FILE   (不要 rm)"
echo "   2. 删除后 df 未释放空间: lsof +L1 | grep deleted   (重启或 kill 持有进程)"
echo "   3. 长期方案: logrotate + journald SystemMaxUse=500M"
echo "============================================================"

保存为 large-file-audit.sh,chmod +x 即可使用。


五、实战:在 Oracle + Docker + MySQL 混合环境下跑一遍

用我自己的测试机(CentOS 7,跑了 Oracle 11g、MySQL 5.7、Docker)跑一下:

[root@myoracle ~]# ./large-file-audit.sh -t 10

输出:

============================================================
 大文件审计报告 v2.0.0
============================================================
 扫描范围 : /boot/ / 
 阈值     : > 500M
 耗时     : 3s
------------------------------------------------------------
 共发现 6 个文件,总计 4.31 GB

 【风险分级汇总】
   🚫 禁止删除     3 个    2.24 GB
   ✅ 可安全清理   1 个    939.00 MB
   🔄 按需清理     2 个    1.13 GB

 ⛔ 警告: 发现 3 个「禁止删除」的关键文件(数据库数据等),
    已在下方详细列表中标红。请勿直接删除!

 💡 建议优先处理「可安全清理 + 按需清理」共 2.06 GB

------------------------------------------------------------
 【详细列表】(按大小降序)
------------------------------------------------------------
🚫 禁止删除  931.00 MB
   路径: /u01/app/oracle/oradata/ORCL11G/system01.dbf
   原因: Oracle 数据文件。删除会导致数据丢失/数据库损坏。禁止删除。

🚫 禁止删除  831.00 MB
   路径: /u01/app/oracle/oradata/ORCL11G/sysaux01.dbf
   原因: Oracle 数据文件。删除会导致数据丢失/数据库损坏。禁止删除。

🚫 禁止删除  513.00 MB
   路径: /u01/app/oracle/oradata/ORCL11G/temp02.dbf
   原因: Oracle 临时表空间。正确清理:SQL> ALTER TABLESPACE TEMP SHRINK SPACE;
        或 ALTER DATABASE TEMPFILE '...' DROP 后重建。严禁 OS 层 rm。

✅ 可安全清理  939.00 MB
   路径: /var/lib/docker/containerd/daemon/io.containerd.content.v1.content/blobs/sha256/8d34...
   原因: 容器运行时数据。优先用 docker system prune -a(或 crictl rmi --prune)清理,
        不要直接 rm 文件。

🔄 按需清理  574.00 MB
   路径: /usr/local/mysql/lib/libmysqld.a
   原因: 静态库/编译中间产物。无编译/链接需求时可删除。

🔄 按需清理  560.00 MB
   路径: /root/backup/mysql-full-20241020.tar.gz
   原因: 归档/压缩包。确认是否为有效备份后再删除。

对比第一版,差异立竿见影:

  • Oracle 三个数据文件全部标红,且给出了正确的表空间清理姿势(SHRINK SPACE 或 TEMPFILE DROP)
  • containerd blob 标绿,并提示用 docker system prune 而不是 rm
  • libmysqld.a 从"系统路径极危"降级为"静态库,无编译需求可删"
  • 备份包识别为"按需清理"
  • 顶部 4 行给全局视图,能立刻判断"这台机器主要是 Oracle 数据占空间,不能乱删"

加上 --plan 生成候选清理清单

[root@myoracle ~]# ./large-file-audit.sh -t 10 --plan
...
------------------------------------------------------------
 【建议清理命令清单】(仅供参考,务必人工复核后再执行)
------------------------------------------------------------
# ⚠ 以下命令仅针对可安全/按需清理的文件,禁止删除类的文件不会在此出现。
# ⚠ 执行前请再次确认文件用途。

# [SAFE] 容器运行时数据
docker system prune -a -f   # 或: crictl rmi --prune

# [OPTIONAL] 确认后删除: static-lib
rm -f '/usr/local/mysql/lib/libmysqld.a'

# [OPTIONAL] 确认后删除: archive
rm -f '/root/backup/mysql-full-20241020.tar.gz'

三个命令都只针对绿色和青色文件。三个 Oracle 数据文件不会出现在这里。运维看到这份清单,只需要判断"libmysqld.a 和备份包删不删",风险被降到极低。


六、一些使用上的技巧

6.1 磁盘告警现场:先看概览,别急着删

./large-file-audit.sh --summary-only

只要一屏就能看到全局分级,判断这台机器的空间到底被什么类型的文件占了。

6.2 大文件很多时:TOP 模式

./large-file-audit.sh -t 20

先看 TOP 20 大文件,往往最大的几个就是问题所在。全量扫描在大磁盘上可能要跑几分钟,TOP 模式快得多。

6.3 定向排查:只看日志目录

./large-file-audit.sh -d /var/log -s 50M

挂载点扫描会跳过 /var/log 下的非本地文件系统,定向模式直接把范围锁死在日志,出结果最快。

6.4 检查占用:-c 加 lsof

./large-file-audit.sh -c -t 50

如果有个大文件被你怀疑"为什么 rm 了空间没释放",加上 -c 会逐个跑 lsof,被占用的会明确标红。代价是慢一点(每个文件 10-100ms)。

6.5 定期做审计,而不是等告警

放到 crontab 里每周跑一次,输出到文件:

0 3 * * 0 /root/large-file-audit.sh -s 1G --summary-only > /var/log/large-file-audit.log 2>&1

趋势一眼可见:什么类型的文件在涨、涨了多少。


七、几个运维大坑(脚本背后的经验)

写这个脚本的过程,其实是在整理自己踩过的坑:

坑 1:rm 正在写入的日志,df 空间不降反升。

Linux 上文件有两个引用:目录项(dirent)和 inode。rm 只是删除了目录项,但如果进程还持有 inode 的文件描述符,内核认为文件仍在使用,不会回收磁盘块。日志进程(rsyslog、nginx、docker)通常开着 O_APPEND 长连接,一直持有 fd。所以 rm 之后 df 毫无变化,只有重启进程才会释放。

正确操作是 truncate -s 0 file 或 : > file:保留 inode,只清空内容,进程不感知,空间立即回收。

脚本里对"正在写入的日志"特意用 CAUTION 而不是 SAFE,原因就在这。

坑 2:删了 Oracle 的 temp02.dbf。

我刚接触 Oracle 时,看到 temp02.dbf 占 500MB 觉得是"临时"文件,可以删吧?删完数据库实例直接报错,只能通过重建临时表空间恢复。Oracle 的"临时"指的是表空间用途是临时排序/哈希,不是"这个文件可以删"。

Oracle 里所有 .dbf 都不该在 OS 层被手动删除。表空间有空闲空间就用 SHRINK,要重建就用 DROP + ADD。

坑 3:手动删 containerd blob 把 Docker 玩坏。

/var/lib/docker/containerd/.../blobs/sha256/... 目录下是内容寻址的镜像层。直接 rm 会导致镜像元数据指向不存在的 blob,docker images 会报错,甚至 pull 新镜像也出问题。

正确姿势是 docker system prune -a 或 docker image prune -a,它会连同元数据一起清。如果只是清理某个特定镜像,用 docker rmi。

坑 4:find / -size +1G 跨文件系统扫描慢得离谱。

不指定 -xdev 的话,find 会从 / 往下扫,遇到 /proc、/sys 会读到内核虚拟文件,遇到 tmpfs 挂载点会读内存,遇到 NFS 网络挂载点甚至会卡死几分钟。脚本里用 -xdev + 挂载点遍历,就是为了避免这个问题。


八、总结

这个脚本的核心价值,不是"发现大文件"——这件事 find 一行就能做——而是把"能不能删、怎么删"的判断从人脑里搬进脚本。

对于运维来说,磁盘告警时的心理压力很大:不删空间不够,删错服务器报废。这个脚本试图做的事,就是把这份压力分成两个确定的部分:

  1. 哪些文件绝对别碰(红色,一眼就知道)
  2. 哪些文件可以放心处理(绿色,附上正确命令)

中间那些黄和青的,交给人判断。

脚本本身只是 bash,--plan 生成的命令也永远需要人工复核,它不自动执行任何删除——这是刻意为之。运维工具的正确姿势是"辅助决策",而不是"替人决定",尤其是在删文件这种事上。

希望这个脚本能帮你少踩几个坑,少几次半夜被磁盘告警叫醒。

posted on 2026-10-26 16:22  刘子毅  阅读(64)  评论(0)    收藏  举报

导航