2157 字
11 分钟
Nginx+SpringBoot主从节点无感部署
Nginx+SpringBoot主从节点无感部署
架构概述
单服务器主从架构,通过 Nginx 反向代理实现主备节点自动故障转移。

服务器信息
- 主机:阿里云 ECS
- IP:1.2.3.4
- 域名:epay.twenhub.com / epaydoc.twenhub.com
- Web 服务器:OpenResty 1.27.1.2
服务端口分配
- HTTP:80(强制跳转 HTTPS)
- HTTPS:443(主站访问)
- Master 主节点:9988
- Slave 从节点:9989
- NotifyPro 服务:41005
目录结构
/home/server/├── backend/digital_card_backend/│ ├── master/│ │ ├── deploy_master.sh│ │ ├── jeecg-system-start-3.4.3.jar│ │ └── master.log│ └── slave/│ ├── deploy_slave.sh│ ├── jeecg-system-start-3.4.3.jar│ └── slave.log├── fontend/│ ├── digital_card_frontend/ # 主站前端│ └── doc/ # 文档站前端└── /home/flowapp/ └── jeecg-system-start-3.4.3.tgz # 部署制品
/home/nginxcert/├── _.twenhub.com.crt└── _.twenhub.com.key
/usr/local/openresty/nginx/conf/└── nginx.confNginx 配置
完整配置文件
路径:/usr/local/openresty/nginx/conf/nginx.conf
# user nginx;worker_processes auto;worker_rlimit_nofile 65535;worker_shutdown_timeout 10s;
error_log logs/error.log warn;pid logs/nginx.pid;
events { worker_connections 65535; multi_accept on; use epoll;}
http { include mime.types; default_type application/octet-stream;
# Gzip 压缩 gzip on; gzip_min_length 1k; gzip_comp_level 6; gzip_vary on; gzip_disable "MSIE [1-6]\\."; gzip_types text/plain text/css application/javascript application/json application/xml text/javascript;
sendfile on; keepalive_timeout 65; server_tokens off; client_max_body_size 30m; client_body_timeout 300s;
# WebSocket 支持 map $http_upgrade $connection_upgrade { default upgrade; '' ""; }
# 后端主备服务 upstream backend_service { zone backend_zone 64k; server 127.0.0.1:9988 max_fails=2 fail_timeout=5s; server 127.0.0.1:9989 backup max_fails=1 fail_timeout=5s; keepalive 64; keepalive_timeout 60s; keepalive_requests 1000; }
# 通知服务 upstream notify_service { zone notify_zone 64k; server 127.0.0.1:41005 max_fails=2 fail_timeout=5s; keepalive 32; keepalive_timeout 60s; keepalive_requests 1000; }
# HTTP 跳转 HTTPS server { listen 80; listen [::]:80; server_name epay.twenhub.com; return 301 https://$host$request_uri; }
# 文档站 server { listen 443 ssl; server_name epaydoc.twenhub.com;
ssl_certificate /home/nginxcert/_.twenhub.com.crt; ssl_certificate_key /home/nginxcert/_.twenhub.com.key; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header Cache-Control "no-store, no-cache, must-revalidate, max-age=0" always; add_header Pragma "no-cache" always; add_header Expires "0" always;
root /home/server/fontend/doc; index index.html index.htm;
location / { try_files $uri $uri/ =404; } }
# 主站 server { listen 443 ssl; listen [::]:443 ssl; http2 on; server_name epay.twenhub.com;
ssl_certificate /home/nginxcert/_.twenhub.com.crt; ssl_certificate_key /home/nginxcert/_.twenhub.com.key; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# 前端 SPA root /home/server/fontend/digital_card_frontend; index index.html; location / { try_files $uri $uri/ /index.html; }
# 后端 API location /api/ { proxy_pass http://backend_service; proxy_http_version 1.1;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504; proxy_next_upstream_tries 2;
proxy_connect_timeout 1s; proxy_send_timeout 30s; proxy_read_timeout 120s;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
# 通知服务 location /notify/ { proxy_pass http://notify_service/notify_pro/; proxy_http_version 1.1;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504; proxy_next_upstream_tries 2;
proxy_connect_timeout 3s; proxy_send_timeout 300s; proxy_read_timeout 300s;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }}配置说明
主备策略
- Master 节点(9988):主节点,处理所有请求
- Slave 节点(9989):backup 模式,仅在主节点故障时接管
- 故障判定:连续 2 次失败(max_fails=2),5 秒超时(fail_timeout=5s)
- 快速失败:proxy_connect_timeout=1s,1 秒内无法连接则切换
连接复用
- keepalive 64:保持 64 个空闲连接到后端
- keepalive_timeout 60s:空闲连接保持 60 秒
- keepalive_requests 1000:单个连接最多处理 1000 个请求后轮换
重试机制
- proxy_next_upstream:定义触发重试的错误类型
- proxy_next_upstream_tries 2:最多重试 2 次(含首次)
部署脚本
Master 主节点部署脚本
路径:/home/server/backend/digital_card_backend/master/deploy_master.sh
#!/bin/bash
#---------------------------配置开始----------------------------------APP_NAME=jeecg-system-start-3.4.3PROG_NAME=$0ACTION=$1APP_START_TIMEOUT_SECONDS=120APP_PORT=9988BASE_URL=http://127.0.0.1:${APP_PORT}/apiHEALTH_CHECK_URL=${BASE_URL}/noauth/heartAPP_HOME=/home/server/backend/digital_card_backend/masterJAR_NAME=${APP_HOME}/${APP_NAME}.jarJAVA_OUT=${APP_HOME}/master.logPACKAGE_PATH=/home/flowapp/jeecg-system-start-3.4.3.tgzSHUTDOWN_TIMEOUT_SECONDS=40SPRING_PROFILE="master"#---------------------------配置结束----------------------------------
mkdir -p ${APP_HOME}
usage() { echo "Usage: $PROG_NAME {start|stop|restart|deploy}" exit 2}
health_check() { exptime=0 echo "checking ${HEALTH_CHECK_URL}" while true do status_code=`/usr/bin/curl -L -o /dev/null --connect-timeout 5 -s -w %{http_code} ${HEALTH_CHECK_URL}` if [ "$?" != "0" ]; then echo -n -e "\rapplication not started" else echo "code is $status_code" if [ "$status_code" == "200" ];then break fi fi sleep 1 ((exptime++))
echo -e "\rWait app to pass health check: $exptime..."
if [ $exptime -gt ${APP_START_TIMEOUT_SECONDS} ]; then echo 'app start failed' exit 1 fi done echo "check ${HEALTH_CHECK_URL} success"}
start_application() { echo "starting java process" nohup java -jar -Xmx512m -Xms256m -Dspring.profiles.active=${SPRING_PROFILE} -Dserver.port=${APP_PORT} ${JAR_NAME} >${JAVA_OUT} 2>&1 & echo "started java process"}
stop_application() { checkjavapid=$(ps -ef | grep java | grep ${APP_NAME} | grep ${APP_PORT} | grep -v grep | awk '{print $2}')
if [[ ! $checkjavapid ]]; then echo "No java process to stop (process not found)" return fi
echo "Sending SIGTERM to Java process with PID ${checkjavapid}." kill -15 ${checkjavapid}
for ((i=0; i<$SHUTDOWN_TIMEOUT_SECONDS; i++)); do http_status=$(curl -o /dev/null -s -w "%{http_code}\n" ${HEALTH_CHECK_URL})
if [ "$http_status" != "200" ]; then echo "Java process stopped gracefully." if [ ! -z "$checkjavapid" ]; then kill -9 $checkjavapid echo "Killed process on pid $checkjavapid." fi return fi
sleep 1 echo "Waiting for Java process to stop..." done
echo "Java process did not stop after $SHUTDOWN_TIMEOUT_SECONDS seconds, sending SIGKILL" if [ ! -z "$checkjavapid" ]; then kill -9 $checkjavapid echo "Killed process on port $APP_PORT." fi}
start() { start_application health_check}
stop() { stop_application}
deploy() { stop
echo "Unpacking $PACKAGE_PATH to $APP_HOME" if tar zxvf $PACKAGE_PATH -C $APP_HOME; then echo "Unpack finished successfully."
echo "Removing the package $PACKAGE_PATH" rm -f $PACKAGE_PATH if [ $? -eq 0 ]; then echo "Package removed successfully." else echo "Failed to remove the package." fi else echo "Error occurred during unpacking. Exiting." exit 1 fi
start}
case "$ACTION" in start) start ;; stop) stop ;; restart) stop start ;; deploy) deploy ;; *) usage ;;esacSlave 从节点部署脚本
路径:/home/server/backend/digital_card_backend/slave/deploy_slave.sh
#!/bin/bash
#---------------------------配置开始----------------------------------APP_NAME=jeecg-system-start-3.4.3PROG_NAME=$0ACTION=$1APP_START_TIMEOUT_SECONDS=120APP_PORT=9989BASE_URL=http://127.0.0.1:${APP_PORT}/apiHEALTH_CHECK_URL=${BASE_URL}/noauth/heartAPP_HOME=/home/server/backend/digital_card_backend/slaveJAR_NAME=${APP_HOME}/${APP_NAME}.jarJAVA_OUT=${APP_HOME}/slave.logPACKAGE_PATH=/home/flowapp/jeecg-system-start-3.4.3.tgzSHUTDOWN_TIMEOUT_SECONDS=40SPRING_PROFILE="slave"#---------------------------配置结束----------------------------------
mkdir -p ${APP_HOME}
usage() { echo "Usage: $PROG_NAME {start|stop|restart|deploy}" exit 2}
health_check() { exptime=0 echo "checking ${HEALTH_CHECK_URL}" while true do status_code=`/usr/bin/curl -L -o /dev/null --connect-timeout 5 -s -w %{http_code} ${HEALTH_CHECK_URL}` if [ "$?" != "0" ]; then echo -n -e "\rapplication not started" else echo "code is $status_code" if [ "$status_code" == "200" ];then break fi fi sleep 1 ((exptime++))
echo -e "\rWait app to pass health check: $exptime..."
if [ $exptime -gt ${APP_START_TIMEOUT_SECONDS} ]; then echo 'app start failed' exit 1 fi done echo "check ${HEALTH_CHECK_URL} success"}
start_application() { echo "starting java process" nohup java -jar -Xmx512m -Xms256m -Dspring.profiles.active=${SPRING_PROFILE} -Dserver.port=${APP_PORT} ${JAR_NAME} >${JAVA_OUT} 2>&1 & echo "started java process"}
stop_application() { checkjavapid=$(ps -ef | grep java | grep ${APP_NAME} | grep ${APP_PORT} | grep -v grep | awk '{print $2}')
if [[ ! $checkjavapid ]]; then echo "No java process to stop (process not found)" return fi
echo "Sending SIGTERM to Java process with PID ${checkjavapid}." kill -15 ${checkjavapid}
for ((i=0; i<$SHUTDOWN_TIMEOUT_SECONDS; i++)); do http_status=$(curl -o /dev/null -s -w "%{http_code}\n" ${HEALTH_CHECK_URL})
if [ "$http_status" != "200" ]; then echo "Java process stopped gracefully." if [ ! -z "$checkjavapid" ]; then kill -9 $checkjavapid echo "Killed process on pid $checkjavapid." fi return fi
sleep 1 echo "Waiting for Java process to stop..." done
echo "Java process did not stop after $SHUTDOWN_TIMEOUT_SECONDS seconds, sending SIGKILL" if [ ! -z "$checkjavapid" ]; then kill -9 $checkjavapid echo "Killed process on port $APP_PORT." fi}
start() { start_application health_check}
stop() { stop_application}
deploy() { stop
echo "Unpacking $PACKAGE_PATH to $APP_HOME" if tar zxvf $PACKAGE_PATH -C $APP_HOME; then echo "Unpack finished successfully."
echo "Removing the package $PACKAGE_PATH" rm -f $PACKAGE_PATH if [ $? -eq 0 ]; then echo "Package removed successfully." else echo "Failed to remove the package." fi else echo "Error occurred during unpacking. Exiting." exit 1 fi
start}
case "$ACTION" in start) start ;; stop) stop ;; restart) stop start ;; deploy) deploy ;; *) usage ;;esac脚本说明
主从节点差异
| 配置项 | Master | Slave |
|---|---|---|
| APP_PORT | 9988 | 9989 |
| APP_HOME | …/master | …/slave |
| JAVA_OUT | master.log | slave.log |
| SPRING_PROFILE | master | slave |
脚本功能
start:启动服务并健康检查stop:优雅停机(SIGTERM → 等待 40s → SIGKILL)restart:先停止后启动deploy:停止 → 解压制品 → 启动
健康检查
- 检查地址:
http://127.0.0.1:{PORT}/api/noauth/heart - 超时时间:120 秒
- 检查间隔:1 秒
- 成功条件:HTTP 200
优雅停机流程
- 发送 SIGTERM 信号(kill -15)
- 每秒检查健康接口
- 接口返回非 200 时执行 kill -9 清理
- 超过 40 秒强制 kill -9
云效流水线部署

流水线配置
流水线名称:digital_card_backend
代码源:
- 分支:master
- 触发方式:Webhook 触发或手动触发
流程阶段:
- Java 构建上传:构建 JAR 包并上传到制品库
- Slave 节点部署:部署从节点
- Master 节点部署:部署主节点
部署任务配置

Slave 节点部署任务
主机组:部署 执行用户:root(或具有执行权限的用户) 部署脚本:
sleep 3cd /home/server/backend/digital_card_backend/slavesh deploy_slave.sh deploy说明:
sleep 3:等待云效将制品(jeecg-system-start-3.4.3.tgz)上传到/home/flowapp/目录- 脚本会自动停止旧服务、解压新制品、启动新服务并进行健康检查
Master 节点部署任务
主机组:部署 执行用户:root(或具有执行权限的用户) 部署脚本:
sleep 3cd /home/server/backend/digital_card_backend/mastersh deploy_master.sh deploy说明:
sleep 3:确保制品上传完成(与 Slave 共享同一个制品文件)- 由于部署顺序是先 Slave 后 Master,此时 Slave 已经启动完成
零停机部署原理

部署流程时序:
1. 云效构建完成,上传制品到 /home/flowapp/jeecg-system-start-3.4.3.tgz ↓2. 延迟 3 秒(确保文件上传完整) ↓3. 部署 Slave 节点 - 停止旧 Slave(端口 9989) - 解压制品到 /home/server/backend/digital_card_backend/slave/ - 启动新 Slave - 健康检查通过(最多等待 120 秒) 【此时流量仍在 Master 9988,服务不中断】 ↓4. 延迟 3 秒 ↓5. 部署 Master 节点 - 停止旧 Master(端口 9988) 【Nginx 检测到 Master 不可用,自动切换流量到 Slave 9989】 - 解压制品到 /home/server/backend/digital_card_backend/master/ - 启动新 Master - 健康检查通过(最多等待 120 秒) 【Nginx 检测到 Master 恢复,自动切换流量回 Master 9988】 ↓6. 部署完成,Slave 保持运行作为备用节点Nginx 自动切换逻辑:
- Master 正常时:100% 流量到 Master(9988)
- Master 故障时:100% 流量到 Slave(9989,backup 节点)
- Master 恢复后:自动切回 Master
- 切换检测:每次请求前检查,1 秒连接超时即判定故障

部署触发方式
自动触发:
- 代码推送到 master 分支时,Webhook 自动触发流水线
- 云效自动执行:构建 → 部署 Slave → 部署 Master
手动触发:
- 在云效控制台点击”运行”按钮
- 选择指定分支或 commit 进行部署
部署验证
查看部署日志:
- 云效控制台 → 运行历史 → 点击对应的运行记录
- 查看”Slave 节点”和”Master 节点”的部署详情
Nginx+SpringBoot主从节点无感部署
https://twenhub.com/posts/nginx-springbootzhu-cong-jie-dian-wu-gan-bu-shu/