公司目前的整个CI体系都是使用了GitLab的全家桶,基本已经趋于完善,目前最大的问题是每次发版都需要停机处理,用户感知明显,所以多数时候发版的时间都集中在晚上十点以后,故而决定设法解决这个问题。
分析
目前整套服务里,部分环境单体,部分环境负载均衡,均使用原生部署,K8S的滚动更新可以解决这个问题,但是目前的状况和K8S无疑还有很大的距离,负载均衡环境中可以一台台逐步更新,但因为实际情况中还涉及到了单体的情况,所以思路就分成了两条:
- 所有的服务都采用负载均衡部署,即使是只有单个API服务,单个API服务在发版时,启动一个新的API服务并加入负载,健康检查合格后,杀死旧的API进程,让服务无痕切换。多个API服务跟以上类似,只不过无需杀死服务,而可以直接将待操作服务的负载全部转到其他服务,直接停掉待操作服务操作。
- 保持原有整个架构不变,但是在原本的 负载均衡/反向代理 和 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
rangeStart=$1 rangeEnd=$2
if [ $1 -le $2 ]; then echo "123" >/dev/null else echo "error: please check port range" exit fi
PORT=0
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" }
get_random_port ${rangeStart} ${rangeEnd}
|
注意:依赖了指令netstat,需要安装对应的包
1 2 3 4
| apt install -y net-tools
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(){ status_code=$(curl -m 5 -s -o /dev/null -w %{http_code} $url) } for varible1 in {1..25}
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 ./
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
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
|