在持续集成/持续部署 (CI/CD) 的流程中,蓝绿部署 (Blue/Green Deployment) 是一种通过运行两个相同的生产环境(版本不同)来减少停机时间和风险的策略。
本文将介绍如何不依赖复杂的容器编排工具(如 K8s),仅使用 Nginx、Systemd 和 Bash 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 命令是平滑的,它不会中断现有的连接。
关键步骤:
cp ... upstream.conf: 覆盖配置文件。nginx -t: 必须先测试配置文件的语法正确性。nginx -s reload: 仅在测试通过后重载。
3.3 状态管理与幂等性
使用一个简单的文本文件 (AorB) 来持久化存储当前的状态。这比依赖内存或复杂的数据库更可靠且易于调试。脚本的逻辑是“读当前 -> 部署当前 -> 写入下一次”,形成闭环。
3.4 容错处理 (Fail-Safe)
脚本中加入了大量的错误检查(|| exit 1)。
- 场景:如果
cp命令因为磁盘满失败了,脚本会立即停止,而不是去启动一个空文件。 - 场景:如果健康检查失败,脚本会主动调用
stop命令清理刚才启动的脏进程,保持环境整洁。
4. 总结
通过这段 Shell 脚本,我们以极低的成本实现了 Java 应用的自动化蓝绿部署。它具备了商业级发布系统的基本要素:可观测性(日志)、健壮性(超时与回滚)和零停机能力。
对于中小型项目,这往往是比迁移到 Kubernetes 性价比更高的选择。