用户无感知更新

公司目前的整个CI体系都是使用了GitLab的全家桶,基本已经趋于完善,目前最大的问题是每次发版都需要停机处理,用户感知明显,所以多数时候发版的时间都集中在晚上十点以后,故而决定设法解决这个问题。

分析

目前整套服务里,部分环境单体,部分环境负载均衡,均使用原生部署,K8S的滚动更新可以解决这个问题,但是目前的状况和K8S无疑还有很大的距离,负载均衡环境中可以一台台逐步更新,但因为实际情况中还涉及到了单体的情况,所以思路就分成了两条:

  1. 所有的服务都采用负载均衡部署,即使是只有单个API服务,单个API服务在发版时,启动一个新的API服务并加入负载,健康检查合格后,杀死旧的API进程,让服务无痕切换。多个API服务跟以上类似,只不过无需杀死服务,而可以直接将待操作服务的负载全部转到其他服务,直接停掉待操作服务操作。
  2. 保持原有整个架构不变,但是在原本的 负载均衡/反向代理 和 API服务 中间加一层端口转发,端口转发服务监听和 负载均衡/反向代理 交互的 端口,API服务采用不固定端口模式,在启用且健康检查合格后才通知端口转发服务将通讯端口转发到新服务的端口上。
    基于现有情况考虑,决定采用第二种思路,为了保证统一性,端口转发依然采用nginx。

实现

整理整个过程为:
获取随机可用端口 -> 使用端口启动服务 -> 健康检查 -> 切换端口

获取随机可用端口

这里我是用Shell脚本做了对应的处理,具体脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#!/bin/bash

# portRange="80-81" # 可用于读取配置文件
# rangeStart=$(echo ${portRange} | awk -F '-' '{print $1}')
# rangeEnd=$(echo ${portRange} | awk -F '-' '{print $2}')

rangeStart=$1
rangeEnd=$2

if [ $1 -le $2 ]; then
echo "123" >/dev/null
else
echo "error: please check port range"
exit
fi

PORT=0
# 判断当前端口是否被占用,没被占用返回0,反之1
function Listening() {
TCPListeningnum=$(netstat -an | grep ":$1 " | awk '$1 == "tcp" && $NF == "LISTEN" {print $0}' | wc -l)
UDPListeningnum=$(netstat -an | grep ":$1 " | awk '$1 == "udp" && $NF == "0.0.0.0:*" {print $0}' | wc -l)
((Listeningnum = TCPListeningnum + UDPListeningnum))
if [ $Listeningnum == 0 ]; then
echo "0"
else
echo "1"
fi
}

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

# 得到随机端口
function get_random_port() {
templ=0
while [ $PORT == 0 ]; do
temp1=$(random_range $1 $2)
if [ $(Listening $temp1) == 0 ]; then
PORT=$temp1
fi
done
echo "$PORT"
}

# main
get_random_port ${rangeStart} ${rangeEnd}

注意:依赖了指令netstat,需要安装对应的包

1
2
3
4
# Debian/Ubuntu
apt install -y net-tools
# CentOS/Redhat
yum install -y net-tools

调用逻辑:

1
2
export AVAILABLE_PORT=$(/opt/script/check/available_port.sh 8000 9000)
echo $AVAILABLE_PORT

使用对应端口启动服务

因为已经使用了PM2作为 Daemon ,所以无法使用启动参数的形式以命令行把口传给服务了,有些服务器上不止放了一个服务,故而全局环境变量的思路也可能不行,最终选择了使用Shell命令修改配置文件中的绑定地址

此处用到了健康检查的脚本 api_health_check.sh ,具体实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/bin/bash
url=$1


#定义函数check_http:
#使用curl命令检查http服务器的状态
#-m设置curl不管访问成功或失败,最大消耗的时间为5秒,5秒连接服务为相应则视为无法连接
#-s设置静默连接,不显示连接时的连接速度、时间消耗等信息
#-o将curl下载的页面内容导出到/dev/null(默认会在屏幕显示页面内容)
#-w设置curl命令需要显示的内容%{http_code},指定curl返回服务器的状态码
check_http(){
status_code=$(curl -m 5 -s -o /dev/null -w %{http_code} $url)
}

for varible1 in {1..25}
#for varible1 in 1 2 3 4 5
do
check_http
date=$(date +%Y%m%d-%H:%M:%S)
#生成报警邮件的内容
echo "当前时间为:$date
$url服务器异常,状态码为${status_code}.
请尽快排查异常." > /tmp/http$$.pid
#指定测试服务器状态的函数,并根据返回码决定是发送邮件报警还是将正常信息写入日志
if [ $status_code -ne 200 ];then
if [ ${varible1} -lt 20 ];then
echo 1
sleep 1
else
echo $varible1
sleep $varible1
fi
else
echo "$url连接正常"
exit 0
fi
done
exit 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 获取版本 自增版本号
if [ ! -f "/opt/rolling-update/ShortUrl.current" ]; then echo 0 > /opt/rolling-update/ShortUrl.current; export CURRENT_VERSION=A; export NEXT_VERSION=0; else export CURRENT_VERSION=$(cat /opt/rolling-update/ShortUrl.current); (echo $CURRENT_VERSION| awk '{print (int($0)+1)%5}') > /opt/rolling-update/ShortUrl.current; export NEXT_VERSION=$(cat /opt/rolling-update/ShortUrl.current); fi
# 新建新版本文件夹
mkdir ~/ShortUrl$NEXT_VERSION
# 发布
dotnet publish -c Release --output bin/publish
cd bin/publish
zip -r $COMMIT_TIME_STR.zip ./
# PUSH文件
lftp sftp://$DEV_URL -e "user $PUB_USER $DEV_PASS; cd /~/ShortUrl$NEXT_VERSION; put $COMMIT_TIME_STR.zip; bye"
# 替换配置文件中的端口
cd ~/ShortUrl$NEXT_VERSION
sed -E "s|http[^\"^\']*:8750|http://*:$AVAILABLE_PORT|g" appsettings.json -i.bak
# 启动 Daemon 监听
pm2 start "dotnet ShortUrl.dll" --name ShortUrl$NEXT_VERSION --env={"ASPNETCORE_ENVIRONMENT":"Development"}
# 健康检查
/opt/script/check/api_health.sh http://127.0.0.1:$AVAILABLE_PORT/healthz
# 切换代理
rm -Rf /etc/nginx/port_transfer/stm/short_url.stm;
echo -e "upstream short-url{ server 127.0.0.1:$AVAILABLE_PORT; }" >> /etc/nginx/port_transfer/stm/short_url.stm;
nginx -t;
nginx -s reload;
# 等待时间
sleep 5
# 杀死旧进程
pm2 stop ShortUrl$CURRENT_VERSION
pm2 del ShortUrl$CURRENT_VERSION
pm2 save
# 清除旧版文件
rm -Rf ~/ShortUrl$CURRENT_VERSION

欢迎关注我的其它发布渠道