提交 6b716808 authored 作者: 陈泽健's avatar 陈泽健

fix(自动化部署): auto_clean_deleted_ubains V3→V4 升级(仓库侧闭环)

- v4 脚本入库 + 逻辑说明文档(三级处置策略:truncate 零中断优先)
- 缺陷修正:容器归属读取上移至杀进程之前(/proc cgroup 进程死后即消失)
- auto_crontab_settings.sh 注册入口 v4 化(条目内容比对自动升级+awk 成对精准删除)
- README 定时脚本总览同步更新
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 39c373d2
......@@ -57,8 +57,8 @@ TASK_LOG[mysql_logs_backup]=""
TASK_STATUS[mysql_logs_backup]=true # 默认启用
# auto_clean 任务定义
TASK_CRON[auto_clean]="0 4 * * *"
TASK_SCRIPT[auto_clean]="/data/services/scripts/auto_clean_deleted_ubains_v3.sh"
TASK_CRON[auto_clean]="20 4 * * *"
TASK_SCRIPT[auto_clean]="/data/services/scripts/auto_clean_deleted_ubains_v4.sh"
TASK_LOG[auto_clean]=""
TASK_STATUS[auto_clean]=true # 默认启用
......@@ -300,8 +300,9 @@ function cleanup_crontab_jobs() {
# 检查脚本是否存在
if [[ ! -f "$script_path" ]]; then
log "WARN" "🧹 脚本不存在,移除定时任务: $task_name ($script_path)"
# 移除该任务(包括标记行和任务行)
grep -vF "$marker" "$crontab_file" | grep -vF "${TASK_SCRIPT[$task_name]}" > "$temp_file"
# 精准删除该 marker 行及其后紧跟的任务行(勿用 grep -v 全量过滤,
# 否则旧脚本路径的任务行(如 v3 残留)会变成无标记孤儿条目继续执行)
awk -v m="$marker" 'BEGIN{skip=0} { if (skip==1) {skip=0; next} if ($0==m) {skip=1; next} print }' "$crontab_file" > "$temp_file"
mv "$temp_file" "$crontab_file"
cleaned=true
fi
......@@ -426,10 +427,20 @@ function _add_single_cron_job() {
# 6. 检查是否已存在该任务(通过标记)
if grep -Fq "$marker" "$crontab_file"; then
# 读取 marker 后紧跟的任务条目行,判断是否与期望配置一致
local existing_job
existing_job=$(grep -A1 -F "$marker" "$crontab_file" | tail -1)
if [[ "$existing_job" == *"$script_path"* ]]; then
log "INFO" "✅ 定时任务已存在,跳过添加: $task_name"
rm -f "$crontab_file"
return 0
fi
# 条目已过期(如脚本升级 v3→v4、cron 时间变更),移除旧条目后重新添加
log "INFO" "🔄 任务条目已过期(期望: $script_path),自动升级: $task_name"
# 精准删除该 marker 行及其后紧跟的任务行(勿用 grep -v 全量过滤,避免误删)
awk -v m="$marker" 'BEGIN{skip=0} { if (skip==1) {skip=0; next} if ($0==m) {skip=1; next} print }' "$crontab_file" > "$crontab_file.new"
mv "$crontab_file.new" "$crontab_file"
fi
# 7. 生成任务条目
if [[ -n "$log_path" ]]; then
......
......@@ -33,7 +33,8 @@
| 脚本 | 清理对象 | 频率 | 逻辑文档 |
|-----|--------|------|---------|
| [auto_clean_deleted_ubains_v3.sh](auto_clean_deleted_ubains_v3.sh) | 已删除大文件(>1GB) | 每天4:00 | [auto_clean_deleted_ubains_v3_logic.md](auto_clean_deleted_ubains_v3_logic.md) |
| [auto_clean_deleted_ubains_v4.sh](auto_clean_deleted_ubains_v4.sh)(当前) | 已删除大文件(>100MB) | 每天4:20 | [auto_clean_deleted_ubains_v4_logic.md](auto_clean_deleted_ubains_v4_logic.md) |
| [auto_clean_deleted_ubains_v3.sh](auto_clean_deleted_ubains_v3.sh)(已停用,保留作回滚备份) | 已删除大文件(>1GB) | 每天4:00 | [auto_clean_deleted_ubains_v3_logic.md](auto_clean_deleted_ubains_v3_logic.md) |
---
......@@ -55,8 +56,9 @@
│ MySQL日志 ─→ gzip 压缩(保留30天) │
│ Nginx日志 ─→ gzip 压缩 + 清空原文件 + reopen(保留30天) │
│ │
│ 【磁盘清理】每天4:00 │
│ deleted大文件 ─→ kill进程 → 重启容器 → 释放磁盘 │
│ 【磁盘清理】每天4:20 │
│ deleted大文件 ─→ truncate原地清空(零中断) │
│ (V3旧方案 kill进程+重启容器 已停用,仅作兜底) │
│ │
└─────────────────────────────────────────────────────────────┘
```
......@@ -78,7 +80,7 @@
0 3 * * * /data/services/scripts/backup_nginx_logs.sh
# ============ 磁盘清理(凌晨)============
0 4 * * * /data/services/scripts/auto_clean_deleted_ubains_v3.sh
20 4 * * * /data/services/scripts/auto_clean_deleted_ubains_v4.sh
```
---
......@@ -160,7 +162,7 @@ check() 成功:
|-----|---------|------|
| ⚠️ 密码硬编码 | Redis/MySQL备份/Java(配置) | 改用环境变量或密钥管理 |
| ⚠️ 数据清理可能丢数据 | Redis(L3清理) | 仅作为最后兜底 |
| ⚠️ 无并发锁 | 备份脚本/clean脚本 | 增加 flock 或 PID 锁 |
| ⚠️ 无并发锁 | 备份脚本 | 增加 flock 或 PID 锁(clean 脚本 V4 已具备 flock) |
| ⚠️ Nacos原版被覆盖 | nacos-service.sh | 增强版更全面,可接受 |
| ⚠️ 无连续失败计数 | Nacos/Java | 借鉴 EMQX 的防抖机制 |
......
#!/bin/bash
#===============================================================================
# 脚本名称:auto_clean_deleted_ubains_v4.sh
# 功能描述:已删除大文件自动清理脚本(truncate 优先 · 精准处置版)
# 版本:V4.0
#
# 与 V3 的区别:
# 1. 超阈值文件优先 truncate 原地清空(不杀进程、不重启容器、服务零中断)
# 2. 仅 truncate 失败时才杀进程,且只重启进程真正所属的容器
# 3. 修正 APP_PATH 为 extapi 实际路径
# 4. 增加 flock 互斥锁,避免与 ujava2-startup.sh 并发竞争
# 5. 支持 DRY_RUN=1 灰度模式(只记录动作,不实际执行)
#
# 定时任务示例(建议 04:20,错开 ujava2-startup.sh 的整点轮询):
# 20 4 * * * /opt/scripts/auto_clean_deleted_ubains_v4.sh
#===============================================================================
# ================= 配置区域 =================
TARGET_KEY="ubains-INFO-AND-ERROR"
MIN_SIZE=$((100*1024*1024)) # 100MB 阈值(字节)
LOG_FILE="/var/log/scripts/auto_clean_deleted_ubains.log"
MAX_LOG_SIZE=$((5*1024*1024))
LOG_RETENTION_DAYS=7
APP_PATH="/data/services/api/java-meeting/java-meeting-extapi" # 修正:extapi 实际路径
APP_START_SCRIPT="${APP_PATH}/run.sh"
DRY_RUN=${DRY_RUN:-0} # DRY_RUN=1 时只记录不执行
LOCK_FILE="/var/run/auto_clean_deleted_ubains.lock"
# ===========================================
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
rotate_logs() {
log_dir=$(dirname "$LOG_FILE")
[ ! -d "$log_dir" ] && mkdir -p "$log_dir"
if [ -f "$LOG_FILE" ]; then
FILE_SIZE=$(stat -c%s "$LOG_FILE")
if [ "$FILE_SIZE" -ge "$MAX_LOG_SIZE" ]; then
mv "$LOG_FILE" "$LOG_FILE.$(date '+%Y%m%d%H%M%S')"
touch "$LOG_FILE"
fi
fi
find "$log_dir" -name "auto_clean_deleted_ubains.log.*" -mtime +$LOG_RETENTION_DAYS -exec rm -f {} \; 2>/dev/null
}
# ---- 互斥锁:同一时刻只允许一个实例 ----
exec 200>"$LOCK_FILE"
if ! flock -n 200; then
log "已有实例正在运行(锁:$LOCK_FILE),本次退出。"
exit 0
fi
# ---- 判断进程所属容器:输出容器名,宿主机进程输出空 ----
get_container_of_pid() {
local pid=$1 cid
cid=$(grep -oE '/docker/[0-9a-f]{64}' "/proc/$pid/cgroup" 2>/dev/null | head -1 | awk -F'/docker/' '{print $2}')
[ -z "$cid" ] && return 0
docker ps --format '{{.ID}} {{.Names}}' 2>/dev/null \
| awk -v c="${cid:0:12}" '$1 ~ "^"c {print $2; exit}'
}
rotate_logs
log "==============================================="
log "开始扫描 deleted 大文件 (V4: truncate 优先) DRY_RUN=$DRY_RUN"
log "==============================================="
FOUND=0
TRUNCATED=0
KILLED=0
RESTARTED_CONTAINERS=""
for fd_path in /proc/[0-9]*/fd/*; do
[ -L "$fd_path" ] || continue
target_file=$(readlink "$fd_path" 2>/dev/null)
[[ "$target_file" == *"(deleted)"* ]] && [[ "$target_file" == *"$TARGET_KEY"* ]] || continue
FOUND=1
pid=$(echo "$fd_path" | cut -d'/' -f3)
fd=$(echo "$fd_path" | cut -d'/' -f5)
[ -d "/proc/$pid" ] || continue
proc_name="unknown"
[ -f "/proc/$pid/comm" ] && proc_name=$(cat "/proc/$pid/comm")
size_bytes=$(stat -L -c %s "$fd_path" 2>/dev/null || echo 0)
size_mb=$((size_bytes / 1024 / 1024))
proc_cwd=$(readlink "/proc/$pid/cwd" 2>/dev/null)
# 归属容器必须在杀进程前读取(进程死后 /proc/<pid>/cgroup 即消失,届时无法判断)
container=$(get_container_of_pid "$pid")
log "-----------------------------------------------"
log "发现: 进程=$proc_name PID=$pid FD=$fd 大小=${size_mb}MB"
log " 文件=$target_file"
[ "$size_bytes" -ge "$MIN_SIZE" ] || { log "⏩ 不足 100MB,跳过"; continue; }
# ---------- 策略 1(首选):truncate 原地清空,进程无感知 ----------
if [ "$DRY_RUN" -eq 1 ]; then
log "[DRY_RUN] 将执行: : > $fd_path"
TRUNCATED=$((TRUNCATED+1))
continue
fi
if : > "$fd_path" 2>/dev/null; then
log "✔ 已原地清空(truncate),空间已释放,进程无需重启"
TRUNCATED=$((TRUNCATED+1))
continue
fi
log "⚠ truncate 失败,降级为杀进程"
# ---------- 策略 2(兜底):杀进程 + 精准重启归属容器 ----------
if [ "$DRY_RUN" -eq 1 ]; then
log "[DRY_RUN] 将杀进程 PID=$pid 并按归属重启"
continue
fi
kill -15 "$pid" 2>/dev/null
sleep 2
[ -d "/proc/$pid" ] && { kill -9 "$pid" 2>/dev/null; sleep 1; }
log "✔ 进程 $pid 已终止"
KILLED=$((KILLED+1))
if [ -n "$container" ]; then
log "➡ 进程属于容器 $container,仅重启该容器"
docker restart "$container" >> "$LOG_FILE" 2>&1 \
&& log "✔ 容器 $container 重启完成" \
|| log "❌ 容器 $container 重启失败"
RESTARTED_CONTAINERS="$RESTARTED_CONTAINERS $container"
elif [ "$proc_cwd" == "$APP_PATH" ]; then
log "➡ 进程属于宿主机应用 $APP_PATH,执行其启动脚本"
[ -f "$APP_START_SCRIPT" ] && bash "$APP_START_SCRIPT" >> "$LOG_FILE" 2>&1 \
&& log "✔ 应用已拉起" \
|| log "❌ 应用拉起失败,请检查 $APP_START_SCRIPT"
else
log "⚠ 进程为宿主机进程(cwd=$proc_cwd)且不在 APP_PATH 名单,仅记录不重启"
fi
done
[ "$FOUND" -eq 0 ] && log "未发现匹配的 deleted 文件。"
log "🎉 执行结束: 发现=$FOUND 清空=$TRUNCATED 杀进程=$KILLED 重启容器=[${RESTARTED_CONTAINERS:-}]"
log "==============================================="
# auto_clean_deleted_ubains_v4.sh 代码逻辑说明
## 一、脚本概述
| 项目 | 说明 |
|-----|------|
| 脚本名称 | auto_clean_deleted_ubains_v4.sh |
| 版本 | V4.0 |
| 功能 | 已删除大文件自动清理(truncate 优先 · 精准处置版) |
| 目标 | 释放 deleted 大文件占用的磁盘空间,**进程无感知、服务零中断** |
| 建议执行频率 | 每天凌晨4点20分(cron: `20 4 * * *`,错开 ujava2-startup.sh 整点轮询) |
| 前序版本 | [auto_clean_deleted_ubains_v3.sh](auto_clean_deleted_ubains_v3.sh)(V3.2,已保留作回滚备份) |
---
## 二、整体流程
```
flock 互斥锁(/var/run/auto_clean_deleted_ubains.lock,抢不到直接退出)
rotate_logs() 日志轮转(5MB轮转、保留7天)
扫描 /proc/[0-9]*/fd/*
├── 找符号链接 (deleted) + 关键字 "ubains-INFO-AND-ERROR"
├── < 100MB → 跳过
└── ≥ 100MB → 三级递进处置:
├── 策略1(首选):truncate 原地清空 /proc/<pid>/fd/<n>
│ └── 成功 → 结束(进程存活、零中断)
└── 策略2(truncate 失败兜底):杀进程(先 -15 后 -9)
├── 归属容器(/proc/<pid>/cgroup)→ 只 docker restart 该容器
├── 宿主机应用且 cwd == APP_PATH → bash run.sh 拉起
└── 其他宿主机进程 → 仅记录,不动
输出统计:发现=N 清空=N 杀进程=N 重启容器=[...]
```
---
## 三、核心逻辑
### 3.1 与 V3 的本质区别
> **释放 deleted 文件的空间 ≠ 杀死持有它的进程。**
`/proc/<pid>/fd/<n>` 执行 `: > 文件`(truncate),内核直接释放该 inode 的数据块,
进程完全无感知。V3 的"杀进程 + 重启整个容器"仅作为 truncate 失败时的最后兜底。
### 3.2 V3 → V4 变更对照
| 变更点 | V3(旧) | V4(新) |
|--------|----------|----------|
| 空间释放方式 | `kill -9` 杀进程 | **truncate 原地清空**,杀进程仅作兜底 |
| 容器重启范围 | 写死 `docker restart ujava2` | 按 `/proc/<pid>/cgroup` 判断,**只重启进程实际所属容器** |
| 阈值 | 1GB | 100MB(更早介入,减少堆积) |
| APP_PATH | 服务器旧版路径错误 | `/data/services/api/java-meeting/java-meeting-extapi`(extapi 实际路径) |
| 并发控制 | 无锁 | flock 互斥锁 |
| 灰度能力 | 无 | `DRY_RUN=1` 只记录不动手 |
| 执行统计 | 无 | 日志输出 发现/清空/杀进程/重启容器 计数 |
### 3.3 容器归属判断
```bash
get_container_of_pid() {
# 从 cgroup 提取容器完整 ID(兼容 cgroup v1/v2 的 /docker/<64位hex> 格式)
cid=$(grep -oE '/docker/[0-9a-f]{64}' "/proc/$pid/cgroup" | head -1)
# 用前 12 位短 ID 匹配 docker ps 输出,得到容器名
docker ps --format '{{.ID}} {{.Names}}' | awk -v c="${cid:0:12}" '$1 ~ "^"c {print $2}'
}
```
> ⚠️ **必须在杀进程之前调用**:进程死后 `/proc/<pid>/cgroup` 即消失,事后读取必然失败。
---
## 四、配置参数
| 参数 | 值 | 说明 |
|-----|---|------|
| TARGET_KEY | ubains-INFO-AND-ERROR | 匹配日志文件名关键字 |
| MIN_SIZE | 100MB | 最小触发清理的文件大小 |
| LOG_FILE | /var/log/scripts/auto_clean_deleted_ubains.log | 日志路径 |
| APP_PATH | /data/services/api/java-meeting/java-meeting-extapi | extapi 宿主机应用路径 |
| MAX_LOG_SIZE | 5MB | 日志轮转阈值 |
| DRY_RUN | 0(默认) | 置 1 时只记录将执行的动作,不做任何变更 |
| LOCK_FILE | /var/run/auto_clean_deleted_ubains.lock | flock 互斥锁文件 |
---
## 五、注意事项
1. **truncate 是首选路径**——正常情况下脚本永不杀进程、永不重启容器;
2. **兜底分支的容器归属判断必须在 kill 前完成**(见 3.3 警告);
3. **与 ujava2-startup.sh 的关系**——两者使用不同锁文件(各自单实例),
靠 cron 错峰(04:20 vs 整点对齐的每3分钟轮询)让行,而非互相阻塞;
4. **DRY_RUN 灰度**——修改阈值/关键字等配置后,建议先 `DRY_RUN=1` 跑一轮核对识别结果;
5. **治本方向**——deleted 文件的源头(删除者)未除,详见修复方案文档第 9 节后续建议。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论