自动化部署方案:基于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配置,实现了:
- 完全自动化:从代码更新到服务重启的端到端自动化
- 配置零侵入:前端无需修改配置即可切换后端服务
- 高可靠性:完善的错误处理和状态追踪机制
- 开发友好:实时通知和日志系统提升开发体验
这套方案不仅适用于Java应用,其核心思想可以扩展到任何需要自动化部署的场景,是DevOps实践的优秀案例。
本文介绍的方案已在实际项目中稳定运行,为团队带来了显著的效率提升。欢迎在评论区交流你的自动化部署经验!