Files
sionrui/script/deploy/deploy-all.sh
sion123 71ed87371b
Some checks failed
Build and Deploy / deploy (push) Has been cancelled
refactor(deploy): 升级生产部署脚本,增加优雅启停、健康检查与自动回滚
升级部署脚本为运维级实现,新增以下核心特性:
- 优雅启停 (SIGTERM → 等待 → SIGKILL 兜底)
- 健康检查 + 自动回滚
- 版本备份,保留最近 5 个版本
- 并发锁防止重复执行
- 前置检查 (磁盘/内存/端口/编译产物)
- 结构化日志

同时启用 Spring Boot 优雅关闭功能,配置 30 秒超时以等待请求和后台任务完成。
2026-06-03 22:22:05 +08:00

474 lines
16 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# ============================================
# 芋道 (Yudao) 生产部署脚本 - 运维级
# ============================================
# 特性:
# - 优雅启停 (SIGTERM → 等待 → SIGKILL 兜底)
# - 健康检查 + 自动回滚
# - 版本备份 + 保留最近 5 个版本
# - 并发锁,防止同时执行
# - 前置检查 (磁盘/内存/端口/编译产物)
# - 结构化日志
# ============================================
set -o pipefail
# ==================== 配置 ====================
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"
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 # 健康检查间隔(秒)
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"
# 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"
# 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"
# ==================== 工具函数 ====================
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; }
# ==================== 并发锁 ====================
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
echo $$ > "$DEPLOY_LOCK"
}
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 ""
log "╔══════════════════════════════════════════╗"
log "║ 用户端前端部署 ║"
log "╚══════════════════════════════════════════╝"
log "=== 构建 ==="
cd "$PROJECT_DIR/frontend"
log " pnpm install..."
pnpm install >> "$BUILD_LOG" 2>&1 || die "pnpm install 失败"
log " pnpm build:gold..."
pnpm build:gold >> "$BUILD_LOG" 2>&1 || die "pnpm build:gold 失败"
log "=== 部署 ==="
rm -rf "${FRONTEND_DIR:?}"/*
cp -r "$PROJECT_DIR/frontend/app/web-gold/dist/"* "$FRONTEND_DIR/"
success "用户端前端部署完成"
}
deploy_admin() {
log ""
log "╔══════════════════════════════════════════╗"
log "║ 管理后台前端部署 ║"
log "╚══════════════════════════════════════════╝"
log "=== 构建 ==="
cd "$PROJECT_DIR/yudao-ui-admin-vue3"
log " pnpm install..."
pnpm install >> "$BUILD_LOG" 2>&1 || die "pnpm install 失败"
log " pnpm build:prod..."
pnpm build:prod >> "$BUILD_LOG" 2>&1 || die "pnpm build:prod 失败"
log "=== 部署 ==="
rm -rf "${ADMIN_DIR:?}"/*
cp -r "$PROJECT_DIR/yudao-ui-admin-vue3/dist-prod/"* "$ADMIN_DIR/"
success "管理后台部署完成"
}
# ==================== 部署后摘要 ====================
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"
# 快速验证
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
# 磁盘
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 "$@"