提交 242408e5 authored 作者: PGY's avatar PGY

feat(自动化部署脚本): 添加新的自动清理已删除文件脚本并优化服务监控功能

添加了 auto_clean_deleted_ubains_v3.sh 脚本用于自动清理大于1GB的已删除文件,
该脚本会检测匹配特定关键字的deleted文件并自动处理相关进程,同时增加docker
容器重启和特定应用启动功能。
同时移除了旧的 cleanup_deleted_files.sh 脚本,并对
monitor_external_api_services_v2.sh 脚本进行了优化,增加了API健康检查
上级 66784c3b
#!/bin/bash
# ================= 配置区域 =================
TARGET_KEY="ubains-INFO-AND-ERROR"
MIN_SIZE=$((1024*1024*1024)) # 1GB (单位:字节)
LOG_FILE="/var/log/scripts/auto_clean_deleted_ubains.log"
MAX_LOG_SIZE=$((5*1024*1024)) # 5MB 日志大小限制
LOG_RETENTION_DAYS=7 # 日志保留天数
CONTAINER_NAME="ujava2" # 容器名称
APP_PATH="/var/www/java/external-meeting-api" # 特定应用路径
APP_START_SCRIPT="${APP_PATH}/run.sh" # 特定应用启动脚本
# ===========================================
# 日志函数
log() {
echo "[$(date
'+%Y-%m-%d %H:%M:%S
')] $*" | tee -a "$LOG_FILE"
}
# 日志轮转
rotate_logs() {
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"
log "日志文件超过 5MB,已自动轮转。"
fi
fi
find "$(dirname "$0")" -name "auto_clean_deleted_ubains.log.*" -mtime +$LOG_RETENTION_DAYS -exec rm -f {} \;
}
rotate_logs
log "==============================================="
log "开始扫描:检测 deleted 大文件并自动清理 (内核直读版)"
log "匹配关键字: $TARGET_KEY"
log "大于尺寸: 1GB"
log "==============================================="
FOUND=0
NEED_RESTART=0
NEED_APP_START=0 # 新增:标记是否需要启动特定应用
# 遍历所有进程的 fd 目录,寻找匹配关键字且标记为 deleted 的文件
for fd_path in /proc/[0-9]*/fd/*; do
# 检查是否为符号链接且指向包含关键字的已删除文件
if [ -L "$fd_path" ]; then
target_file=$(readlink "$fd_path" 2>/dev/null)
if [[ "$target_file" == *"(deleted)"* ]] && [[ "$target_file" == *"$TARGET_KEY"* ]]; then
FOUND=1
# 提取 PID 和 FD
pid=$(echo "$fd_path" | cut -d'/' -f3)
fd=$(echo "$fd_path" | cut -d'/' -f5)
# 获取进程名
proc_name="unknown"
[ -f "/proc/$pid/comm" ] && proc_name=$(cat "/proc/$pid/comm")
# 获取文件大小 (字节) - 使用 stat -L 获取链接指向的实际文件状态
size_bytes=$(stat -L -c %s "$fd_path" 2>/dev/null || echo 0)
size_mb=$((size_bytes / 1024 / 1024))
log "-----------------------------------------------"
log "发现匹配文件:"
log "进程: $proc_name"
log "PID: $pid"
log "FD: $fd"
log "文件: $target_file"
log "大小: $size_mb MB"
if [ "$size_bytes" -ge "$MIN_SIZE" ]; then
log "⚠ 文件超过1GB,执行自动处理。"
log "➡ 杀死进程 PID: $pid"
kill -9 "$pid" 2>/dev/null
sleep 1
NEED_RESTART=1
# 获取进程的当前工作目录,判断是否为特定应用
proc_cwd=$(readlink /proc/$pid/cwd 2>/dev/null)
if [ "$proc_cwd" == "$APP_PATH" ]; then
log "检测到被杀死的进程属于特定应用:$APP_PATH,将在容器重启后尝试启动。"
NEED_APP_START=1
fi
else
log "⏩ 文件不足 1GB,跳过。"
fi
fi
fi
done
if [ "$FOUND" -eq 0 ]; then
log "未发现匹配的 deleted 文件。"
fi
if [ "$NEED_RESTART" -eq 1 ]; then
log "➡ 检测到大文件进程已处理,统一重启 docker 容器:$CONTAINER_NAME"
# 检查 docker 命令是否存在
if command -v docker >/dev/null 2>&1; then
docker restart "$CONTAINER_NAME" >> "$LOG_FILE" 2>&1
log "✔ Docker 容器重启完成。"
else
log "❌ 错误: 未找到 docker 命令,请手动重启容器。"
fi
# 如果需要启动特定应用,则执行启动脚本
if [ "$NEED_APP_START" -eq 1 ]; then
log "➡ 启动特定应用脚本:$APP_START_SCRIPT"
if [ -f "$APP_START_SCRIPT" ]; then
bash "$APP_START_SCRIPT" >> "$LOG_FILE" 2>&1
log "✔ 特定应用启动脚本执行完成。"
else
log "❌ 错误: 未找到特定应用启动脚本:$APP_START_SCRIPT。"
fi
fi
else
log "无需重启 docker 容器。"
fi
log "🎉 执行结束。"
log "==============================================="
#!/bin/bash
#===============================================================================
# 脚本名称:cleanup_deleted_files.sh
# 功能描述:清理被进程占用的已删除文件脚本
# 版本:V1.0
# 创建日期:2026-01-27
# 基于文档:_PRD_预定系统_已删除文件清理需求文档.md
#
# 清理对象:
# 1. 使用lsof +L1查找link count为0但仍被进程打开的文件
# 2. 向相关进程发送SIGHUP信号尝试释放文件句柄
# 3. 避免强制kill进程,防止服务中断
# 4. 适用于Nginx、rsyslog等支持reload的服务
#
# 使用方法:
# chmod +x cleanup_deleted_files.sh
# ./cleanup_deleted_files.sh
#
# 定时任务示例:
# 0 4 * * * /opt/scripts/cleanup_deleted_files.sh
#===============================================================================
# clear_deleted_files.sh - 定时清理被进程占用的已删除文件
LOG_FILE="/var/log/scripts/cleanup_deleted_files.log"
DATE=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$DATE] 开始检查被占用的已删除文件..." >> "$LOG_FILE"
# 使用 lsof +L1 查找 link count 为 0(即已删除)但仍被打开的文件
DELETED_ENTRIES=$(lsof +L1 2>/dev/null | grep -v "COMMAND")
if [ -z "$DELETED_ENTRIES" ]; then
echo "[$DATE] 未发现被占用的已删除文件。" >> "$LOG_FILE"
exit 0
fi
echo "[$DATE] 发现以下被占用的已删除文件:" >> "$LOG_FILE"
echo "$DELETED_ENTRIES" >> "$LOG_FILE"
# 提取唯一 PID 列表
PIDS=$(echo "$DELETED_ENTRIES" | awk 'NR>1 {print $2}' | sort -u)
for PID in $PIDS; do
if ! kill -0 "$PID" 2>/dev/null; then
echo "[$DATE] PID $PID 已不存在,跳过。" >> "$LOG_FILE"
continue
fi
CMD=$(ps -p "$PID" -o comm= 2>/dev/null | tr -d ' ')
echo "[$DATE] 处理进程: PID=$PID, CMD=$CMD" >> "$LOG_FILE"
# 尝试发送 SIGHUP(适用于 Nginx、rsyslog 等支持 reload 的服务)
if kill -0 "$PID" 2>/dev/null; then
echo "[$DATE] 向 PID $PID 发送 SIGHUP 信号..." >> "$LOG_FILE"
kill -HUP "$PID" 2>/dev/null && continue
fi
# 若 SIGHUP 无效或不支持,记录警告(不强制 kill,避免服务中断)
echo "[$DATE] WARNING: 无法通过 SIGHUP 释放 PID $PID 的文件句柄。建议手动处理或配置 logrotate。" >> "$LOG_FILE"
done
echo "[$DATE] 检查完成。" >> "$LOG_FILE"
#!/bin/bash
#===============================================================================
# 脚本名称:monitor_external_api_services.sh
# 功能描述:外部API服务监测与自愈脚本
# 版本:V1.0
# 创建日期:2026-01-27
# 基于文档:_PRD_预定系统_外部API服务需求文档.md
#
# 监测对象:
# 1. ubains-meeting-api-1.0-SNAPSHOT.jar服务
# 2. malan服务
# 3. 宿主机进程运行状态
# 4. 服务目录和启动脚本存在性
#
# 恢复策略:
# 1. 检查进程是否运行(pgrep -f)
# 2. 验证服务目录是否存在
# 3. 检查启动脚本权限
# 4. 异常时自动启动服务
# 5. 启动后验证进程状态
#
# 使用方法:
# chmod +x monitor_external_api_services.sh
# ./monitor_external_api_services.sh
#
# 定时任务示例:
# */5 * * * * /opt/scripts/monitor_external_api_services.sh
#===============================================================================
# --- 配置区域 ---
# 日志文件路径
LOG_FILE="/var/log/scripts/monitor_external_api_services.log"
# 定义要监控的服务及其相关信息
# 格式: "进程名:目录路径:启动脚本路径"
SERVICES=(
"ubains-meeting-api-1.0-SNAPSHOT.jar:/var/www/java/external-meeting-api:/var/www/java/external-meeting-api/run.sh"
"malan:/var/www/malan:/var/www/malan/run.sh"
)
# --- 函数定义 ---
# 记录日志的函数
log_message() {
local message="$1"
echo "$(date '+%Y-%m-%d %H:%M:%S') - $message" >> "$LOG_FILE"
}
# 检查进程是否运行
is_process_running() {
local process_name="$1"
# 使用 pgrep -f 搜索命令行中包含该名称的进程
# 对于 .jar 文件,通常搜索的是 java -jar ...<jar_name>
# 对于其他程序,搜索其进程名
if [[ "$process_name" == *.jar ]]; then
# 如果是 JAR 文件,搜索 java 进程中包含该 jar 名称的
pgrep -f "java.*$process_name" > /dev/null
else
# 否则直接搜索进程名
pgrep -x "$process_name" > /dev/null
fi
if [ $? -eq 0 ]; then
return 0 # 进程运行中
else
return 1 # 进程未运行
fi
}
# 检查目录是否存在
is_directory_exists() {
local dir_path="$1"
[ -d "$dir_path" ]
}
# 检查启动脚本是否存在且可执行
is_script_executable() {
local script_path="$1"
[ -x "$script_path" ]
}
# 启动服务
start_service() {
local service_name="$1"
local dir_path="$2"
local script_path="$3"
log_message "尝试启动服务 '$service_name'..."
# 切换到服务目录并执行启动脚本
# 使用子shell (cd ...) 以确保不影响当前脚本的工作目录
(
cd "$dir_path" || { log_message "错误: 无法切换到目录 '$dir_path'"; return 1; }
# 检查启动脚本是否存在且可执行
if ! is_script_executable "$script_path"; then
log_message "错误: 启动脚本 '$script_path' 不存在或不可执行。"
return 1
fi
# 执行启动脚本
# 使用 nohup 将进程与终端脱离,使其在后台持续运行
# 输出重定向到 startup.log 或 /dev/null
nohup "./$(basename "$script_path")" > startup.log 2>&1 &
# 等待一小段时间,让进程有机会启动
sleep 600
# 再次检查进程是否启动成功
if is_process_running "$service_name"; then
log_message "服务 '$service_name' 启动成功。"
return 0
else
log_message "错误: 服务 '$service_name' 启动失败。"
return 1
fi
)
}
# --- 主逻辑 ---
# 初始化日志
log_message "=== 服务监控脚本开始执行 ==="
# 遍历所有服务
for service_info in "${SERVICES[@]}"; do
# 解析服务信息
IFS=':' read -r service_name dir_path script_path <<< "$service_info"
# 去除可能的前后空格
service_name=$(echo "$service_name" | xargs)
dir_path=$(echo "$dir_path" | xargs)
script_path=$(echo "$script_path" | xargs)
log_message "检查服务: $service_name (目录: $dir_path, 启动脚本: $script_path)"
# 检查目录是否存在
if ! is_directory_exists "$dir_path"; then
log_message "跳过服务 '$service_name': 目录 '$dir_path' 不存在。"
continue # 跳过当前循环,检查下一个服务
fi
# 检查进程是否运行
if is_process_running "$service_name"; then
log_message "服务 '$service_name' 正在运行。"
else
log_message "服务 '$service_name' 未运行。"
# 尝试启动服务
start_service "$service_name" "$dir_path" "$script_path"
fi
done
log_message "=== 服务监控脚本执行完毕 ==="
#!/bin/bash
#===============================================================================
# 脚本名称:monitor_external_api_services.sh
# 功能描述:外部 API 服务监测与自愈脚本 (严格精确匹配版)
# 版本:V2.1
# 创建日期:2026-03-05
#
# 核心策略:
# 1. Java Jar 包:精确匹配命令行中包含特定 Jar 文件名的 Java 进程。
# 2. 普通进程:严格使用 pgrep -x 进行全名匹配,拒绝模糊搜索。
# 3. 启动验证:轮询检测,确保进程真正拉起。
#
# 使用方法:
# chmod +x monitor_external_api_services.sh
# */5 * * * * /opt/scripts/monitor_external_api_services.sh >> /var/log/cron_monitor.log 2>&1
#===============================================================================
# --- 配置区域 ---
set -euo pipefail
# 日志配置
LOG_DIR="/var/log/scripts"
LOG_FILE="${LOG_DIR}/monitor_external_api_services.log"
# 监控参数
STARTUP_TIMEOUT=600 # 最大等待启动时间 (秒)
POLL_INTERVAL=3 # 状态轮询间隔 (秒)
STOP_WAIT_TIME=30 # 停止残留进程的等待时间 (秒)
# 定义要监控的服务
# 格式: "进程标识:目录路径:启动脚本路径"
# 注意:
# - 对于 Jar 包,'进程标识' 必须是 Jar 文件的完整名称 (如 app-1.0.jar)
# - 对于普通程序,'进程标识' 必须是准确的进程名 (如 nginx, malan)
# 日志文件路径
LOG_FILE="/var/log/scripts/monitor_external_api_services.log"
# 定义要监控的服务及其相关信息
# 格式: "服务名:目录路径:启动脚本路径"
SERVICES=(
"ubains-meeting-api-1.0-SNAPSHOT.jar:/var/www/java/external-meeting-api:/var/www/java/external-meeting-api/run.sh"
"malan:/var/www/malan:/var/www/malan/run.sh"
)
# API 检查配置
API_URL="https://127.0.0.1/exapi/system/v2/login"
API_DATA='{
"account": "test",
"password": "test"
}'
# --- 函数定义 ---
# 记录日志的函数
log_message() {
local level="$1"
local message="$2"
local ts
ts=$(date '+%Y-%m-%d %H:%M:%S')
[ ! -d "$LOG_DIR" ] && mkdir -p "$LOG_DIR" 2>/dev/null || true
echo "${ts} [${level}] ${message}" >> "$LOG_FILE"
if [[ "$level" == "ERROR" || "$level" == "FATAL" ]]; then
echo "${ts} [${level}] ${message}" >&2
fi
local message="$1"
echo "$(date '+%Y-%m-%d %H:%M:%S') - $message" >> "$LOG_FILE"
}
# 获取进程 PID (严格模式)
get_process_pid() {
local identifier="$1"
local pid=""
if [[ "$identifier" == *.jar ]]; then
# 【Java 策略】
# 必须匹配到 'java' 命令且命令行参数中包含该具体的 jar 文件名
# 使用 -f 是因为 java 进程的 cmdline 包含 jar 路径,但我们要确保 jar 名完全匹配
# 这里的正则确保 jar 文件名前后有边界 (空格或结束符),防止部分匹配
pid=$(pgrep -f "java.*${identifier}" | head -n 1)
# 检查服务是否正常(通过 API 请求)
is_service_healthy() {
local service_name="$1"
# 使用 curl 发送 POST 请求
# -k: 忽略 SSL 证书检查
# -s: 静默模式
# -o /dev/null: 不输出响应体
# -w "%{http_code}": 只输出 HTTP 状态码
# --max-time 10: 设置超时时间为 10 秒
local http_code=$(curl -k -s -o /dev/null -w "%{http_code}" \
--location --request POST "$API_URL" \
--header 'Content-Type: application/json' \
--data-raw "$API_DATA" \
--max-time 10)
if [ "$http_code" -eq 200 ]; then
return 0 # 服务正常
else
# 【普通进程策略】
# 严格使用 -x (exact match),只匹配进程名完全等于 identifier 的进程
# 绝不进行模糊搜索
pid=$(pgrep -x "$identifier" | head -n 1)
log_message "警告: 服务 '$service_name' 接口返回状态码: $http_code"
return 1 # 服务异常
fi
echo "$pid"
}
# 检查进程是否存在
is_process_running() {
local identifier="$1"
local pid
pid=$(get_process_pid "$identifier")
[ -n "$pid" ]
# 检查目录是否存在
is_directory_exists() {
local dir_path="$1"
[ -d "$dir_path" ]
}
# 清理残留进程
cleanup_stale_process() {
local identifier="$1"
local pid
pid=$(get_process_pid "$identifier")
if [ -n "$pid" ]; then
log_message "WARN" "发现残留进程 '${identifier}' (PID: ${pid}),正在清理..."
# 尝试优雅停止
kill "$pid" 2>/dev/null || true
local count=0
while kill -0 "$pid" 2>/dev/null && [ $count -lt $STOP_WAIT_TIME ]; do
sleep 1
((count++))
done
# 强制杀死
if kill -0 "$pid" 2>/dev/null; then
log_message "WARN" "进程未响应,强制杀死 (PID: ${pid})"
kill -9 "$pid" 2>/dev/null || true
sleep 1
fi
log_message "INFO" "残留进程已清理完毕。"
fi
# 检查启动脚本是否存在且可执行
is_script_executable() {
local script_path="$1"
[ -x "$script_path" ]
}
# 启动服务并验证
# 启动服务
start_service() {
local name="$1"
local dir="$2"
local script="$3"
local service_name="$1"
local dir_path="$2"
local script_path="$3"
log_message "INFO" ">>> 开始启动服务: ${name}"
log_message "尝试启动服务 '$service_name'..."
# 1. 环境校验
if [ ! -d "$dir" ]; then
log_message "ERROR" "启动失败:目录不存在 -> ${dir}"
return 1
fi
# 切换到服务目录并执行启动脚本
(
cd "$dir_path" || { log_message "错误: 无法切换到目录 '$dir_path'"; return 1; }
if [ ! -x "$script" ]; then
log_message "ERROR" "启动失败:脚本不可执行 -> ${script}"
# 检查启动脚本是否存在且可执行
if ! is_script_executable "$script_path"; then
log_message "错误: 启动脚本 '$script_path' 不存在或不可执行。"
return 1
fi
# 2. 清理可能占用的旧进程
cleanup_stale_process "$name"
# 3. 执行启动
cd "$dir" || { log_message "ERROR" "无法切换目录: ${dir}"; return 1; }
# 执行启动脚本
nohup "./$(basename "$script_path")" > startup.log 2>&1 &
local startup_log="${dir}/startup_last.log"
# 等待一段时间,让服务有足够时间启动并响应 API
log_message "等待服务启动 (300秒)..."
sleep 300
# 使用 setsid 独立运行
setsid "$script" > "$startup_log" 2>&1 &
log_message "INFO" "启动命令已发送,进入状态轮询 (最多 ${STARTUP_TIMEOUT}s)..."
# 4. 轮询验证 (严格匹配)
local elapsed=0
while [ $elapsed -lt $STARTUP_TIMEOUT ]; do
sleep $POLL_INTERVAL
elapsed=$((elapsed + POLL_INTERVAL))
if is_process_running "$name"; then
local running_pid
running_pid=$(get_process_pid "$name")
log_message "INFO" "✅ 服务 '${name}' 启动成功 (PID: ${running_pid})"
# 再次检查服务是否正常
if is_service_healthy "$service_name"; then
log_message "服务 '$service_name' 启动并验证成功。"
return 0
fi
done
log_message "ERROR" "❌ 服务 '${name}' 启动超时!请检查日志: ${startup_log}"
else
log_message "错误: 服务 '$service_name' 启动后接口验证仍失败。"
return 1
fi
)
}
# --- 主逻辑 ---
main() {
log_message "INFO" "========== 监控周期开始 =========="
for entry in "${SERVICES[@]}"; do
IFS=':' read -r identifier work_dir start_script <<< "$entry"
# 初始化日志
log_message "=== 服务监控脚本开始执行 (API 检查模式) ==="
# 去除空格
identifier=$(echo "$identifier" | xargs)
work_dir=$(echo "$work_dir" | xargs)
start_script=$(echo "$start_script" | xargs)
# 遍历所有服务
for service_info in "${SERVICES[@]}"; do
# 解析服务信息
IFS=':' read -r service_name dir_path script_path <<< "$service_info"
# 去除可能的前后空格
service_name=$(echo "$service_name" | xargs)
dir_path=$(echo "$dir_path" | xargs)
script_path=$(echo "$script_path" | xargs)
[ -z "$identifier" ] && continue
log_message "检查服务: $service_name"
log_message "DEBUG" "检测目标: ${identifier}"
# 检查目录是否存在
if ! is_directory_exists "$dir_path"; then
log_message "跳过服务 '$service_name': 目录 '$dir_path' 不存在。"
continue
fi
if is_process_running "$identifier"; then
local pid
pid=$(get_process_pid "$identifier")
log_message "INFO" "正常: ${identifier} (PID: ${pid})"
# 检查服务是否正常
if is_service_healthy "$service_name"; then
log_message "服务 '$service_name' 运行正常 (HTTP 200)。"
else
log_message "WARN" "异常: ${identifier} 未运行,触发自愈..."
if ! start_service "$identifier" "$work_dir" "$start_script"; then
log_message "ERROR" "自愈失败: ${identifier},等待下一周期。"
# TODO: 此处可集成钉钉/邮件报警
fi
log_message "服务 '$service_name' 响应异常或未运行。"
# 尝试启动服务
start_service "$service_name" "$dir_path" "$script_path"
fi
done
log_message "INFO" "========== 监控周期结束 =========="
}
done
main || log_message "FATAL" "脚本执行异常退出"
\ No newline at end of file
log_message "=== 服务监控脚本执行完毕 ==="
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论