核心流程
#mermaid-svg-tbUY37Nw3kaABcjX{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-tbUY37Nw3kaABcjX .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-tbUY37Nw3kaABcjX .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-tbUY37Nw3kaABcjX .error-icon{fill:#552222;}#mermaid-svg-tbUY37Nw3kaABcjX .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-tbUY37Nw3kaABcjX .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-tbUY37Nw3kaABcjX .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tbUY37Nw3kaABcjX .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tbUY37Nw3kaABcjX .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-tbUY37Nw3kaABcjX .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tbUY37Nw3kaABcjX .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tbUY37Nw3kaABcjX .marker{fill:#333333;stroke:#333333;}#mermaid-svg-tbUY37Nw3kaABcjX .marker.cross{stroke:#333333;}#mermaid-svg-tbUY37Nw3kaABcjX svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tbUY37Nw3kaABcjX p{margin:0;}#mermaid-svg-tbUY37Nw3kaABcjX .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-tbUY37Nw3kaABcjX .cluster-label text{fill:#333;}#mermaid-svg-tbUY37Nw3kaABcjX .cluster-label span{color:#333;}#mermaid-svg-tbUY37Nw3kaABcjX .cluster-label span p{background-color:transparent;}#mermaid-svg-tbUY37Nw3kaABcjX .label text,#mermaid-svg-tbUY37Nw3kaABcjX span{fill:#333;color:#333;}#mermaid-svg-tbUY37Nw3kaABcjX .node rect,#mermaid-svg-tbUY37Nw3kaABcjX .node circle,#mermaid-svg-tbUY37Nw3kaABcjX .node ellipse,#mermaid-svg-tbUY37Nw3kaABcjX .node polygon,#mermaid-svg-tbUY37Nw3kaABcjX .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-tbUY37Nw3kaABcjX .rough-node .label text,#mermaid-svg-tbUY37Nw3kaABcjX .node .label text,#mermaid-svg-tbUY37Nw3kaABcjX .image-shape .label,#mermaid-svg-tbUY37Nw3kaABcjX .icon-shape .label{text-anchor:middle;}#mermaid-svg-tbUY37Nw3kaABcjX .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-tbUY37Nw3kaABcjX .rough-node .label,#mermaid-svg-tbUY37Nw3kaABcjX .node .label,#mermaid-svg-tbUY37Nw3kaABcjX .image-shape .label,#mermaid-svg-tbUY37Nw3kaABcjX .icon-shape .label{text-align:center;}#mermaid-svg-tbUY37Nw3kaABcjX .node.clickable{cursor:pointer;}#mermaid-svg-tbUY37Nw3kaABcjX .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-tbUY37Nw3kaABcjX .arrowheadPath{fill:#333333;}#mermaid-svg-tbUY37Nw3kaABcjX .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-tbUY37Nw3kaABcjX .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-tbUY37Nw3kaABcjX .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tbUY37Nw3kaABcjX .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-tbUY37Nw3kaABcjX .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tbUY37Nw3kaABcjX .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-tbUY37Nw3kaABcjX .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-tbUY37Nw3kaABcjX .cluster text{fill:#333;}#mermaid-svg-tbUY37Nw3kaABcjX .cluster span{color:#333;}#mermaid-svg-tbUY37Nw3kaABcjX div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-tbUY37Nw3kaABcjX .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-tbUY37Nw3kaABcjX rect.text{fill:none;stroke-width:0;}#mermaid-svg-tbUY37Nw3kaABcjX .icon-shape,#mermaid-svg-tbUY37Nw3kaABcjX .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tbUY37Nw3kaABcjX .icon-shape p,#mermaid-svg-tbUY37Nw3kaABcjX .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-tbUY37Nw3kaABcjX .icon-shape .label rect,#mermaid-svg-tbUY37Nw3kaABcjX .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tbUY37Nw3kaABcjX .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-tbUY37Nw3kaABcjX .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-tbUY37Nw3kaABcjX :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 有
无
是
否
客户端请求
Gateway 路由判断
Header 中是否有 X-Gray-User?
转发到新服务
blog:9092
转发到旧服务
blog:9091
新服务运行测试
旧服务处理常规流量
测试无误?
Gateway 配置变更:
-
删除 blog:9091 路由
-
删除 X-Gray-User 判断条件
全部流量转发到新服务 blog:9092
Nacos 操作:
找到 blog 服务下端口 9091
设置元数据 autoclose=true
旧服务 blog:9091 自动关闭
灰度发布完成
nacos 配置
新旧服务同时运行
yaml
spring:
cloud:
gateway:
routes:
# 规则1:灰度路由 - 当请求头 X-Gray-User 存在时,转发至灰度版本
- id: blog-gray-route
uri: http://127.0.0.1:9092
predicates:
- Path=/api/manage/**
- Header=X-Gray-User, .+ # 正则匹配:只要 Header 存在且非空
filters:
- StripPrefix=1
- AddRequestHeader=gray-version, v2.0 # 可选:为下游服务添加灰度标识
# 规则2:默认路由 - 所有其他请求,转发至稳定版本
- id: blog-default-route
uri: http://127.0.0.1:9091
predicates:
- Path=/api/manage/**
filters:
- StripPrefix=1
只剩新服务运行
yaml
spring:
cloud:
gateway:
routes:
# 规则1:灰度路由 - 当请求头 X-Gray-User 存在时,转发至灰度版本
- id: blog-gray-route
uri: http://127.0.0.1:9092
predicates:
- Path=/api/manage/**
filters:
- StripPrefix=1
- AddRequestHeader=gray-version, v2.0 # 可选:为下游服务添加灰度标识
如果你要路由中还保留旧服务,只是不走流量
yaml
spring:
cloud:
gateway:
routes:
# 规则1:100% 流量走这个路由
- id: blog-gray-route
uri: http://127.0.0.1:9092
predicates:
- Path=/api/manage/**
filters:
- StripPrefix=1
- AddRequestHeader=gray-version, v2.0 # 可选:为下游服务添加灰度标识
- Weight=group1, 100 # 100% 流量走这个路由
# 规则2:0% 流量
- id: blog-default-route
uri: http://127.0.0.1:9091
predicates:
- Path=/api/manage/**
filters:
- StripPrefix=1
- Weight=group1, 0 # 0% 流量
shell
目的:新启动的程序,只会替换最新时间的那个程序。
旧程序的关闭,是监听 元数据 autoclose=true 时,自动关闭。
bash
#!/bin/bash
# === 配置区域 ===
PORT1=9091
PORT2=9092
APP_BASE=blog
APP_PATH=/data/website/blog
JAVA_HOME=/data/jdk-25/bin/java
# 生成带端口的 jar 文件名
get_jar_name() {
echo "${APP_BASE}.$1.jar"
}
# === 工具函数 ===
# 检查指定端口的进程是否存在,返回 PID
is_port_running() {
local port=$1
local jar_name=$(get_jar_name $port)
pid=$(ps -ef | grep "$jar_name" | grep -v grep | awk '{print $2}')
if [ -z "$pid" ]; then
return 1
else
echo $pid
return 0
fi
}
# 获取进程的启动时间戳(秒)
get_start_time() {
local pid=$1
local lstart=$(ps -p $pid -o lstart= 2>/dev/null)
if [ -n "$lstart" ]; then
date -d "$lstart" +%s 2>/dev/null
else
echo 0
fi
}
# 获取当前运行的端口及 PID(如果两个都运行,返回两个)
get_running_ports() {
local pid1
local pid2
local running_ports=""
if pid1=$(is_port_running $PORT1); then
running_ports="${PORT1}:${pid1}"
fi
if pid2=$(is_port_running $PORT2); then
if [ -n "$running_ports" ]; then
running_ports="${running_ports},${PORT2}:${pid2}"
else
running_ports="${PORT2}:${pid2}"
fi
fi
echo "$running_ports"
}
# === 原有函数(修改) ===
usage() {
echo "Usage: sh $0 [start|stop|restart|status]"
exit 1
}
# 检查是否有任何实例运行(兼容旧调用)
is_exist() {
if is_port_running $PORT1 >/dev/null || is_port_running $PORT2 >/dev/null; then
return 0
else
return 1
fi
}
# 启动日志(仅输出启动结果)
start_log() {
local port=$1
local jar_name=$(get_jar_name $port)
local new_pid=$(is_port_running $port)
if [ -n "$new_pid" ]; then
echo "${jar_name} 启动成功! pid=${new_pid} 端口=${port}"
else
echo "${jar_name} 启动失败!请检查后重试"
fi
}
# 启动方法(核心逻辑)
start() {
local running_info=$(get_running_ports)
local current_port=""
local current_pid=""
# 情况1:两个实例都在运行
if [[ "$running_info" == *"${PORT1}"* && "$running_info" == *"${PORT2}"* ]]; then
echo "检测到两个实例都在运行,将比较启动时间戳并杀掉较新的一个..."
# 解析两个端口和pid
IFS=',' read -ra entries <<< "$running_info"
declare -A port_pid
for entry in "${entries[@]}"; do
port=${entry%:*}
pid=${entry#*:}
port_pid[$port]=$pid
done
time1=$(get_start_time ${port_pid[$PORT1]})
time2=$(get_start_time ${port_pid[$PORT2]})
if [ $time1 -gt $time2 ]; then
kill_pid=${port_pid[$PORT1]}
kill_port=$PORT1
else
kill_pid=${port_pid[$PORT2]}
kill_port=$PORT2
fi
echo "杀掉较新实例:端口 ${kill_port} PID ${kill_pid}"
kill -9 $kill_pid
# 杀掉后,剩下的那个就是我们要保留的
if [ $kill_port -eq $PORT1 ]; then
current_port=$PORT2
current_pid=${port_pid[$PORT2]}
else
current_port=$PORT1
current_pid=${port_pid[$PORT1]}
fi
echo "保留实例:端口 ${current_port} PID ${current_pid}"
# 情况2:只有一个实例在运行
elif [ -n "$running_info" ]; then
# 解析当前运行的端口和pid
IFS=',' read -ra entries <<< "$running_info"
entry="${entries[0]}"
current_port=${entry%:*}
current_pid=${entry#*:}
echo "当前运行实例:端口 ${current_port} PID ${current_pid}"
# 情况3:无实例运行
else
echo "没有运行中的实例,将启动默认端口 ${PORT1}"
current_port=""
fi
# 决定要启动的新端口
if [ -z "$current_port" ]; then
new_port=$PORT1
else
if [ $current_port -eq $PORT1 ]; then
new_port=$PORT2
else
new_port=$PORT1
fi
fi
echo "准备启动新实例:端口 ${new_port}"
# 复制基础 jar 包为带端口的 jar(确保使用最新代码)
local base_jar="${APP_PATH}/${APP_BASE}.jar"
local target_jar="${APP_PATH}/$(get_jar_name $new_port)"
if [ ! -f "$base_jar" ]; then
echo "错误:基础 jar 包不存在 ${base_jar}"
exit 1
fi
cp "$base_jar" "$target_jar"
echo "已复制 ${base_jar} -> ${target_jar}"
# 启动新实例
nohup ${JAVA_HOME}/java -jar "$target_jar" \
--server.port=$new_port \
--spring.profiles.active=prod \
--spring.cloud.nacos.config.server-addr=127.0.0.1:8848 \
> /dev/null 2>&1 &
start_log $new_port
# 如果之前有旧实例在运行,等待新实例就绪后将其停止(滚动更新)
#if [ -n "$current_port" ]; then
# echo "等待 5 秒,确保新实例稳定..."
# sleep 5
# 再次确认新实例已启动
# if is_port_running $new_port >/dev/null; then
# echo "新实例已就绪,停止旧实例(端口 ${current_port} PID ${current_pid})"
# kill $current_pid
# echo "旧实例已停止"
# else
# echo "警告:新实例未能成功启动,旧实例未停止"
# fi
#fi
}
# 停止所有实例
stop() {
local stopped=0
for port in $PORT1 $PORT2; do
local pid=$(is_port_running $port)
if [ -n "$pid" ]; then
kill $pid
echo "已关闭 $(get_jar_name $port) pid=${pid}"
stopped=1
fi
done
if [ $stopped -eq 0 ]; then
echo "没有运行中的实例"
fi
}
# 输出运行状态
status() {
local running_info=$(get_running_ports)
if [ -z "$running_info" ]; then
echo "没有运行中的实例"
else
IFS=',' read -ra entries <<< "$running_info"
for entry in "${entries[@]}"; do
port=${entry%:*}
pid=${entry#*:}
start_time=$(get_start_time $pid)
start_str=$(date -d "@$start_time" "+%Y-%m-%d %H:%M:%S" 2>/dev/null)
echo "$(get_jar_name $port) 运行中 pid=${pid} 启动时间=${start_str}"
done
fi
}
# 重启:先停止所有,再启动默认端口
restart() {
stop
echo "准备重启..."
sleep 3
# 清空当前运行信息,启动新实例
start
}
# === 参数解析 ===
case "$1" in
"start")
start
;;
"stop")
stop
;;
"status")
status
;;
"restart")
restart
;;
*)
usage
;;
esac