refactor(deploy): 升级生产部署脚本,增加优雅启停、健康检查与自动回滚
Some checks failed
Build and Deploy / deploy (push) Has been cancelled

升级部署脚本为运维级实现,新增以下核心特性:
- 优雅启停 (SIGTERM → 等待 → SIGKILL 兜底)
- 健康检查 + 自动回滚
- 版本备份,保留最近 5 个版本
- 并发锁防止重复执行
- 前置检查 (磁盘/内存/端口/编译产物)
- 结构化日志

同时启用 Spring Boot 优雅关闭功能,配置 30 秒超时以等待请求和后台任务完成。
This commit is contained in:
2026-06-03 22:22:05 +08:00
parent 3a3638295b
commit 71ed87371b
2 changed files with 448 additions and 95 deletions

View File

@@ -1,127 +1,473 @@
#!/bin/bash
# ============================================
# 芋道 (Yudao) 全量部署脚本 - 生产增强版
# 芋道 (Yudao) 生产部署脚本 - 运维级
# ============================================
set -e
# 特性:
# - 优雅启停 (SIGTERM → 等待 → SIGKILL 兜底)
# - 健康检查 + 自动回滚
# - 版本备份 + 保留最近 5 个版本
# - 并发锁,防止同时执行
# - 前置检查 (磁盘/内存/端口/编译产物)
# - 结构化日志
# ============================================
set -o pipefail
# ==================== 1. 核心路径配置 ====================
PROJECT_DIR="/www/wwwroot/sionrui" # 源码目录
BACKEND_DIR="/www/wwwroot/yudao-server" # 后端运行目录
FRONTEND_DIR="/www/wwwroot/muyetools.cn" # 用户端前端 (web-gold)
ADMIN_DIR="/www/wwwroot/8.155.172.147" # 管理后台前端 (yudao-ui-admin-vue3)
# ==================== 配置 ====================
readonly PROJECT_DIR="/www/wwwroot/sionrui"
readonly BACKEND_DIR="/www/wwwroot/yudao-server"
readonly FRONTEND_DIR="/www/wwwroot/muyetools.cn"
readonly ADMIN_DIR="/www/wwwroot/8.155.172.147"
BACKEND_JAR_NAME="sion-rui.jar"
BUILD_LOG="/tmp/gitea_build_$(date +%Y%m%d).log"
readonly BACKEND_JAR="sion-rui.jar"
readonly BACKEND_PORT=9900
readonly BACKUP_KEEP=5 # 保留最近 N 个版本
readonly SHUTDOWN_GRACE_SEC=30 # 优雅关闭等待上限
readonly HEALTH_CHECK_RETRIES=30 # 健康检查重试次数 (30 × 2s = 60s)
readonly HEALTH_CHECK_INTERVAL=2 # 健康检查间隔(秒)
# ==================== 2. 环境强制指定 ====================
# 强制指定 JDK 17 (根据你的截图)
export JAVA_HOME="/www/server/java/jdk-17.0.8"
export PATH=$JAVA_HOME/bin:$PATH
readonly DEPLOY_LOCK="/tmp/yudao_deploy.lock"
readonly BUILD_LOG="/tmp/yudao_build_$(date +%Y%m%d_%H%M%S).log"
readonly DEPLOY_LOG="/tmp/yudao_deploy.log"
readonly BACKUP_DIR="$BACKEND_DIR/backups"
# 自动寻找 Maven 路径
MVN_EXEC=$(which mvn || find /www/server -name "mvn" | head -n 1 || echo "mvn")
# 限制内存防止 OOM设置 Maven 多线程加速编译
# Java 环境
export JAVA_HOME="/www/server/java/jdk-17.0.8"
export PATH="$JAVA_HOME/bin:$PATH"
export MAVEN_OPTS="-Xms512m -Xmx1024m"
export NODE_OPTIONS="--max-old-space-size=2048"
# 指定本地仓库(解决 www 用户权限问题)
# Maven
MVN_EXEC=$(which mvn 2>/dev/null || find /www/server -name "mvn" -type f 2>/dev/null | head -1 || echo "mvn")
MAVEN_REPO="$PROJECT_DIR/.m2_repo"
mkdir -p $MAVEN_REPO && chmod -R 777 $MAVEN_REPO
# ==================== 3. 工具函数 ====================
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a $BUILD_LOG
}
# ==================== 工具函数 ====================
log() { echo "[$(date '+%H:%M:%S')] $1" | tee -a "$DEPLOY_LOG"; }
success() { echo "[$(date '+%H:%M:%S')] $1" | tee -a "$DEPLOY_LOG"; }
warn() { echo "[$(date '+%H:%M:%S')] ⚠️ $1" | tee -a "$DEPLOY_LOG"; }
error() { echo "[$(date '+%H:%M:%S')] ❌ $1" | tee -a "$DEPLOY_LOG"; }
die() { error "$1"; release_lock; exit 1; }
# ==================== 4. 后端部署逻辑 ====================
deploy_backend() {
log "🚀 [后端] 开始部署流程..."
cd $PROJECT_DIR
log "1.1 同步最新代码..."
git fetch origin
git reset --hard origin/main
log "1.2 开始多线程构建 (4核加速)..."
# -T 1C 表示每个 CPU 核心一个线程,显著缩短 99% CPU 占用的时长
$MVN_EXEC clean package -DskipTests -pl yudao-server -am -T 1C -Dmaven.repo.local=$MAVEN_REPO >> $BUILD_LOG 2>&1
log "1.3 清理旧进程与文件锁..."
# 查找并强杀旧 JAR 进程
OLD_PID=$(ps -ef | grep "$BACKEND_JAR_NAME" | grep -v "grep" | awk '{print $2}')
[ -z "$OLD_PID" ] || kill -9 $OLD_PID
sleep 2
log "1.4 复制 JAR 包 (安全覆盖)..."
SOURCE_JAR="$PROJECT_DIR/yudao-server/target/$BACKEND_JAR_NAME"
if [ ! -f "$SOURCE_JAR" ]; then
log "❌ 编译失败:未在 $SOURCE_JAR 找到文件!请检查 $BUILD_LOG"
exit 1
# ==================== 并发锁 ====================
acquire_lock() {
if [ -f "$DEPLOY_LOCK" ]; then
local pid
pid=$(cat "$DEPLOY_LOCK" 2>/dev/null)
if ps -p "$pid" > /dev/null 2>&1; then
die "部署脚本已在运行中 (PID=$pid),请等待完成或手动删除 $DEPLOY_LOCK"
fi
warn "发现残留锁文件(进程 $pid 已不存在),清理后继续"
rm -f "$DEPLOY_LOCK"
fi
mkdir -p $BACKEND_DIR/logs
rm -f "$BACKEND_DIR/$BACKEND_JAR_NAME"
cp -f "$SOURCE_JAR" "$BACKEND_DIR/"
log "1.5 后台启动服务..."
cd $BACKEND_DIR
# 完全脱离终端重定向,防止脚本挂起
# 注意JVM参数(-Xms/-Xmx)必须在 -jar 之前
nohup $JAVA_HOME/bin/java -Xms512m -Xmx1024m -jar $BACKEND_JAR_NAME --server.port=9900 > ./logs/console.log 2>&1 &
log "✅ 后端启动成功 (PID: $!)"
echo $$ > "$DEPLOY_LOCK"
}
# ==================== 5. 用户端前端部署逻辑 ====================
release_lock() {
rm -f "$DEPLOY_LOCK"
}
# 确保退出时释放锁
trap release_lock EXIT INT TERM
# ==================== 前置检查 ====================
pre_flight() {
log "=== 前置检查 ==="
# 磁盘空间 (至少 2GB)
local avail
avail=$(df -m "$BACKEND_DIR" | awk 'NR==2 {print $4}')
if [ "${avail:-0}" -lt 2048 ]; then
die "磁盘空间不足:${BACKEND_DIR} 可用 ${avail}MB需要至少 2048MB"
fi
log " 磁盘可用: ${avail}MB ✓"
# 内存 (至少 512MB 空闲)
local mem_free
mem_free=$(free -m | awk '/Mem:/ {print $7}')
if [ "${mem_free:-0}" -lt 512 ]; then
warn "可用内存偏低:${mem_free}MB构建可能失败"
else
log " 可用内存: ${mem_free}MB ✓"
fi
# JDK
if [ ! -x "$JAVA_HOME/bin/java" ]; then
die "JDK 未找到:$JAVA_HOME/bin/java"
fi
log " JDK: $($JAVA_HOME/bin/java -version 2>&1 | head -1)"
# 项目目录
if [ ! -d "$PROJECT_DIR/.git" ]; then
die "项目目录不存在或不是 Git 仓库:$PROJECT_DIR"
fi
log " 项目目录: $PROJECT_DIR"
# 端口 (是否被非本应用的进程占用)
local port_pid
port_pid=$(ss -tlnp 2>/dev/null | grep ":$BACKEND_PORT " | grep -oP 'pid=\K\d+' || true)
if [ -n "$port_pid" ]; then
local port_cmd
port_cmd=$(ps -p "$port_pid" -o comm= 2>/dev/null || echo "unknown")
if ! echo "$port_cmd" | grep -q "java"; then
die "端口 $BACKEND_PORT 被非 Java 进程占用 (PID=$port_pid, cmd=$port_cmd)"
fi
log " 端口 $BACKEND_PORT: 被当前后端占用 (PID=$port_pid),将在停止阶段处理"
else
log " 端口 $BACKEND_PORT: 空闲 ✓"
fi
success "前置检查通过"
}
# ==================== 停止旧服务 ====================
stop_service() {
log "=== 停止旧服务 ==="
local old_pid
old_pid=$(ps -ef | grep "$BACKEND_JAR" | grep -v grep | awk '{print $2}')
if [ -z "$old_pid" ]; then
log " 未发现运行中的旧进程"
return 0
fi
# 记录旧进程信息用于回滚
local old_start
old_start=$(ps -p "$old_pid" -o lstart= 2>/dev/null || echo "unknown")
log " 旧进程: PID=$old_pid, 启动时间=$old_start"
# Step 1: SIGTERM 优雅关闭
log " 发送 SIGTERM ..."
kill -15 "$old_pid" 2>/dev/null || true
local waited=0
while [ $waited -lt $SHUTDOWN_GRACE_SEC ]; do
if ! ps -p "$old_pid" > /dev/null 2>&1; then
success "旧进程优雅退出 (PID=$old_pid, 耗时 ${waited}s)"
break
fi
sleep 1
waited=$((waited + 1))
# 每 10 秒汇报一次状态
if [ $((waited % 10)) -eq 0 ] && [ $waited -gt 0 ]; then
log " 等待中... (${waited}s/${SHUTDOWN_GRACE_SEC}s)"
fi
done
# Step 2: 仍未退出 → SIGKILL
if ps -p "$old_pid" > /dev/null 2>&1; then
warn "优雅关闭超时 (${SHUTDOWN_GRACE_SEC}s),执行 kill -9"
kill -9 "$old_pid" 2>/dev/null || true
sleep 3
if ps -p "$old_pid" > /dev/null 2>&1; then
die "无法杀死进程 PID=$old_pid,请手动处理"
fi
log " 进程已强制终止"
fi
# Step 3: 确认端口释放
local retries=10
while [ $retries -gt 0 ]; do
if ! ss -tlnp 2>/dev/null | grep -q ":$BACKEND_PORT "; then
log " 端口 $BACKEND_PORT 已释放 ✓"
return 0
fi
sleep 2
retries=$((retries - 1))
done
# 最后手段fuser 释放端口
warn "端口 $BACKEND_PORT 未自动释放,使用 fuser 强制释放"
fuser -k "${BACKEND_PORT}/tcp" 2>/dev/null || true
sleep 2
}
# ==================== 版本备份 ====================
backup_current() {
if [ -f "$BACKEND_DIR/$BACKEND_JAR" ]; then
mkdir -p "$BACKUP_DIR"
local backup_name="${BACKEND_JAR%.jar}_$(date +%Y%m%d_%H%M%S).jar"
cp "$BACKEND_DIR/$BACKEND_JAR" "$BACKUP_DIR/$backpack_name"
log " 已备份: $backup_name"
# 清理旧备份,只保留最近 N 个
local count
count=$(ls -1t "$BACKUP_DIR"/*.jar 2>/dev/null | wc -l)
if [ "$count" -gt "$BACKUP_KEEP" ]; then
ls -1t "$BACKUP_DIR"/*.jar | tail -n +$((BACKUP_KEEP + 1)) | xargs rm -f
log " 清理旧备份,保留最近 $BACKUP_KEEP"
fi
fi
}
# ==================== 回滚 ====================
rollback() {
error "=== 健康检查失败,开始回滚 ==="
# 停止新进程
local new_pid="$1"
if [ -n "$new_pid" ] && ps -p "$new_pid" > /dev/null 2>&1; then
log " 停止新进程 PID=$new_pid"
kill -9 "$new_pid" 2>/dev/null || true
sleep 2
fi
# 恢复最近的备份
local latest_backup
latest_backup=$(ls -1t "$BACKUP_DIR"/*.jar 2>/dev/null | head -1)
if [ -n "$latest_backup" ]; then
log " 恢复备份: $latest_backup"
cp -f "$latest_backup" "$BACKEND_DIR/$BACKEND_JAR"
else
error "没有可用的备份文件!"
return 1
fi
# 启动旧版本
log " 启动旧版本..."
cd "$BACKEND_DIR"
nohup "$JAVA_HOME/bin/java" -Xms512m -Xmx1024m \
-jar "$BACKEND_JAR" --server.port="$BACKEND_PORT" \
> ./logs/console.log 2>&1 &
local rollback_pid=$!
log " 回滚进程 PID=$rollback_pid"
# 等待旧版本启动
for i in $(seq 1 $HEALTH_CHECK_RETRIES); do
if curl -s -o /dev/null -w "%{http_code}" \
"http://127.0.0.1:${BACKEND_PORT}/actuator/health" 2>/dev/null | grep -q "200"; then
success "回滚成功 (PID=$rollback_pid)"
return 0
fi
if ! ps -p "$rollback_pid" > /dev/null 2>&1; then
break
fi
sleep $HEALTH_CHECK_INTERVAL
done
error "回滚失败!请手动检查!"
return 1
}
# ==================== 启动新服务 ====================
start_service() {
log "=== 启动新服务 ==="
cd "$BACKEND_DIR"
# 确保日志目录
mkdir -p "$BACKEND_DIR/logs"
nohup "$JAVA_HOME/bin/java" \
-Xms512m -Xmx1024m \
-jar "$BACKEND_JAR" --server.port="$BACKEND_PORT" \
> ./logs/console.log 2>&1 &
local new_pid=$!
log " 新进程 PID=$new_pid"
# 健康检查
log " 等待健康检查..."
local started=false
for i in $(seq 1 $HEALTH_CHECK_RETRIES); do
if ! ps -p "$new_pid" > /dev/null 2>&1; then
error "新进程已退出!最近日志:"
tail -30 "$BACKEND_DIR/logs/console.log" | while read -r line; do
error " $line"
done
rollback "$new_pid"
die "启动失败,已回滚到上一版本"
fi
if curl -s -o /dev/null -w "%{http_code}" \
"http://127.0.0.1:${BACKEND_PORT}/actuator/health" 2>/dev/null | grep -q "200"; then
success "后端启动成功 (PID=$new_pid, 耗时 $((i * HEALTH_CHECK_INTERVAL))s)"
started=true
break
fi
# 每 5 次汇报进度
if [ $((i % 5)) -eq 0 ]; then
log " 等待中... ($((i * HEALTH_CHECK_INTERVAL))s / $((HEALTH_CHECK_RETRIES * HEALTH_CHECK_INTERVAL))s)"
fi
sleep $HEALTH_CHECK_INTERVAL
done
if [ "$started" != "true" ]; then
error "健康检查超时"
rollback "$new_pid"
die "启动超时,已回滚到上一版本"
fi
}
# ==================== 后端部署 ====================
deploy_backend() {
log ""
log "╔══════════════════════════════════════════╗"
log "║ 后端部署 ║"
log "╚══════════════════════════════════════════╝"
# 0. 前置检查
pre_flight
# 1. 拉取代码 & 构建
log "=== 构建 ==="
cd "$PROJECT_DIR"
log " 拉取最新代码..."
git fetch origin 2>&1 | tee -a "$BUILD_LOG"
local old_commit new_commit
old_commit=$(git rev-parse --short HEAD)
git reset --hard origin/main 2>&1 | tee -a "$BUILD_LOG"
new_commit=$(git rev-parse --short HEAD)
if [ "$old_commit" != "$new_commit" ]; then
log " 代码更新: ${old_commit}${new_commit}"
else
log " 代码无变化 (${old_commit})"
fi
log " Maven 构建..."
mkdir -p "$MAVEN_REPO" && chmod -R 777 "$MAVEN_REPO" 2>/dev/null || true
if ! $MVN_EXEC clean package -DskipTests -pl yudao-server -am -T 1C \
-Dmaven.repo.local="$MAVEN_REPO" >> "$BUILD_LOG" 2>&1; then
error "Maven 构建失败查看tail -100 $BUILD_LOG"
die "构建失败"
fi
success "Maven 构建完成"
# 2. 检查产物
local source_jar="$PROJECT_DIR/yudao-server/target/$BACKEND_JAR"
if [ ! -f "$source_jar" ]; then
die "编译产物不存在:$source_jar"
fi
log " 产物大小: $(du -h "$source_jar" | cut -f1)"
# 3. 备份当前版本
backup_current
# 4. 停止旧服务
stop_service
# 5. 部署新 JAR
log "=== 部署 ==="
rm -f "$BACKEND_DIR/$BACKEND_JAR"
cp -f "$source_jar" "$BACKEND_DIR/"
log " JAR 已复制到 $BACKEND_DIR/"
# 6. 启动
start_service
log ""
success "后端部署完成 ($old_commit$new_commit)"
}
# ==================== 前端部署 ====================
deploy_frontend() {
log "🚀 [用户端前端] 开始部署流程..."
cd $PROJECT_DIR/frontend
log ""
log "╔══════════════════════════════════════════╗"
log "║ 用户端前端部署 ║"
log "╚══════════════════════════════════════════╝"
log "2.1 安装依赖 (pnpm)..."
pnpm install >> $BUILD_LOG 2>&1
log "=== 构建 ==="
cd "$PROJECT_DIR/frontend"
log "2.2 构建生产文件..."
pnpm build:gold >> $BUILD_LOG 2>&1
log " pnpm install..."
pnpm install >> "$BUILD_LOG" 2>&1 || die "pnpm install 失败"
log "2.3 刷新 Web 目录..."
rm -rf $FRONTEND_DIR/*
cp -r $PROJECT_DIR/frontend/app/web-gold/dist/* $FRONTEND_DIR/
log " pnpm build:gold..."
pnpm build:gold >> "$BUILD_LOG" 2>&1 || die "pnpm build:gold 失败"
log "✅ 用户端前端部署完成"
log "=== 部署 ==="
rm -rf "${FRONTEND_DIR:?}"/*
cp -r "$PROJECT_DIR/frontend/app/web-gold/dist/"* "$FRONTEND_DIR/"
success "用户端前端部署完成"
}
# ==================== 5.1 管理后台前端部署逻辑 ====================
deploy_admin() {
log "🚀 [管理后台] 开始部署流程..."
cd $PROJECT_DIR/yudao-ui-admin-vue3
log ""
log "╔══════════════════════════════════════════╗"
log "║ 管理后台前端部署 ║"
log "╚══════════════════════════════════════════╝"
log "3.1 安装依赖 (pnpm)..."
pnpm install >> $BUILD_LOG 2>&1
log "=== 构建 ==="
cd "$PROJECT_DIR/yudao-ui-admin-vue3"
log "3.2 构建生产文件..."
pnpm build:prod >> $BUILD_LOG 2>&1
log " pnpm install..."
pnpm install >> "$BUILD_LOG" 2>&1 || die "pnpm install 失败"
log "3.3 刷新管理后台目录..."
rm -rf $ADMIN_DIR/*
cp -r $PROJECT_DIR/yudao-ui-admin-vue3/dist-prod/* $ADMIN_DIR/
log " pnpm build:prod..."
pnpm build:prod >> "$BUILD_LOG" 2>&1 || die "pnpm build:prod 失败"
log "✅ 管理后台部署完成"
log "=== 部署 ==="
rm -rf "${ADMIN_DIR:?}"/*
cp -r "$PROJECT_DIR/yudao-ui-admin-vue3/dist-prod/"* "$ADMIN_DIR/"
success "管理后台部署完成"
}
# ==================== 6. 执行主流程 ====================
# 清理可能残留的构建进程
pkill -f "maven" || true
# ==================== 部署后摘要 ====================
print_summary() {
log ""
log "╔══════════════════════════════════════════╗"
log "║ 部署摘要 ║"
log "╚══════════════════════════════════════════╝"
log " 构建日志: $BUILD_LOG"
log " 部署日志: $DEPLOY_LOG"
log " 版本备份: $BACKUP_DIR/ ($(ls -1 "$BACKUP_DIR"/*.jar 2>/dev/null | wc -l) 个)"
log " 控制台日志: $BACKEND_DIR/logs/console.log"
case "$1" in
backend-only) deploy_backend ;;
frontend-only) deploy_frontend ;;
admin-only) deploy_admin ;;
*)
deploy_backend
deploy_frontend
deploy_admin
;;
esac
# 快速验证
if curl -s -o /dev/null -w "%{http_code}" \
"http://127.0.0.1:${BACKEND_PORT}/actuator/health" 2>/dev/null | grep -q "200"; then
success "健康检查: UP"
else
warn "健康检查: DOWN (可能仍在启动中)"
fi
log "🏁 [$(date '+%H:%M:%S')] 部署流水线全部执行完毕!"
# 磁盘
local disk_use
disk_use=$(df -h "$BACKEND_DIR" | awk 'NR==2 {print $5}')
log " 磁盘使用: ${disk_use}"
log ""
}
# ==================== 主流程 ====================
main() {
acquire_lock
# 清理残留 Maven 进程
pkill -f "maven" 2>/dev/null || true
echo "" >> "$DEPLOY_LOG"
log "═══════════════════════════════════════════"
log " 部署开始 (模式: ${1:-full})"
log "═══════════════════════════════════════════"
case "${1:-full}" in
backend-only)
deploy_backend
;;
frontend-only)
deploy_frontend
;;
admin-only)
deploy_admin
;;
backend|full|"")
deploy_backend
deploy_frontend
deploy_admin
;;
*)
echo "用法: $0 {backend-only|frontend-only|admin-only|full}"
release_lock
exit 1
;;
esac
print_summary
log "🏁 部署流水线全部执行完毕 ($(date '+%H:%M:%S'))"
}
main "$@"

View File

@@ -1,5 +1,12 @@
server:
port: 9900
# 优雅关闭:等待 HTTP 请求和后台任务完成后再退出
shutdown: graceful
spring:
lifecycle:
# 每个阶段最多等待 30 秒,给 CompletableFuture 和线程池排空的机会
timeout-per-shutdown-phase: 30s
--- #################### 数据库相关配置 ####################
spring: