如何配置云服务器日志的清理与归档流程规范?

摘要:# EC2*ECS日志清理与归档配置规范 > 标准化日志生命周期管理:压缩 - 归档(S3) - 清理,防止磁盘打满导致服务不可用。 ## 目录 - [架构概览](#架构概览) - [清理策略说明](#清理策略说明)
# EC2*ECS日志清理与归档配置规范 > 标准化日志生命周期管理:压缩 - 归档(S3) - 清理,防止磁盘打满导致服务不可用。 --- ## 目录 - [架构概览](#架构概览) - [清理策略说明](#清理策略说明) - [配置参数规范](#配置参数规范) - [优化脚本(可配置版)](#优化脚本可配置版) - [部署与运维](#部署与运维) - [监控与告警](#监控与告警) - [原始脚本分析与改进点](#原始脚本分析与改进点) --- ## 架构概览 ``` 日志文件 (.log) | +-- 正常流程(磁盘 < 普通阈值) | --> 压缩 (.log.gz) --> 可选上传 S3 --> 删除原始 .log | +-- 普通清理(磁盘 > 普通阈值) | --> 删除最旧的 .gz 文件(分批删除,直到低于阈值) | +-- 紧急清理(磁盘 > 紧急阈值,需开启 ENABLE_EMERGENCY_CLEANUP) --> 激进删除:.gz -> 旧 .log -> 最旧日志文件(保护活跃日志) ``` ### 生命周期 ``` 写入中 (.log) 保留(活跃日志,受 KEEP_FILES 保护) | 60 分钟后 已轮转 (.20260410.log) 压缩为 .gz -> 删除原始文件 | 磁盘超阈值时 压缩文件 (.gz) 按时间从旧到新依次删除 | 可选 S3 归档 长期保留,按 S3 lifecycle 策略自动过期 ``` --- ## 清理策略说明 ### 三级清理机制 | 级别 | 触发条件 | 开关 | 行为 | 风险等级 | |----------------|----------------------------------|-----------------------------------|------------------------------------------------|----------| | **压缩归档** | 每轮循环 | 始终开启 | 压缩轮转日志,可选上传 S3,删除原始文件 | 低 | | **普通清理** | 磁盘 > `THRESHOLD` | 始终开启 | 分批删除最旧 `.gz` 文件,直到低于阈值 | 中 | | **紧急清理** | 磁盘 > `EMERGENCY_THRESHOLD` | **`ENABLE_EMERGENCY_CLEANUP`** | 激进删除所有可清理日志(保护 `KEEP_FILES`) | 高 | > **紧急清理开关说明:** `ENABLE_EMERGENCY_CLEANUP=1` 开启(默认),`=0` 关闭。 > 关闭后磁盘即使超过紧急阈值也仅执行普通清理 + 发送告警,不会激进删除文件。 > 适用于希望人工介入而非自动处理的场景。 ### 文件保护机制 `KEEP_FILES` 列表中的文件在**任何清理级别**下都不会被删除,确保正在写入的活跃日志不被中断: ```bash KEEP_FILES_CSV="rapidtrade_server.log,exchange.log,rapidtrade_order.log" ``` ### S3 归档路径结构 ``` s3://{bucket}/{server-ip}/{relative-path}.gz 示例: s3://app-logs-prod/10.0.1.23/logs/trading-gateway/trading-gateway.20260410.1.log.gz ``` --- ## 配置参数规范 ### 参数说明 | 参数 | 类型 | 默认值 | 说明 | |------------------------------|------------|-----------|---------------------------------------------------| | `LOG_DIR` | 路径 | `/data/` | 日志根目录 | | `THRESHOLD` | 百分比 | `70` | 普通清理触发阈值 | | `EMERGENCY_THRESHOLD` | 百分比 | `85` | 紧急清理触发阈值 | | `ENABLE_EMERGENCY_CLEANUP` | 0/1 | `1` | **紧急清理开关**,0=关闭(仅告警不删除),1=开启 | | `DELETE_BATCH` | 整数 | `20` | 每批删除文件数 | | `SLEEP_INTERVAL` | 秒 | `60` | 主循环间隔 | | `COMPRESS_AGE_MIN` | 分钟 | `60` | 多久前的轮转日志才压缩 | | `ENABLE_S3_UPLOAD` | 0/1 | `0` | S3 上传开关 | | `AWS_BUCKET_NAME` | 字符串 | - | S3 存储桶名称 | | `AWS_REGION` | 字符串 | - | AWS 区域 | | `KEEP_FILES_CSV` | 逗号分隔 | - | 受保护的活跃日志文件名 | | `LOG_ENV` | 字符串 | `prod` | 环境标识 | | `DRY_RUN` | 0/1 | `0` | 试运行模式,只打印不执行 | | `ENABLE_NOTIFICATION` | 0/1 | `0` | 是否发送告警通知 | | `NOTIFY_WEBHOOK_URL` | URL | - | 飞书/Slack webhook 地址 | ### 各环境推荐值 | 参数 | prod | uat/sit | dev | |------------------------------|--------|---------|-------| | `THRESHOLD` | `70` | `75` | `80` | | `EMERGENCY_THRESHOLD` | `85` | `90` | `90` | | `ENABLE_EMERGENCY_CLEANUP` | `1` | `1` | `0` | | `SLEEP_INTERVAL` | `60` | `120` | `300` | | `COMPRESS_AGE_MIN` | `60` | `30` | `30` | | `ENABLE_S3_UPLOAD` | `1` | `0` | `0` | | `DELETE_BATCH` | `20` | `50` | `50` | --- ## 优化脚本(可配置版) 相比原始脚本的**核心改进**: | 改进项 | 说明 | |------------------------|------------------------------------------------------------| | 外部配置文件 | 参数与脚本分离,一份脚本适配所有环境 | | 紧急清理独立开关 | `ENABLE_EMERGENCY_CLEANUP` 控制是否执行激进删除 | | `set -euo pipefail` | 严格模式,避免静默失败 | | flock 文件锁 | 防止 cron 重复调度导致并发执行 | | dry-run 模式 | 上线前可安全验证逻辑 | | 消除 `eval` | 使用数组传参,避免命令注入风险 | | 告警通知 | 紧急清理或关闭紧急清理时自动发送告警 | | 凭据外置 | AWS 凭据从环境变量或 IAM Role 获取,不硬编码 | ### 配置文件 ```bash # /etc/log-cleaner/config.env # ============================================================ # 日志清理配置 - 按环境修改 # ============================================================ # ---- 基础配置 ---- LOG_DIR="/data/" LOG_ENV="prod" SLEEP_INTERVAL=60 DRY_RUN=0 # ---- 磁盘阈值 ---- THRESHOLD=70 EMERGENCY_THRESHOLD=85 # ---- 紧急清理开关 ---- # 1=开启:磁盘超过 EMERGENCY_THRESHOLD 时自动激进删除 # 0=关闭:仅发送告警,不执行激进删除(需人工介入) ENABLE_EMERGENCY_CLEANUP=1 # ---- 压缩与清理 ---- COMPRESS_AGE_MIN=60 DELETE_BATCH=20 # ---- S3 归档 ---- ENABLE_S3_UPLOAD=1 AWS_BUCKET_NAME="app-logs-prod" AWS_REGION="ap-northeast-1" # AWS 凭据通过 IAM Role 或 EnvironmentFile 注入,不在此配置 # ---- 保护文件(逗号分隔) ---- KEEP_FILES_CSV="rapidtrade_server.log,exchange.log,rapidtrade_order.log" # ---- 通知 ---- ENABLE_NOTIFICATION=0 # NOTIFY_WEBHOOK_URL="https://open.feishu.cn/open-apis/bot/v2/hook/xxx" ``` ### 脚本主体 ```bash #!/usr/bin/env bash set -euo pipefail # ============================================================ # 日志清理与归档脚本(通用版) # 用途:压缩轮转日志 -> 可选上传 S3 -> 磁盘超阈值时自动清理 # 部署:systemd service 或 cron # ============================================================ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CONFIG_FILE="${LOG_CLEANER_CONFIG:-/etc/log-cleaner/config.env}" LOCK_FILE="/var/run/log-cleaner.lock" # ---- 加载配置 ---- if [[ -f "$CONFIG_FILE" ]]; then # shellcheck source=/dev/null source "$CONFIG_FILE" else echo "FATAL: 配置文件不存在: $CONFIG_FILE" >&2 exit 1 fi # ---- 解析 KEEP_FILES ---- IFS=',' read -ra KEEP_FILES <<< "${KEEP_FILES_CSV:-}" # ---- 默认值 ---- LOG_DIR="${LOG_DIR:-/data/}" THRESHOLD="${THRESHOLD:-70}" EMERGENCY_THRESHOLD="${EMERGENCY_THRESHOLD:-85}" ENABLE_EMERGENCY_CLEANUP="${ENABLE_EMERGENCY_CLEANUP:-1}" DELETE_BATCH="${DELETE_BATCH:-20}" SLEEP_INTERVAL="${SLEEP_INTERVAL:-60}" COMPRESS_AGE_MIN="${COMPRESS_AGE_MIN:-60}" ENABLE_S3_UPLOAD="${ENABLE_S3_UPLOAD:-0}" DRY_RUN="${DRY_RUN:-0}" ENABLE_NOTIFICATION="${ENABLE_NOTIFICATION:-0}" LOG_ENV="${LOG_ENV:-unknown}" SERVER_IP=$(hostname -I | awk '{print $1}') # ---- 文件锁(防止并发执行) ---- exec 200>"$LOCK_FILE" if ! flock -n 200; then echo "WARN: 另一个实例正在运行,退出" >&2 exit 0 fi # ---- 日志函数 ---- log() { local level="$1"; shift echo "$(date '+%Y-%m-%d %H:%M:%S') [$level] [env=$LOG_ENV] $*" } # ---- dry-run 包装 ---- safe_rm() { if [[ "$DRY_RUN" -eq 1 ]]; then log "DRY-RUN" "would rm: $*" else rm -f "$@" fi } safe_gzip() { local src="$1" dst="$2" if [[ "$DRY_RUN" -eq 1 ]]; then log "DRY-RUN" "would gzip: $src -> $dst" else gzip -c "$src" > "$dst" fi } # ---- 磁盘使用率 ---- check_disk_usage() { df "$LOG_DIR" | awk 'NR==2 {gsub("%", "", $5); print $5}' } # ---- AWS CLI 检测 ---- if [[ "$ENABLE_S3_UPLOAD" -eq 1 ]]; then if ! command -v aws &>/dev/null; then log "WARN" "AWS CLI 不可用,禁用 S3 上传" ENABLE_S3_UPLOAD=0 fi fi # ---- 构建 find 排除参数(数组,安全传参) ---- build_exclude_args() { local args=() for keep_file in "${KEEP_FILES[@]}"; do [[ -z "$keep_file" ]] && continue args+=("!" "-name" "$keep_file" "!" "-name" "${keep_file}.gz") done printf '%s\n' "${args[@]}" } # ---- 告警通知 ---- send_alert() { local message="$1" if [[ "$ENABLE_NOTIFICATION" -ne 1 ]]; then return 0; fi if [[ -z "${NOTIFY_WEBHOOK_URL:-}" ]]; then return 0; fi local payload="{\"msg_type\":\"text\",\"content\":{\"text\":\"[Log-Cleaner] [${LOG_ENV}] [${SERVER_IP}] ${message}\"}}" curl -s -X POST "$NOTIFY_WEBHOOK_URL" \ -H "Content-Type: application/json" \ -d "$payload" >/dev/null 2>&1 || true } # ---- 上传 S3 ---- upload_to_s3() { local gz_file="$1" local relative_path="${gz_file#"$LOG_DIR"}" local s3_path="s3://${AWS_BUCKET_NAME}/${SERVER_IP}/${relative_path}" if [[ "$ENABLE_S3_UPLOAD" -ne 1 ]]; then log "INFO" "S3 上传已禁用,跳过: $gz_file" return 0 fi log "INFO" "上传到 S3: $s3_path" if [[ "$DRY_RUN" -eq 1 ]]; then log "DRY-RUN" "would upload: $gz_file -> $s3_path" return 0 fi if ! aws s3 cp "$gz_file" "$s3_path" --region "$AWS_REGION" --quiet; then log "ERROR" "上传失败: $gz_file" return 1 fi return 0 } # ---- 压缩轮转日志 ---- compress_rotated_logs() { log "INFO" "查找 ${COMPRESS_AGE_MIN} 分钟前的轮转日志..." mapfile -t exclude_args < <(build_exclude_args) local -a log_files=() while IFS= read -r -d '' line; do log_files+=("${line#* }") done < <( find "$LOG_DIR" -type f -name "*.log" \ -mmin +"$COMPRESS_AGE_MIN" \ "${exclude_args[@]}" \ -regextype posix-extended \ -regex '.*\.[0-9]+\.log$' \ ! -name "*.current.log" \ -printf "%T@ %p\0" 2>/dev/null \ | sort -z -n ) if [[ ${#log_files[@]} -eq 0 ]]; then log "INFO" "没有需要压缩的日志文件" return fi log "INFO" "找到 ${#log_files[@]} 个待压缩文件" for file in "${log_files[@]}"; do local gz_file="${file}.gz" log "INFO" "压缩: $file" if ! safe_gzip "$file" "$gz_file"; then log "ERROR" "压缩失败: $file" continue fi upload_to_s3 "$gz_file" || continue log "INFO" "删除原始文件: $file" safe_rm "$file" done } # ---- 普通清理(删除最旧 .gz) ---- normal_cleanup() { local usage usage=$(check_disk_usage) while (( usage > THRESHOLD )); do log "WARN" "磁盘 ${usage}% 超过阈值 ${THRESHOLD}%,删除最旧的 ${DELETE_BATCH} 个 .gz 文件" mapfile -t exclude_args < <(build_exclude_args) local -a old_gz=() while IFS= read -r -d '' line; do old_gz+=("${line#* }") done < <( find "$LOG_DIR" -type f -name "*.log.gz" \ "${exclude_args[@]}" \ -printf "%T@ %p\0" 2>/dev/null \ | sort -z -n | head -z -n "$DELETE_BATCH" ) if [[ ${#old_gz[@]} -eq 0 ]]; then log "WARN" "没有可删除的 .gz 文件,无法进一步降低磁盘使用率" break fi for f in "${old_gz[@]}"; do log "WARN" "删除: $f" safe_rm "$f" done sleep 2 usage=$(check_disk_usage) log "INFO" "清理后磁盘使用率: ${usage}%" done } # ---- 紧急清理 ---- emergency_cleanup() { local usage="$1" # === 紧急清理开关检查 === if [[ "$ENABLE_EMERGENCY_CLEANUP" -ne 1 ]]; then log "CRIT" "磁盘 ${usage}% 超过紧急阈值 ${EMERGENCY_THRESHOLD}%,但紧急清理已关闭 (ENABLE_EMERGENCY_CLEANUP=0)" log "CRIT" "仅执行普通清理 + 发送告警,需人工介入处理" send_alert "磁盘使用率 ${usage}% 超紧急阈值 ${EMERGENCY_THRESHOLD}%,紧急清理已关闭,需人工介入!" # 回退到普通清理 normal_cleanup return fi log "CRIT" "磁盘 ${usage}% 超过紧急阈值 ${EMERGENCY_THRESHOLD}%,执行紧急清理" send_alert "磁盘使用率 ${usage}% 超紧急阈值 ${EMERGENCY_THRESHOLD}%,正在执行紧急清理" mapfile -t exclude_args < <(build_exclude_args) # 第一步:删除所有 .gz 文件 log "CRIT" "[Step 1/3] 删除所有压缩日志 (.log.gz)" while IFS= read -r -d '' line; do local f="${line#* }" log "CRIT" "紧急删除: $f" safe_rm "$f" done < <( find "$LOG_DIR" -type f -name "*.log.gz" \ "${exclude_args[@]}" \ -printf "%T@ %p\0" 2>/dev/null | sort -z -n ) sleep 2 usage=$(check_disk_usage) if (( usage <= EMERGENCY_THRESHOLD )); then log "CRIT" "Step 1 后磁盘 ${usage}%,已低于阈值,跳过后续步骤" return fi # 第二步:删除 10 分钟前的旧 .log 文件 log "CRIT" "[Step 2/3] 删除 10 分钟前的旧日志 (.log)" while IFS= read -r -d '' line; do local f="${line#* }" log "CRIT" "紧急删除: $f" safe_rm "$f" done < <( find "$LOG_DIR" -type f -name "*.log" -mmin +10 \ "${exclude_args[@]}" \ -printf "%T@ %p\0" 2>/dev/null | sort -z -n ) sleep 2 usage=$(check_disk_usage) if (( usage <= EMERGENCY_THRESHOLD )); then log "CRIT" "Step 2 后磁盘 ${usage}%,已低于阈值,跳过后续步骤" return fi # 第三步:删除所有日志变体文件(最多 50 个最旧的) log "CRIT" "[Step 3/3] 删除最旧的日志变体文件 (.log-*, .log.*)" while IFS= read -r -d '' line; do local f="${line#* }" log "CRIT" "紧急删除: $f" safe_rm "$f" done < <( find "$LOG_DIR" -type f \ \( -name "*.log" -o -name "*.log-*" -o -name "*.log.gz" -o -name "*.log.*" \) \ "${exclude_args[@]}" \ -printf "%T@ %p\0" 2>/dev/null \ | sort -z -n | head -z -n 50 ) sleep 5 local new_usage new_usage=$(check_disk_usage) log "CRIT" "紧急清理完成,磁盘使用率: ${new_usage}%" if (( new_usage > EMERGENCY_THRESHOLD )); then log "CRIT" "清理后仍高于阈值,需要人工检查" log "CRIT" "占用最大的日志文件:" find "$LOG_DIR" -type f \( -name "*.log" -o -name "*.log.gz" \) \ -printf "%s %p\n" 2>/dev/null | sort -rn | head -20 | \ awk '{printf " %8.1fM %s\n", $1/1048576, $2}' send_alert "紧急清理后磁盘仍达 ${new_usage}%,需要人工介入" fi } # ---- 优雅退出 ---- cleanup() { log "INFO" "收到退出信号,清理中..." flock -u 200 exit 0 } trap cleanup SIGTERM SIGINT # ============================================================ # 主循环 # ============================================================ log "INFO" "日志清理服务启动" log "INFO" " dir=$LOG_DIR threshold=$THRESHOLD% emergency=$EMERGENCY_THRESHOLD%" log "INFO" " emergency_cleanup=$ENABLE_EMERGENCY_CLEANUP s3=$ENABLE_S3_UPLOAD dry_run=$DRY_RUN" while true; do usage=$(check_disk_usage) log "INFO" "当前磁盘使用率: ${usage}%" if (( usage > EMERGENCY_THRESHOLD )); then emergency_cleanup "$usage" else compress_rotated_logs normal_cleanup fi log "INFO" "等待 ${SLEEP_INTERVAL}s..." sleep "$SLEEP_INTERVAL" done ``` --- ## 部署与运维 ### 目录结构 ``` /etc/log-cleaner/ config.env # 配置文件(按环境区分) log-cleaner.sh # 脚本主体(所有环境共用) ``` ### systemd 部署(推荐) ```ini # /etc/systemd/system/log-cleaner.service [Unit] Description=Log Cleaner and Archiver After=network.target [Service] Type=simple ExecStart=/etc/log-cleaner/log-cleaner.sh Restart=on-failure RestartSec=10s User=root # AWS 凭据通过 IAM Role 注入 # 如必须使用 AK/SK,通过 EnvironmentFile 注入: # EnvironmentFile=/etc/log-cleaner/aws-credentials.env [Install] WantedBy=multi-user.target ``` ```bash sudo systemctl daemon-reload sudo systemctl enable --now log-cleaner sudo journalctl -u log-cleaner -f # 查看日志 ``` ### cron 部署(备选) ```bash # 不建议用 cron,因为主循环自带 sleep。如果仍要用 cron: # 需移除脚本中的 while true 循环,改为单次执行 */2 * * * * flock -n /var/run/log-cleaner.lock /etc/log-cleaner/log-cleaner.sh >> /var/log/log-cleaner.log 2>&1 ``` ### 首次上线流程 ```bash # 1. 部署配置和脚本 sudo mkdir -p /etc/log-cleaner sudo cp config.env /etc/log-cleaner/ sudo cp log-cleaner.sh /etc/log-cleaner/ sudo chmod +x /etc/log-cleaner/log-cleaner.sh # 2. dry-run 验证(不会实际删除任何文件) sudo DRY_RUN=1 LOG_CLEANER_CONFIG=/etc/log-cleaner/config.env /etc/log-cleaner/log-cleaner.sh # 观察输出,确认匹配的文件符合预期,Ctrl+C 停止 # 3. 正式启动 sudo systemctl start log-cleaner sudo journalctl -u log-cleaner -f ``` ### 运行时动态调整 紧急清理开关支持**运行时热切换**,无需重启服务: ```bash # 关闭紧急清理(不重启服务) sudo sed -i 's/^ENABLE_EMERGENCY_CLEANUP=1/ENABLE_EMERGENCY_CLEANUP=0/' /etc/log-cleaner/config.env # 脚本下一轮循环会自动读取新值(需脚本支持 reload) # 或者重启服务使配置立即生效 sudo systemctl restart log-cleaner ``` ### S3 Lifecycle 策略(推荐) 归档到 S3 的日志建议配置自动过期策略: ```json { "Rules": [ { "ID": "log-archive-lifecycle", "Status": "Enabled", "Filter": { "Prefix": "" }, "Transitions": [ { "Days": 30, "StorageClass": "STANDARD_IA" }, { "Days": 90, "StorageClass": "GLACIER" } ], "Expiration": { "Days": 365 } } ] } ``` ```bash aws s3api put-bucket-lifecycle-configuration \ --bucket app-logs-prod \ --lifecycle-configuration file://lifecycle.json ``` --- ## 监控与告警 ### Prometheus 指标(通过 node_exporter textfile) ```bash # 在主循环末尾追加(可选) METRICS_FILE="/var/lib/node_exporter/textfile/log_cleaner.prom" cat > "$METRICS_FILE" <<PROM # HELP log_cleaner_disk_usage_percent Current disk usage # TYPE log_cleaner_disk_usage_percent gauge log_cleaner_disk_usage_percent{mount="$LOG_DIR",env="$LOG_ENV"} $usage # HELP log_cleaner_last_run_timestamp Last run time # TYPE log_cleaner_last_run_timestamp gauge log_cleaner_last_run_timestamp{env="$LOG_ENV"} $(date +%s) # HELP log_cleaner_emergency_enabled Emergency cleanup switch # TYPE log_cleaner_emergency_enabled gauge log_cleaner_emergency_enabled{env="$LOG_ENV"} $ENABLE_EMERGENCY_CLEANUP PROM ``` ### 建议告警规则 ```yaml groups: - name: log-cleaner rules: - alert: DiskUsageHigh expr: log_cleaner_disk_usage_percent > 80 for: 5m labels: severity: warning annotations: summary: "磁盘使用率 {{ $value }}% 超过 80%" - alert: DiskUsageCritical expr: log_cleaner_disk_usage_percent > 90 for: 2m labels: severity: critical annotations: summary: "磁盘使用率 {{ $value }}% 超过 90%,可能影响服务" - alert: LogCleanerStale expr: time() - log_cleaner_last_run_timestamp > 300 for: 5m labels: severity: warning annotations: summary: "日志清理服务超过 5 分钟未运行" - alert: EmergencyCleanupDisabled expr: log_cleaner_emergency_enabled == 0 and log_cleaner_disk_usage_percent > 85 for: 1m labels: severity: critical annotations: summary: "紧急清理已关闭但磁盘超 85%,需立即人工介入" ``` --- ## 原始脚本分析与改进点 ### 原始脚本存在的问题 | 问题 | 风险等级 | 优化方案 | |----------------------------------------|------------|----------------------------------------------| | AWS AK/SK 硬编码在脚本中 | **严重** | 使用 IAM Role 或 EnvironmentFile 注入 | | 缺少 `set -euo pipefail` | 高 | 添加严格模式,避免静默失败 | | 使用 `eval` 拼接 find 命令 | 高 | 改用数组传参,消除命令注入风险 | | 无紧急清理独立开关 | 中 | 添加 `ENABLE_EMERGENCY_CLEANUP` 开关 | | 无文件锁,可能并发执行 | 中 | 添加 `flock` 锁 | | 无 dry-run 模式 | 中 | 添加 `DRY_RUN` 开关 | | 参数全部硬编码在脚本内 | 中 | 抽取为外部配置文件 | | 紧急清理无告警通知 | 中 | 添加飞书/Slack webhook 通知 | | 无优雅退出处理 | 低 | 添加 `trap` 信号处理 | | 紧急清理不区分步骤是否已解决问题 | 低 | 每步后检查磁盘,低于阈值则提前退出 | ### 版本差异对照 | 特性 | V1(基础版) | V2(增强版) | 本文档(优化版) | |------------------|----------------|--------------------|----------------------------------| | 磁盘阈值 | 单级(75%) | 双级(70%+85%) | 双级(可配置) | | 紧急清理开关 | 无 | 无(始终开启) | **`ENABLE_EMERGENCY_CLEANUP`** | | 文件保护 | 无 | `KEEP_FILES` 数组 | `KEEP_FILES`(外部配置) | | 紧急清理 | 无 | 三步激进清理 | 三步 + 每步检查 + 告警 | | 文件匹配 | 简单 grep | find + eval | find + 数组参数(安全) | | S3 上传 | 默认开启 | 默认关闭 | 可配置 | | 凭据管理 | 硬编码 | 硬编码 | IAM Role / EnvFile | | 并发保护 | 无 | 无 | flock 文件锁 | | 试运行 | 无 | 无 | DRY_RUN 模式 | | 信号处理 | 无 | 无 | trap SIGTERM/SIGINT | | 配置方式 | 脚本内 | 脚本内 | 外部 config.env |