基于 Nginx 与 Shell 的轻量级蓝绿部署实践:以 RuoYi-Admin 为例

101 阅读4分钟

在持续集成/持续部署 (CI/CD) 的流程中,蓝绿部署 (Blue/Green Deployment) 是一种通过运行两个相同的生产环境(版本不同)来减少停机时间和风险的策略。

本文将介绍如何不依赖复杂的容器编排工具(如 K8s),仅使用 NginxSystemdBash Shell,为 Java 应用(如 RuoYi-Admin)构建一套稳健的蓝绿部署系统。

1. 核心架构设计

我们的目标是实现零停机发布。系统由两个环境组成:

  • Blue (A) : 当前在线环境,承载用户流量。
  • Green (B) : 待发布环境,部署新版本代码。

切换机制:

通过修改 Nginx 的 upstream 配置文件指向,并重载 Nginx (reload),将流量瞬间从旧环境切到新环境。

目录结构示例:

Plaintext

/data/deploy/
├── nginx/
│   ├── AorB           # 状态文件,记录下一次该发 A 还是 B
│   ├── upstream.conf  # Nginx 实际引用的配置文件
│   ├── ruoyi-admin.a.conf.tmpl # 指向端口 9001 的模板
│   └── ruoyi-admin.b.conf.tmpl # 指向端口 9002 的模板
└── system/
    ├── prod/ruoyi-admin.jar    # 待发布的最新包
    ├── ruoyi-admin.a.jar       # A 环境运行包
    └── ruoyi-admin.b.jar       # B 环境运行包

2. 生产级部署脚本 (Shell Implementation)

以下是优化后的生产环境部署脚本。它集成了健康检查、超时控制和自动回滚逻辑。

Bash

#!/usr/bin/env bash

# --- 1. 配置区域 ---
ROOT="/data/deploy"
AB_FILE="${ROOT}/nginx/AorB"
JAR_SRC="${ROOT}/system/prod/ruoyi-admin.jar"

# 健康检查配置:30次重试 * 2秒间隔 = 60秒超时
MAX_RETRIES=30
SLEEP_SEC=2

# 初始化状态文件
if [ ! -f "$AB_FILE" ]; then echo "A" > "$AB_FILE"; fi
TARGET_KEY=$(cat "${AB_FILE}")

log() { echo -e "$(date +'%Y-%m-%d %H:%M:%S') $1"; }

# --- 2. 健康检查函数 ---
check_health() {
    local port=$1
    local count=0
    local url="http://127.0.0.1:${port}/auth/code" # 根据实际业务接口调整

    log "正在检查端口 ${port} 的健康状态..."

    while [ $count -lt $MAX_RETRIES ]; do
        # 静默捕获 curl 输出
        local resp=$(curl -s "${url}")

        # 判定逻辑:根据业务返回的关键字符判断
        if echo "$resp" | grep -q "操作成功"; then
            log "健康检查通过 (Success)。"
            return 0
        elif echo "$resp" | grep -q "Visit too frequently"; then
            log "健康检查通过 (Rate Limit)。"
            return 0
        fi

        echo -n "."
        sleep $SLEEP_SEC
        ((count++))
    done

    log "\n错误:健康检查超时 (60s)。"
    return 1
}

# --- 3. 部署核心逻辑 ---
deploy() {
    local target=$1      # e.g., "a"
    local port=$2        # e.g., 9001
    local old_target=$3  # e.g., "b"

    log "=== 开始部署环境: ${target} (端口 ${port}) ==="

    # 步骤 A: 复制 JAR 包
    if ! cp "${JAR_SRC}" "${ROOT}/system/ruoyi-admin.${target}.jar"; then
        log "错误:JAR 包复制失败。"
        exit 1
    fi

    # 步骤 B: 启动新服务
    log "正在启动 Systemd 服务: ruoyi-admin.${target} ..."
    sudo systemctl start "ruoyi-admin.${target}"

    # 步骤 C: 执行健康检查
    if ! check_health "$port"; then
        log "部署失败:服务无法启动或接口异常。正在停止新服务..."
        sudo systemctl stop "ruoyi-admin.${target}"
        exit 1
    fi

    # 步骤 D: 切换流量 (Nginx)
    log "服务健康。正在切换 Nginx 流量..."
    cp "${ROOT}/nginx/ruoyi-admin.${target}.conf.tmpl" "${ROOT}/nginx/upstream.conf"

    # 预检 Nginx 配置,防止配置错误导致整个服务挂掉
    if ! sudo nginx -t; then
        log "错误:Nginx 配置测试失败,取消切换。"
        exit 1
    fi

    sudo nginx -s reload
    log "Nginx 重载成功,流量已切换。"

    # 步骤 E: 下线旧服务
    log "正在停止旧服务: ruoyi-admin.${old_target} ..."
    sudo systemctl stop "ruoyi-admin.${old_target}"

    # 步骤 F: 更新状态标记
    local next_target="A"
    [ "$target" == "a" ] && next_target="B"
    echo "$next_target" > "${AB_FILE}"

    log "=== 部署成功。下一次发布目标: ${next_target} ==="
}

# --- 4. 主流程入口 ---
echo "本次发布目标: $TARGET_KEY"

if [ "$TARGET_KEY" == "A" ]; then
    deploy "a" 9001 "b"
elif [ "$TARGET_KEY" == "B" ]; then
    deploy "b" 9002 "a"
else
    log "错误:状态文件内容异常 ($TARGET_KEY),必须为 A 或 B。"
    exit 1
fi

3. 技术要点解析

3.1 拒绝无限等待 (Timeout Mechanism)

旧脚本中常见的错误是使用 while true 轮询服务状态。如果新代码引入了 Bug 导致 Spring Boot 启动报错,脚本会陷入死循环,阻塞 CI/CD 流水线。

改进: 引入计数器 count 和最大重试次数 MAX_RETRIES。一旦超时,脚本立即退出并返回错误码(Non-zero exit code),通知发布系统构建失败。

3.2 优雅的流量切换

Nginx 的 reload 命令是平滑的,它不会中断现有的连接。

关键步骤:

  1. cp ... upstream.conf: 覆盖配置文件。
  2. nginx -t: 必须先测试配置文件的语法正确性。
  3. nginx -s reload: 仅在测试通过后重载。

3.3 状态管理与幂等性

使用一个简单的文本文件 (AorB) 来持久化存储当前的状态。这比依赖内存或复杂的数据库更可靠且易于调试。脚本的逻辑是“读当前 -> 部署当前 -> 写入下一次”,形成闭环。

3.4 容错处理 (Fail-Safe)

脚本中加入了大量的错误检查(|| exit 1)。

  • 场景:如果 cp 命令因为磁盘满失败了,脚本会立即停止,而不是去启动一个空文件。
  • 场景:如果健康检查失败,脚本会主动调用 stop 命令清理刚才启动的脏进程,保持环境整洁。

4. 总结

通过这段 Shell 脚本,我们以极低的成本实现了 Java 应用的自动化蓝绿部署。它具备了商业级发布系统的基本要素:可观测性(日志)、健壮性(超时与回滚)和零停机能力

对于中小型项目,这往往是比迁移到 Kubernetes 性价比更高的选择。