Spring Cloud 不停机更新 – shell 脚本

本文简单介绍关于 Spring Cloud 技术栈的对外无感知的优雅停机、更新和重启,仅通过一些微服务组件和 shell 脚本实现,不依赖容器技术。

1. 实现原理
实现对外透明的优雅更新的基本原理就是在原服务没有停机的时候启动一个新的进程,通过负载均衡让访问可以到达两个节点,然后将旧的服务“优雅”地下线,其本质是不接收新的请求,同时将正在处理中的请求完成,然后停机,之后的请求全部路由到新的服务上。
2. 服务架构
实现以上方案主要依靠 Spring Gateway 和 Nacos。
Spring Gateway 聚合内部的服务接口向外暴露,所有服务注册在 Nacos 上,在 Spring Gateway 上配置好 Nacos 使其动态路由到具体的服务而非写死 IP 和端口。
3. 具体细节
首先,优雅下线的配置是最简单的,在 Spring Boot 的配置文件中添加

server:
  shutdown: graceful

(Spring Boot 2.3+ 支持)
然后在停机时用 kill 命令就可以了,配置了优雅下线后会关闭各种连接、释放资源并主动从 Nacos 中注销。需要特别注意的是不能使用 kill -9 强制杀进程。

然后,因为需要能够在同一时间内运行两个服务,所以我们需要给它们指定不同的端口,一种较为简单的方式是在配置文件中将 server.port 配置为 0,这样启动时就会在本机没有占用的端口中随机使用一个,但缺点是无法指定端口的范围,因而我使用的是另一种方式,即在 shell 脚本中寻找一个端口然后在 jar 包的启动参数中指定。

shell 编写的大体思路是:

  1. 找到一个在指定范围内没被占用的端口,开启新的服务。
  2. 找到旧版本的进程然后 kill 关闭。

按道理说这样就行了,但是我在实际测试中发现在旧服务关闭的短暂时间内会有 Gateway 仍然路由到旧服务然后报错的情况,当前的解决方案是在关闭服务前先调用 Nacos 的 API 将旧实例从中注销,过一段时间后再杀掉进程,还需要一段时间考察。

脚本正在优化中~

#!/bin/bash
if [ $# == 0 ];then
   echo "No service version specified"
	exit 1
else
	echo "Service version: $1"
fi

# 定义变量
SERVICE_NAME="service-name"
# jar版本
JAR_VER=$1
JAR_NAME="${SERVICE_NAME}-${JAR_VER}.jar"

# 不存在版本的文件提示
if [ ! -f "$JAR_NAME" ];then
   echo "${JAR_NAME} 不存在"
	exit 1
fi

# 日志路径,加不加引号都行。 注意:等号两边 不能 有空格,否则会提示command找不到
LOG_PATH="/deploy/service-name/log/${SERVICE_NAME}-service/info.log"
# 端口范围
MIN_PORT=8000
MAX_PORT=8500

# @Desc 此脚本用于获取一个指定区间且未被占用的随机端口号
# @Author Hellxz <hellxz001@foxmail.com>

# 判断当前端口是否被占用,没被占用返回0,反之1
function checkPort {
   existPort=`/usr/sbin/lsof -i :$1|grep -v "PID" | awk '{print $2}'`
   if [ "$existPort" != "" ]; then
      echo "1"
   else
      echo "0"
   fi
}

# 指定区间随机数
function randomRange {
   shuf -i $1-$2 -n1
}

# 获取随机端口
function getRandomPort {
   temp1=0
   while [ $temp1 == 0 ]; do
       temp1=`randomRange $1 $2`
       if [ `checkPort $temp1` == 0 ] ; then
			echo $temp1
       else
         getRandomPort $1 $2
       fi
   done
}


# 启动方法
function startNew {
      # 指定端口
      nohup java -jar -Dspring.profiles.active=prod -Dserver.port=$1 $JAR_NAME > /dev/null &
      # 查出使用当前端口的新进程的 pid
      pid=`ps -ef | grep java | grep $JAR_NAME | grep $1 | grep -v grep | awk '{print $2}'`
		echo ""
      echo "Service ${JAR_NAME} is starting!pid=${pid}"
		echo "........................Here is the log.............................."
		echo "....................................................................."
      timeout 20 tail -f $LOG_PATH
		echo "........................Start successfully!........................."
}


# 旧实例的pid
OLD_PID=`ps -ef | grep java | grep $SERVICE_NAME | grep -v grep | awk '{print $2}'`
# 旧实例的端口
OLD_PORT=`netstat -anopt |grep $OLD_PID |grep LISTEN|awk '{print $4}'|rev|cut -d: -f 1|grep -E '^[0-9]{4}'|rev`
# 启动新实例
startNew $(getRandomPort $MIN_PORT $MAX_PORT)

# 本机 IP
machine_physics_net=$(ls /sys/class/net/ | grep -v "`ls /sys/devices/virtual/net/`")
LOCAL_IP=$(ip addr | grep "$machine_physics_net" | awk '/^[0-9]+: / {}; /inet.*global/ {print gensub(/(.*)\/(.*)/, "\\1", "g", $2)}')
# 通知 nacos 下线实例
curl -X PUT "127.0.0.1:8848/nacos/v1/ns/instance?serviceName=${SERVICE_NAME}&ip=${LOCAL_IP}&port=${OLD_PORT}&enabled=false"

sleep 35
# 停止旧实例
kill $OLD_PID

使用前先修改脚本中的变量,然后上传 jar 包到同目录中,执行sh update.sh ${版本号},例如 jar 包的名称是service-1.0.1.jar则执行

sh update.sh 1.0.1

可以看到启动日志,如果没有错误应该可以在 Nacos 控制台里查看新的实例。

GitHub 地址:https://github.com/kytrun/snippets/blob/master/sh/service-name-update.sh

请登录后发表评论