上线我的 2.0

上线我的 2.0

马图图

岁月变迁何必不悔,尘世喧嚣怎能无愧。

4 文章数
1 评论数

Java项目双服务交替部署脚本使用指南

Matuto
2025-11-25 / 1 评论 / 97 阅读 / 0 点赞

在日常的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文件名

脚本工作流程

1. 智能检测当前状态

脚本首先会检查:

  • 当前哪个服务正在运行

  • Nginx当前代理的是哪个端口

  • 如果自动检测失败,会提示手动输入当前端口

2. 确定部署目标

根据当前运行状态,自动选择要部署的服务实例:

  • 如果服务1在运行,就部署服务2

  • 如果服务2在运行,就部署服务1

  • 如果都没有运行,默认部署服务1

3. 安全部署过程

部署过程包含多重安全措施:

解压新版本

  • 创建临时目录进行解压,避免影响现有服务

  • 自动查找压缩包中的JAR文件

  • 备份原有JAR文件,便于回滚

启动新服务

  • 使用java-service命令启动服务

  • 等待服务端口监听成功

  • 通过健康检查接口确认服务完全就绪

流量切换

  • 修改Nginx配置,指向新服务端口

  • 验证Nginx配置语法正确性

  • 平滑重载Nginx配置

清理旧服务

  • 等待5秒确保流量切换完成

  • 安全停止旧版本服务

异常处理机制

脚本设计了完善的错误处理:

  • 解压失败:检查压缩包是否存在、格式是否正确

  • 服务启动失败:自动停止服务并恢复备份的JAR文件

  • Nginx配置错误:自动回滚到备份配置

  • 健康检查超时:在指定时间内未就绪则自动回滚

使用建议

准备工作

  1. 确保两个服务目录都已创建并有相应权限

  1. 配置好java-service服务管理命令

  1. 确认健康检查接口能正常返回服务状态

日常使用

# 将新版本打包为.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

下一篇
评论
来首音乐
光阴似箭
今日已经过去小时
这周已经过去
本月已经过去
今年已经过去个月
文章目录
每日一句