在日常的Java项目部署中,我们经常面临一个难题:如何在不中断服务的情况下完成新版本的部署?今天就来分享一个实用的自动化部署脚本,它通过双服务交替部署的方式实现了零停机更新。
这个部署脚本就像一位智能管家,帮你自动完成以下工作:
双服务交替部署:维护两个服务实例,轮流进行更新,确保始终有一个服务在线
Nginx自动切换:部署完成后自动修改Nginx配置,将流量切换到新版本服务
健康检查机制:等待新服务完全启动并健康后再切换流量
配置备份保护:自动备份重要配置文件,出现问题可快速回滚
使用这个脚本非常简单,只需要修改以下几个配置项就能适配你的项目:
# 服务1配置(第一个服务实例)
APP1_NAME="oms" # 服务名称
APP1_PATH="/www/wwwroot/matuto_oms/oms" # 部署目录
APP1_JAR="oms.jar" # JAR文件名
APP1_PORT="13561" # 服务端口
# 服务2配置(第二个服务实例)
APP2_NAME="oms_2" # 服务名称
APP2_PATH="/www/wwwroot/matuto_oms/oms_2" # 部署目录
APP2_JAR="oms_2.jar" # JAR文件名
APP2_PORT="13562" # 服务端口
TAR_FILE="/www/wwwroot/matuto_oms/oms.taz" # 新版本压缩包路径
NGINX_CONF="/path/to/nginx.conf" # Nginx配置文件路径
HEALTH_CHECK_PATH="api/common/health" # 健康检查接口
COMPRESSED_JAR_FIXED_NAME="oms.jar" # 压缩包内JAR文件名
脚本首先会检查:
当前哪个服务正在运行
Nginx当前代理的是哪个端口
如果自动检测失败,会提示手动输入当前端口
根据当前运行状态,自动选择要部署的服务实例:
如果服务1在运行,就部署服务2
如果服务2在运行,就部署服务1
如果都没有运行,默认部署服务1
部署过程包含多重安全措施:
解压新版本
创建临时目录进行解压,避免影响现有服务
自动查找压缩包中的JAR文件
备份原有JAR文件,便于回滚
启动新服务
使用java-service命令启动服务
等待服务端口监听成功
通过健康检查接口确认服务完全就绪
流量切换
修改Nginx配置,指向新服务端口
验证Nginx配置语法正确性
平滑重载Nginx配置
清理旧服务
等待5秒确保流量切换完成
安全停止旧版本服务
脚本设计了完善的错误处理:
解压失败:检查压缩包是否存在、格式是否正确
服务启动失败:自动停止服务并恢复备份的JAR文件
Nginx配置错误:自动回滚到备份配置
健康检查超时:在指定时间内未就绪则自动回滚
确保两个服务目录都已创建并有相应权限
配置好java-service服务管理命令
确认健康检查接口能正常返回服务状态
# 将新版本打包为.tar.gz文件,重命名为.taz后缀
# 上传到TAR_FILE指定的路径
# 直接运行脚本即可
./oms_deploy.sh
脚本会详细输出每个步骤的执行情况,重点关注:
"【成功】"开头的消息表示步骤完成
"【错误】"开头的消息需要及时处理
最后的部署状态总结
这个脚本大大简化了Java项目的部署流程,特别适合需要高可用性的生产环境。通过自动化交替部署,既保证了服务连续性,又降低了人工操作的风险。
#!/bin/bash
##############################################################################
# 通用化部署脚本:仅需修改下方「项目配置变量」即可适配不同Java项目
# 核心功能:双服务交替部署(避免 downtime)、Nginx自动切换、健康检查、配置备份
##############################################################################
# ============================== 项目配置变量(需根据项目修改)==============================
# 服务1配置(项目1的路径、JAR包名、端口)
APP1_NAME="oms" # 服务1名称(需与java-service命令的项目名一致)
APP1_PATH="/www/wwwroot/matuto_oms/oms" # 服务1部署路径
APP1_JAR="oms.jar" # 服务1的JAR文件名
APP1_PORT="13561" # 服务1端口
# 服务2配置(项目2的路径、JAR包名、端口,用于交替部署)
APP2_NAME="oms_2" # 服务2名称(需与java-service命令的项目名一致)
APP2_PATH="/www/wwwroot/matuto_oms/oms_2" # 服务2部署路径
APP2_JAR="oms_2.jar" # 服务2的JAR文件名
APP2_PORT="13562" # 服务2端口
# 其他核心配置
TAR_FILE="/www/wwwroot/matuto_oms/oms.taz" # 待解压的JAR压缩包路径
NGINX_CONF="/www/server/panel/vhost/nginx/html_order.matuto.com.conf" # Nginx配置文件路径
HEALTH_CHECK_PATH="api/common/health" # 服务健康检查接口路径(无需包含端口)
HEALTH_TIMEOUT="300" # 服务启动超时时间(秒)
HEALTH_INTERVAL="5" # 健康检查间隔(秒)
CURL_TIMEOUT="5" # curl请求超时时间(秒)
COMPRESSED_JAR_FIXED_NAME="oms.jar" # 压缩包内固定的JAR文件名(核心新增配置)
##############################################################################
# ============================== 工具函数(修复核心问题)==============================
# 1. 检查端口是否在监听(兼容 netstat/ss)
check_port() {
local port=$1
if command -v netstat >/dev/null 2>&1; then
netstat -tunlp | grep ":$port" > /dev/null
else
ss -tunlp | grep ":$port" > /dev/null
fi
return $?
}
# 2. 获取当前Nginx配置的代理端口(修复:用 # 作为sed分隔符,适配路径含 / 的情况)
get_current_nginx_port() {
# 关键修复1:用 # 替代 / 作为sed分隔符,避免与路径中的 / 冲突
# 关键修复2:兼容 proxy_pass 多种格式(如 http://127.0.0.1:8081;、http://127.0.0.1:8081/、http://127.0.0.1:8081/api/)
local port=$(grep "proxy_pass http://127.0.0.1:" "$NGINX_CONF" 2>/dev/null |
sed -n 's#.*proxy_pass http://127.0.0.1:\([0-9]*\)[;/].*#\1#p' |
head -n 1) # 取第一个匹配结果,避免多配置冲突
# 若未提取到端口,返回空字符串(后续主流程会处理)
echo "$port"
}
# 3. 更新Nginx代理端口(修复:sed分隔符 + 增强配置匹配)
update_nginx_port() {
local new_port=$1
local temp_file="${NGINX_CONF}.tmp"
local proxy_pattern="proxy_pass http://127.0.0.1:"
# 关键修复:用 # 作为sed分隔符,避免与路径中的 / 冲突
sed -E "s#(${proxy_pattern})[0-9]+([;\/])#\1${new_port}\2#" "$NGINX_CONF" > "$temp_file"
# 验证替换结果
if ! grep -q "${proxy_pattern}${new_port}[;/]" "$temp_file" 2>/dev/null; then
echo "【错误】Nginx端口替换失败,未匹配到 ${proxy_pattern}[端口] 格式"
echo "当前Nginx配置中的proxy_pass内容:"
grep "$proxy_pattern" "$NGINX_CONF" 2>/dev/null || echo "未找到 proxy_pass 配置"
rm -f "$temp_file"
return 1
fi
# 权限检查与配置应用
if [ ! -w "$NGINX_CONF" ]; then
echo "【错误】无权限修改Nginx配置文件:$NGINX_CONF"
rm -f "$temp_file"
return 1
fi
cp "$NGINX_CONF" "${NGINX_CONF}.bak"
mv "$temp_file" "$NGINX_CONF"
# 验证Nginx配置并重载
if nginx -t > /dev/null 2>&1; then
nginx -s reload
echo "【成功】Nginx已切换到代理端口:$new_port"
return 0
else
echo "【错误】Nginx配置语法错误,回滚修改"
nginx -t
mv "${NGINX_CONF}.bak" "$NGINX_CONF"
return 1
fi
}
# 4. 检查当前运行的服务(保留原逻辑,增强匹配精度)
check_running_app() {
# 用 --fixed-strings 确保匹配完整路径,避免部分文件名冲突
if ps aux | grep -v grep | grep --fixed-strings "$APP1_PATH/$APP1_JAR" > /dev/null; then
echo "$APP1_NAME"
elif ps aux | grep -v grep | grep --fixed-strings "$APP2_PATH/$APP2_JAR" > /dev/null; then
echo "$APP2_NAME"
else
echo "none"
fi
}
# 5. 启动服务(保留原逻辑)
start_service() {
local project_name=$1
echo "【操作】启动服务:$project_name"
if ! java-service "$project_name" start > /dev/null 2>&1; then
echo "【错误】启动服务 $project_name 失败"
return 1
fi
return 0
}
# 6. 停止服务(保留原逻辑)
stop_service() {
local project_name=$1
echo "【操作】停止服务:$project_name"
if ! java-service "$project_name" stop > /dev/null 2>&1; then
echo "【错误】停止服务 $project_name 失败(可能已停止)"
return 1
fi
return 0
}
# 7. 检查服务健康状态(保留原逻辑)
check_service_health() {
local target_port=$1
local counter=0
local health_url="http://127.0.0.1:${target_port}/${HEALTH_CHECK_PATH}"
echo "【等待】服务启动中,监听端口:$target_port(超时:${HEALTH_TIMEOUT}s)"
while [ $counter -lt $HEALTH_TIMEOUT ]; do
if check_port "$target_port"; then
echo "【进度】端口 $target_port 已监听,等待服务就绪..."
sleep 10
local response=$(curl -s -m "$CURL_TIMEOUT" "$health_url" 2>/dev/null)
if echo "$response" | grep -qE '"code":200' && echo "$response" | grep -qE '"status":"UP"'; then
echo "【成功】服务健康检查通过(响应:$response)"
return 0
else
echo "【进度】服务未就绪,健康接口响应:$response"
fi
fi
sleep "$HEALTH_INTERVAL"
counter=$((counter + HEALTH_INTERVAL))
echo "【进度】已等待 ${counter}s(剩余:$((HEALTH_TIMEOUT - counter))s)"
done
echo "【错误】服务启动超时(${HEALTH_TIMEOUT}s)"
return 1
}
# 8. 备份Nginx配置(保留原逻辑)
backup_nginx_config() {
local backup_file="${NGINX_CONF}.bak.$(date +%Y%m%d%H%M%S)"
cp "$NGINX_CONF" "$backup_file"
echo "【备份】Nginx配置已保存至:$backup_file"
return 0
}
# 9. 安全解压JAR文件(核心修复:支持解压后自动重命名JAR)
safe_extract_jar() {
local target_path=$1
local target_jar=$2
local temp_dir=$(mktemp -d)
echo "【操作】开始解压JAR压缩包:$TAR_FILE(临时目录:$temp_dir)"
# 检查压缩包是否存在
if [ ! -f "$TAR_FILE" ]; then
echo "【错误】JAR压缩包不存在:$TAR_FILE"
rm -rf "$temp_dir"
return 1
fi
# 解压压缩包(支持.tar.gz格式,若原后缀为.taz需确保是gzip压缩)
if ! tar zxvf "$TAR_FILE" -C "$temp_dir" > /dev/null 2>&1; then
echo "【错误】解压压缩包失败(需 .tar.gz 或 .taz 格式,确保是gzip压缩)"
rm -rf "$temp_dir"
return 1
fi
# 查找压缩包内固定名称的JAR(核心修改:不再依赖目标JAR名查找)
local extracted_jar=$(find "$temp_dir" -name "$COMPRESSED_JAR_FIXED_NAME" -type f | head -n 1)
if [ -z "$extracted_jar" ]; then
echo "【错误】压缩包中未找到固定JAR:$COMPRESSED_JAR_FIXED_NAME"
echo "压缩包内容列表:"
tar ztvf "$TAR_FILE" | grep -i ".jar"
rm -rf "$temp_dir"
return 1
fi
echo "【进度】已找到压缩包内JAR:$extracted_jar"
# 备份原有JAR(若存在)
if [ -f "$target_path/$target_jar" ]; then
local backup_jar="${target_path}/${target_jar}.bak.$(date +%Y%m%d%H%M%S)"
cp "$target_path/$target_jar" "$backup_jar"
echo "【备份】原有JAR已保存至:$backup_jar"
fi
# 核心修复:将压缩包内的固定JAR重命名为目标JAR名,再复制到目标路径
cp "$extracted_jar" "$temp_dir/$target_jar"
mv "$temp_dir/$target_jar" "$target_path/$target_jar"
# 清理临时目录
rm -rf "$temp_dir"
echo "【成功】JAR文件更新完成(原JAR:$COMPRESSED_JAR_FIXED_NAME → 目标JAR:$target_path/$target_jar)"
return 0
}
# ============================== 主部署流程(增强端口提取异常处理)==============================
echo "==================================== 部署开始 ===================================="
# 1. 获取当前运行状态(修复:增加端口提取失败的处理)
RUNNING_APP=$(check_running_app)
CURRENT_NGINX_PORT=$(get_current_nginx_port)
# 关键修复:若未提取到Nginx端口,提示并手动指定初始端口(避免主流程卡住)
if [ -z "$CURRENT_NGINX_PORT" ] || [ "$CURRENT_NGINX_PORT" = "" ]; then
echo "【警告】未从Nginx配置中提取到代理端口,检查以下内容:"
echo "1. Nginx配置文件路径是否正确:$NGINX_CONF"
echo "2. 配置中是否存在 proxy_pass http://127.0.0.1:端口; 格式"
# 手动询问初始端口(避免部署流程中断)
read -p "请输入当前Nginx实际代理的端口(如 9090/9091):" MANUAL_PORT
# 验证手动输入的端口是否为数字
if ! [[ "$MANUAL_PORT" =~ ^[0-9]+$ ]]; then
echo "【错误】输入的端口不是有效数字,部署终止"
exit 1
fi
CURRENT_NGINX_PORT="$MANUAL_PORT"
echo "【手动指定】Nginx代理端口:$CURRENT_NGINX_PORT"
fi
# 打印当前状态(确保端口正常显示)
echo "【当前状态】运行中服务:$RUNNING_APP | Nginx代理端口:$CURRENT_NGINX_PORT"
# 2. 确定目标部署服务(保留原逻辑)
if [ "$RUNNING_APP" = "$APP1_NAME" ] || [ "$CURRENT_NGINX_PORT" = "$APP1_PORT" ]; then
TARGET_NAME="$APP2_NAME"
TARGET_PATH="$APP2_PATH"
TARGET_JAR="$APP2_JAR"
TARGET_PORT="$APP2_PORT"
OLD_NAME="$APP1_NAME"
OLD_PORT="$APP1_PORT"
elif [ "$RUNNING_APP" = "$APP2_NAME" ] || [ "$CURRENT_NGINX_PORT" = "$APP2_PORT" ]; then
TARGET_NAME="$APP1_NAME"
TARGET_PATH="$APP1_PATH"
TARGET_JAR="$APP1_JAR"
TARGET_PORT="$APP1_PORT"
OLD_NAME="$APP2_NAME"
OLD_PORT="$APP2_PORT"
else
echo "【初始状态】无运行服务,默认部署:$APP1_NAME"
TARGET_NAME="$APP1_NAME"
TARGET_PATH="$APP1_PATH"
TARGET_JAR="$APP1_JAR"
TARGET_PORT="$APP1_PORT"
OLD_NAME="$APP2_NAME"
OLD_PORT="$APP2_PORT"
fi
echo "【部署目标】服务名:$TARGET_NAME | 路径:$TARGET_PATH | 端口:$TARGET_PORT | 目标JAR:$TARGET_JAR"
# 3. 执行部署步骤(保留原逻辑)
backup_nginx_config || exit 1
if ! safe_extract_jar "$TARGET_PATH" "$TARGET_JAR"; then
echo "【终止】JAR解压失败,部署终止"
exit 1
fi
if ! start_service "$TARGET_NAME"; then
echo "【终止】启动目标服务失败,部署终止"
exit 1
fi
if ! check_service_health "$TARGET_PORT"; then
echo "【回滚】目标服务启动失败,开始回滚"
stop_service "$TARGET_NAME"
local backup_jar="${TARGET_PATH}/${TARGET_JAR}.bak"
if [ -f "$backup_jar" ]; then
mv "$backup_jar" "$TARGET_PATH/$TARGET_JAR"
echo "【回滚】已恢复原有JAR文件"
fi
echo "【终止】部署失败,已回滚"
exit 1
fi
if ! update_nginx_port "$TARGET_PORT"; then
echo "【回滚】切换Nginx端口失败,开始回滚"
stop_service "$TARGET_NAME"
local backup_jar="${TARGET_PATH}/${TARGET_JAR}.bak"
if [ -f "$backup_jar" ]; then
mv "$backup_jar" "$TARGET_PATH/$TARGET_JAR"
echo "【回滚】已恢复原有JAR文件"
fi
echo "【终止】部署失败,已回滚"
exit 1
fi
if [ "$RUNNING_APP" != "none" ]; then
echo "【操作】等待5秒确保请求切换完成..."
sleep 5
stop_service "$OLD_NAME" || echo "【警告】停止旧服务失败(可能已提前停止)"
fi
echo "==================================== 部署完成 ===================================="
echo "【最终状态】当前运行服务:$TARGET_NAME | Nginx代理端口:$TARGET_PORT"
exit 0