自动化部署方案:基于Git监控的后端服务动态部署实践

2025年12月18日33 次阅读0 人喜欢
自动化部署DevOpsBash脚本Git监控NginxJava部署

自动化部署方案:基于Git监控的后端服务动态部署实践

前言

在日常开发中,频繁的手动部署和端口管理往往成为开发效率的瓶颈。特别是在多后端服务环境下,如何实现代码更新的自动检测、构建部署,以及灵活的端口切换,是一个值得深入思考的问题。

本文将介绍一套完整的自动化部署解决方案,通过Bash脚本实现Git代码监控、自动构建、服务重启,并结合Nginx实现端口动态代理,让前端无需修改配置即可灵活切换后端服务。

核心设计思想

1. Git驱动的自动化流程

  • 代码监控:定时检查远程develop分支的提交变化
  • 自动拉取:检测到新代码后自动执行git merge origin/develop
  • 智能构建:使用Maven重新构建应用,跳过测试以提升效率
  • 无缝重启:自动停止旧进程,启动新进程,确保服务连续性

2. 端口动态代理架构

通过Nginx的upstream机制,实现前端代理的灵活性:

nginx 复制代码
upstream ceec_back_server {
    # 可以动态切换不同的后端服务实例
    server 172.18.1.28:18084;  # 当前活跃的服务
    # server 172.18.1.14:18084;  # 备用服务
    # server 172.18.1.16:18084;  # 开发环境
}

关键优势

  • 前端配置零改动,只需修改Nginx的upstream配置
  • 支持多环境快速切换(开发、测试、生产)
  • 实现灰度发布和A/B测试

脚本功能详解

核心监控机制

bash 复制代码
# 每5分钟检查一次代码更新
CHECK_INTERVAL=300

# 通过hash值比对,避免重复构建
REMOTE_DEVELOP_HASH=$(git rev-parse origin/develop)
PREVIOUS_HASH=$(cat "$HASH_FILE")

if [ "$REMOTE_DEVELOP_HASH" != "$PREVIOUS_HASH" ]; then
    # 执行构建、重启流程
    execute_build_command
    execute_kill_command
    execute_start_command
fi

完整命令集

命令 功能 使用场景
./monit.sh watch 启动持续监控 开发环境自动部署
./monit.sh start 手动启动应用 首次启动或重启
./monit.sh build 手动构建应用 代码更新后手动构建
./monit.sh force 强制更新重启 紧急修复或版本回滚
./monit.sh status 查看状态 监控运行状态
./monit.sh kill 停止应用 手动停止服务
./monit.sh log 查看监控日志 调试监控过程
./monit.sh java-log 查看应用日志 调试应用运行

智能通知系统

集成Mac原生通知系统,在关键节点推送状态信息:

bash 复制代码
send_mac_notification() {
    osascript -e "display notification \"$message\" with title \"$title\" subtitle \"$subtitle\""
}

通知场景

  • ✅ 应用启动成功
  • ? 检测到代码更新
  • ❌ 构建失败
  • ? 强制更新完成

技术亮点

1. 进程管理优化

  • 使用nohup确保进程后台持续运行
  • PID文件管理,避免重复启动
  • 进程检查机制,防止僵尸进程

2. 构建信息追踪

bash 复制代码
# 记录详细的构建信息
echo "$CURRENT_TIME||$COMMIT_HASH||$AUTHOR||$COMMIT_MSG||$COMMIT_TIME" > "$BUILD_INFO_FILE"

3. 错误处理机制

  • 构建失败时阻止重启,保持服务稳定
  • 详细的错误日志记录
  • 失败通知推送

4. 前端部署支持

bash 复制代码
./monit.sh front
  • 自动备份前端构建产物
  • 时间戳版本管理
  • 软链接快速切换

完整脚本代码

以下是经过脱敏处理的完整自动化部署脚本:

bash 复制代码
#!/bin/bash
# 根目录
ROOT="/path/to/project"
# bin目录
BIN_PATH="$ROOT/bin"

# 前端代码仓库地址
FRONT_PATH="$ROOT/project-front"

# 设置Git仓库的路径
GIT_REPO_PATH="$ROOT/project-back"

PID_FILE="$ROOT/bin/monit.pid"  # 进程ID文件,用于存储监控进程的PID

LOG_FILE="$ROOT/bin/monit.logs"  # 日志文件,用于记录监控过程中的信息

# 设置记录文件的路径
HASH_FILE="$ROOT/bin/.last_develop_hash"  # 存储上次开发分支的提交hash值

# Java进程标识符,用于pgrep搜索
JAVA_PROCESS_IDENTIFIER="app.jar"

# 确保输出文件路径存在或nohup.out将不会被创建
OUTPUT_FILE="$ROOT/bin/nohup.out"  # nohup输出文件

JAR_FILE_PATH="$ROOT/project-back/app-webapp/app-admin/target/app.jar"  # JAR文件的路径

# 执行的jar包路径
EXEC_JAR_FILE_PATH="$BIN_PATH/app.jar"  # 当前执行的JAR文件路径

# 定时检查的间隔
CHECK_INTERVAL=300  # 检查间隔时间(秒)

# 在文件开头的配置部分添加新的记录文件路径
BUILD_INFO_FILE="$ROOT/bin/.last_build_info"  # 存储上次成功构建的信息

# 检查是否已经有Java进程在运行
is_java_running() {
  pgrep -f "$JAVA_PROCESS_IDENTIFIER" >/dev/null  # 检查Java进程是否存在
  return $?  # 返回检查结果
}

execute_pull_command() {
  eval "git merge origin/develop"  # 执行git合并命令
}

execute_build_command() {
  eval "mvn clean && mvn clean package -Dmaven.test.skip=true"  # 执行Maven构建命令
  if [ $? -ne 0 ]; then  # 检查上一个命令的返回值
    echo "Error: Build failed. The JAR file will not be moved."  # 构建失败提示
    send_mac_notification "Build Failure" "app.jar" "The build process failed. The application will not be restarted."  # 发送构建失败通知
    return 1  # 返回错误
  fi
  
  # 记录构建信息
  CURRENT_TIME=$(date "+%Y-%m-%d %H:%M:%S")
  COMMIT_HASH=$(git rev-parse HEAD)
  AUTHOR=$(git log -1 --pretty=format:"%an")
  COMMIT_MSG=$(git log -1 --pretty=format:"%s")
  COMMIT_TIME=$(git log -1 --pretty=format:"%ar")
  echo "$CURRENT_TIME||$COMMIT_HASH||$AUTHOR||$COMMIT_MSG||$COMMIT_TIME" > "$BUILD_INFO_FILE"
  
  # 打包后的产物移动到bin目录
  mv "$JAR_FILE_PATH" "$BIN_PATH/"  # 移动构建生成的JAR文件
  EXEC_JAR_FILE_PATH="$BIN_PATH/$(basename "$JAR_FILE_PATH")"  # 更新JAR文件路径
}

# 检查jar包是否存在,不存在则返回false
check_jar_exists() {
  if [ -f "$EXEC_JAR_FILE_PATH" ]; then  # 检查JAR文件是否存在
    return 0  # 存在
  else
    return 1  # 不存在
  fi
}

execute_kill_command() {
  eval "pkill -f 'java.*app.jar'"  # 杀死指定的Java进程
}

# 定义函数:执行启动命令
execute_start_command() {
  eval "nohup java -jar $EXEC_JAR_FILE_PATH > '${OUTPUT_FILE}' 2>&1 &"  # 启动Java应用并将输出重定向到nohup文件
}

send_mac_notification() {
  local title="$1"  # 通知标题
  local subtitle="$2"  # 通知副标题
  local message="$3"  # 通知内容
  osascript -e "display notification \"$message\" with title \"$title\" subtitle \"$subtitle\""  # 发送Mac通知
}

# 当检测到新的代码更新时发送通知
notify_code_update() {
  echo "New commits detected in remote develop branch."  # 输出检测到的新提交信息
  send_mac_notification "Code Update" "develop branch" "New code updates have been detected and the build process has started."  # 发代码更新通知
}

# 构建应用
build_application() {
  # 拉取最新的远程分支信息
  # echo "Fetching the latest changes from remote..."
  git fetch --quiet  # 拉取远程分支信息

  # 获取远程develop分支的最新提交hash值
  REMOTE_DEVELOP_HASH=$(git rev-parse origin/develop)  # 获取最新提交的hash值

  # 读取之前记录的hash值
  if [ -f "$HASH_FILE" ]; then
    PREVIOUS_HASH=$(cat "$HASH_FILE")  # 读取上次记录的hash值
  else
    PREVIOUS_HASH=""  # 如果没有记录,则为空
  fi

  # 比较最新提交的hash值是否发生变化
  if [ "$REMOTE_DEVELOP_HASH" != "$PREVIOUS_HASH" ]; then
    CURRENT_TIME=$(date "+%Y-%m-%d %H:%M:%S")  # 获取当前时间

    notify_code_update  # 发送代码更新通知
    echo "$CURRENT_TIME New commits detected in remote develop branch."  # 输出检测到的新提交信息

    # 更新记录文件
    echo "$REMOTE_DEVELOP_HASH" >"$HASH_FILE"  # 更新hash记录文件
    # rebase 拉代码
    execute_pull_command  # 执行拉取命令
    # 执行构建和启动命令
    if execute_build_command; then  # 如果构建成功
      # 杀死当前的
      execute_kill_command  # 杀死当前运行的Java进程
      # 启动新的
      execute_start_command  # 启动新的Java进程
    fi
  fi
}

# 启动脚本
start_application() {
  if is_java_running; then  # 检查Java进程是否在运行
    echo "Java process is already running."  # 如果在运行,输出提示
  else
    # 检查jar包是否存在 不存在先build
    if ! check_jar_exists; then  # 如果JAR文件不存在
      echo "No jar file found. Building the application..."  # 输出提示信息
      execute_build_command  # 执行构建命令
    fi

    execute_start_command  # 启动Java应用
    echo "Application started successfully."  # 输出启动成功信息
    send_mac_notification "Application Start" "app.jar" "The application has been started successfully."  # 发送启动成功通知
  fi
}

watch_application() {
  # 当前时间
  CURRENT_TIME=$(date "+%Y-%m-%d %H:%M:%S")  # 获取当前时间
  echo "$CURRENT_TIME Watching for new code updates..."  # 输出监控信息
  # 无限循环,每分钟执行一次检查
  while true; do
    build_application  # 执行构建应用
    sleep $CHECK_INTERVAL  # 等待指定的检查间隔
  done
}

stop_watching() {
  # 检查PID文件是否存在
  if [ -f "$PID_FILE" ]; then
    # 读取PID文件中的进程ID
    PID=$(cat "$PID_FILE")  # 读取PID
    # 使用kill命令终止进程
    if kill -TERM "$PID" 2>/dev/null; then  # 尝试终止进程
      CURRENT_TIME=$(date "+%Y-%m-%d %H:%M:%S")  # 获取当前时间
      msg="$CURRENT_TIME Monitoring process with PID $PID has been terminated."  # 输出终止信息
      echo "$msg"  # 输出信息
      # 输出到日志文件
      echo "$msg" >>"$LOG_FILE"  # 记录到日志文件
      # 确认进程已终止后,删除PID文件
      rm -f "$PID_FILE"  # 删除PID文件
    else
      echo "Failed to terminate process with PID $PID."  # 输出终止失败信息
    fi
  else
    echo "No process is currently being watched."  # 如果没有监控进程,输出提示
  fi
}

# 在case语句之前添加新的强制更新函数
force_update() {
  CURRENT_TIME=$(date "+%Y-%m-%d %H:%M:%S")
  echo "$CURRENT_TIME 开始强制更新..."
  
  # 删除当前运行的jar包
  if [ -f "$EXEC_JAR_FILE_PATH" ]; then
    rm -f "$EXEC_JAR_FILE_PATH"
    echo "已删除现有jar包"
  fi
  
  # 拉取最新代码
  execute_pull_command
  
  # 杀死当前运行的进程
  if is_java_running; then
    execute_kill_command
    echo "已停止现有Java进程"
  fi
  
  # 重新构建并启动
  if execute_build_command; then
    execute_start_command
    echo "应用已重新启动"
    send_mac_notification "Force Update" "app.jar" "应用已完成强制更新并重启"
  fi
}

# 进入Git仓库目录
cd $GIT_REPO_PATH  # 切换到Git仓库目录

# 检查是否有足够的参数
if [ $# -eq 0 ]; then
  echo "Error: No argument provided."  # 如果没有参数,输出错误信息
  echo "Usage: $0 {--watch|--unwatch|--kill|--log|--java-log|--pull|--force}"  # 输出用法提示
  exit 1  # 退出脚本
fi

# 使用case语句处理不同的参数
case "$1" in
pull)
  execute_pull_command  # 执行拉取命令
  ;;
start)
  start_application  # 启动应用
  ;;
watch)
  # 启动监控任务
  echo "Starting the monitoring task in the background."  # 输出启动监控任务信息
  start_application  # 启动应用
  stop_watching  # 停止监控
  watch_application >>"$LOG_FILE" 2>&1 &  # 启动监控并将输出重定向到日志文件
  echo $! >"$PID_FILE"  # 将PID写入PID文件
  disown  # 使进程在后台运行
  ;;
build)
  build_application  # 执行构建应用
  ;;
unwatch)
  # 停止--watch 的pid
  stop_watching  # 停止监控
  ;;
kill)
  # 杀死java 进程
  if is_java_running; then  # 检查Java进程是否在运行
    execute_kill_command  # 杀死Java进程
  else
    echo "Java process is not running."  # 如果不在运行,输出提示
  fi
  ;;
log)
  # 显示日志文件内容
  tail -n 100 -f "$LOG_FILE"  # 输出日志文件的最后100行
  ;;
status)
  if is_java_running; then  # 检查Java进程是否在运行
    echo "Java process is already running."  # 如果在运行,输出提示
  else
    echo "Java process is not running."  # 如果不在运行,输出提示
  fi
  if [ -f "$PID_FILE" ]; then  # 检查PID文件是否存在
    echo "Monitoring process is currently being watched with PID $(cat "$PID_FILE")."  # 输出监控进程信息
  else
    echo "Monitoring process is not currently being watched."  # 如果没有监控进程,输出提示
  fi
  
  # 显示最新的develop分支提交信息
  echo -e "\n[Latest Develop Branch Commit]"
  git log --oneline -1 origin/develop --pretty=format:"%an: %s (%ar)"
  
  # 显示最后构建信息
  if [ -f "$BUILD_INFO_FILE" ]; then
    echo -e "\n[Last Successful Build Info]"
    IFS='||' read -r BUILD_TIME COMMIT_HASH AUTHOR COMMIT_MSG COMMIT_TIME < "$BUILD_INFO_FILE"
    
    echo "Build Time: $BUILD_TIME"
    echo "Commit: ${COMMIT_HASH:0:8}"
    echo "Author: $AUTHOR"
    echo "Message: $COMMIT_MSG"
    echo "Committed: $COMMIT_TIME"
  else
    echo -e "\n[No build information available]"
  fi
  ;;
java-log)
  # tail -f 并且查看最后100行
  tail -n 100 -f "$OUTPUT_FILE"  # 输出nohup文件的最后100行
  ;;
front)
  # cd $FRONT_PATH
  # git pull
  # npm run build:prod
  # 日期变量
  DATE=$(date "+%Y%m%d_%H%M_%S")  # 获取当前日期时间
  # 拷贝最新的文件名 dist_日期
  newFileName="dist_$DATE"  # 新文件名
  cp -r $FRONT_PATH/dist $BIN_PATH/$newFileName  # 拷贝前端构建文件
  # 创建一个软连接 dist 连接到新的文件夹 清除旧的
  ln -s $BIN_PATH/$newFileName/ $BIN_PATH/dist  # 创建软连接
  echo "font copy success"  # 输出拷贝成功信息
  ;;
force)
  force_update
  ;;
*)
  # 参数不匹配时的错误信息
  echo "Error: Invalid argument '$1'."  # 输出无效参数错误信息
  echo "Usage: $0 {watch|unwatch|kill|log|java-log|force}"  # 输出用法提示
  exit 1  # 退出脚本
  ;;
esac

部署架构图

复制代码
┌─────────────────┐
│   Git仓库       │
│  (develop分支)  │
└────────┬────────┘
         │
         │ git fetch
         ▼
┌─────────────────┐      定时检查      ┌─────────────────┐
│  监控脚本       │◄──────────────────┤  5分钟间隔      │
│  monit.sh       │                    │  CHECK_INTERVAL │
└────────┬────────┘                    └─────────────────┘
         │
         │ 检测到变更
         ▼
┌─────────────────┐
│  自动构建流程   │
│  1. git merge   │
│  2. mvn package │
│  3. 移动jar包   │
└────────┬────────┘
         │
         │ 构建成功
         ▼
┌─────────────────┐
│  服务重启       │
│  kill旧进程     │
│  start新进程    │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│   Nginx代理     │
│  18084端口      │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│   前端应用      │
│  (配置不变)     │
└─────────────────┘

实际应用效果

开发效率提升

  • 零手动干预:代码提交后5分钟内自动部署
  • 即时反馈:Mac通知实时推送部署状态
  • 快速回滚force命令支持紧急回滚

运维成本降低

  • 统一入口:Nginx统一管理后端服务
  • 环境隔离:通过upstream配置实现多环境
  • 日志追踪:完整的构建和运行日志

团队协作优化

  • 标准化流程:所有成员使用相同的部署机制
  • 状态透明status命令提供完整的运行信息
  • 故障定位:详细的日志记录便于问题排查

配置建议

Nginx多环境配置示例

nginx 复制代码
# 开发环境
upstream ceec_back_server {
    server 172.18.1.16:18084;
}

# 测试环境  
upstream ceec_back_server {
    server 172.17.76.151:11026;
}

# 生产环境
upstream ceec_back_server {
    server 172.18.1.28:18084;
    server 172.18.1.29:18084 backup;
}

监控间隔调优

bash 复制代码
# 开发环境:1分钟(快速反馈)
CHECK_INTERVAL=60

# 生产环境:5分钟(减少资源消耗)
CHECK_INTERVAL=300

总结

这套自动化部署方案通过巧妙的脚本设计和Nginx配置,实现了:

  1. 完全自动化:从代码更新到服务重启的端到端自动化
  2. 配置零侵入:前端无需修改配置即可切换后端服务
  3. 高可靠性:完善的错误处理和状态追踪机制
  4. 开发友好:实时通知和日志系统提升开发体验

这套方案不仅适用于Java应用,其核心思想可以扩展到任何需要自动化部署的场景,是DevOps实践的优秀案例。


本文介绍的方案已在实际项目中稳定运行,为团队带来了显著的效率提升。欢迎在评论区交流你的自动化部署经验!

加载评论中...