SpringCloud使用jenkins进行平滑发布

1、阅读目标人群

需要对Spring cloud和Jenkins的原理和操作有基本的了解,本文不适合完全的新手阅读。

2、使用背景

当使用Spring Cloud构架的时候往往对于每一个微服务会进行集群部署,以达到高可用和扩展性能的目的。但是这样的架构和部署方式大大增加了部署的难度。假设我们有A\B\C\D\E,5个微服务,每个微服务都部署2台服务器,那么做一次大更新则需要发布10次项目才可以全部完成。如果这一切都需要人工操作那么将会非常的麻烦,而且容易出错。这个时候我们就需要借助Jenkins来帮我们发布项目,当你配置好Jenkins之后就只要点一个按钮就可以了。发布的过程会交给Jenkins自己来完成。

3、要解决的问题

当使用Jenkins发布之后,会存在一个问题:发布的过程中,对外提供服务会有短暂的不可用(持续时间为分钟级别)。这个就是我们要解决的问题。如何保证使用jenkins发布的时候保证对外提供持续稳定的服务。

4、出现问题的原因

如果不经过精心的设计,一般jenkins发布一个项目的流程如下:

  1. 从git\svn下载最新代码
  2. 在本地进行构建
  3. kill本地java进程
  4. 启动刚刚构建好的jar包

问题出现在第三步和第四步的时候。当kill掉本地java进程时,并没有及时通知注册中心eureka该微服务在这台机器已经下线。而此时新构建的jar包还未能提供服务。于是网关zuul还是不断的把流量转发给这台机器的微服务,然而这个微服务已经不能正常工作了。其他微服务也没及时感知到这个微服务已经下线,于是也会导致其他微服务出错。

5、解决问题的原理

总结来说,为什么不能平滑发布是因为其他微服务(包括网关zuul)并没有立即感知到这个微服务下线了,所以还会继续转发流量过来。那么我们优化一下流程即可:

  1. 从git\svn下载最新代码
  2. 在本地进行构建
  3. 使用api通知注册中心eureka下线微服务
  4. 等待1分钟,让下线的状态广播到所有微服务
  5. kill本地java进程
  6. 启动刚刚构建好的jar包

通过增加了第三步和第四步,我们在kill之前提前通知所有微服务下线,等1分钟之后就没有流量转发到这个微服务,于是这个微服务就可以安全的下线了。

具体eureka下线的接口可以参考这个文档

6、实战解决

6.1、缩短微服务拉去注册中心状态的时间

在上一节讲到等待1分钟,那么这1分钟的时间内就要保证将下线的状态通知到所有的微服务,这就要求其他微服务能在1分钟内从注册中心拉取到最新的微服务列表。具体的配置可以参考这两篇文章

6.2、jenkins发布脚本

这个shell的功能比较全,包含了“发布”,“自动备份”,“回滚”,”检测本次发布是否成功”这4个核心功能。使用时只需要根据自己的情况修改“变量区”的内容就好了。

额外的,这里要求jenkins多几个配置项以支撑归滚功能。

jenkins回滚

jenkins回滚

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258

#################################变量区######################################
#发布环境
START_ENV="prod"
#项目名称
PROJECT="order-system"
#项目在git仓库中的路径
GIT_PROJECT_PATH="./"
#在注册中心的名称
APP_NAME="order-system"
#微服务的端口
APP_PORT="8080"
#注册中心的地址
EUREKA_URL="http://192.168.10.11:8761/eureka"
#备份路径
BAK_DIR="/tmp/backup/$PROJECT"
#最多备份数
MAX_BAK_NUM=10;
#部署路径
DEPLOY_DIR="/home/jenkins_deploy"
#################################变量区######################################
#请勿修改该参数!是否在注册中心在线(1是0否)
IS_ONLINE="0"
#获取ip
getIp(){
IP=`/sbin/ifconfig -a|grep inet|grep -v 127.0.0.1|grep -v 172.|grep -v inet6|awk '{print $2}'|tr -d "addr:|地址"`
if [ "$IP" = "" ];then
echo -e "\033[31m can't get Ip \033[0m"
exit 1
fi
}

#指定进程是否在运行
isProcessRun(){
PIDS=`ps -ef |grep ${PROJECT} |grep -v grep | awk '{print $2}'`
}


#服务下线
changeStatusOffline(){
echo "change status url:${EUREKA_URL}/apps/${APP_NAME}/${IP}:${APP_NAME}:${APP_PORT}/status?value=OUT_OF_SERVICE";
statusResult=`curl -X PUT -s -m 10 --connect-timeout 10 -i ${EUREKA_URL}/apps/${APP_NAME}/${IP}:${APP_NAME}:${APP_PORT}/status?value=OUT_OF_SERVICE`
statusCode=`echo $statusResult|grep "HTTP"|awk '{print $2}'`
if [[ "$statusCode" != "200" ]] && [[ "$statusCode" != "404" ]]; then
echo -e "\033[31m offline service is fail!!!! response code:${statusCode} \033[0m"
exit 1
else
echo -e "\033[33m wait offline status notify other service,it will take 60 seconds... \033[0m"
sleep 60
fi
}

#服务上线
changeStatusUp(){
echo -e "\033[33m wait service ready to use,it will take 3 seconds... \033[0m"
sleep 3
echo "change status url:${EUREKA_URL}/apps/${APP_NAME}/${IP}:${APP_NAME}:${APP_PORT}/status?value=UP";
statusResult=`curl -X PUT -s -m 10 --connect-timeout 10 -i ${EUREKA_URL}/apps/${APP_NAME}/${IP}:${APP_NAME}:${APP_PORT}/status?value=UP`
statusCode=`echo $statusResult|grep "HTTP"|awk '{print $2}'`
if [[ "$statusCode" != "200" ]] && [[ "$statusCode" != "404" ]]; then
echo -e "\033[31m let service online is fail!!!! response code:${statusCode} \033[0m"
exit 1
fi
}

#备份jar之前的包
backUp(){
if [ ! -d ${BAK_DIR} ]; then
mkdir -p "${BAK_DIR}"
fi

if [ "$?" != "0" ]; then
echo -e "\033[31m backup fail,can't operate ${BAK_DIR} directory!!!!!!!!! \033[0m"
exit 1
fi

if [ -f ${DEPLOY_DIR}/${PROJECT}".jar" ]; then
backUpPath="${BAK_DIR}/${PROJECT}_${BUILD_NUMBER}.jar"
if [ -f ${backUpPath} ]; then
echo -e "\033[31m backUp build number(${BUILD_NUMBER}) is repeat! \033[0m"
exit 1
fi
#复制到备份文件夹
cp -f ${DEPLOY_DIR}/${PROJECT}.jar ${backUpPath}
echo -e "\033[32m backup success!(${backUpPath}) \033[0m"
else
echo -e "\033[33m ${PROJECT}.jar is not found,skip backup process! \033[0m"
fi
}


#查看已经注册中心上是否已经关闭
isRemoteAlive(){
info=`curl -s -m 10 --connect-timeout 10 -i ${EUREKA_URL}/instances/${IP}:${APP_NAME}:${APP_PORT}`
code=`echo $info|grep "HTTP"|awk '{print $2}'`
if [ "$code" = "" ]; then
echo -e "\033[31m eureka is not online!!!! \033[0m"
exit 1
fi
status=`echo $info|grep -e "OUT_OF_SERVICE" -e "<status>UP</status>"`
if [ "$status" != "" ]; then
echo -e "\033[32m online in eureka! \033[0m"
IS_ONLINE=1
else
echo -e "\033[33m offline in eureka! \033[0m"
IS_ONLINE=0
fi
}

#关停服务
shutdownService(){
#先下线服务
changeStatusOffline
#获取是否运行了服务
isProcessRun
echo -e "\033[32m service PIDS:$PIDS \033[0m"
if [ "$PIDS" != "" ]; then
ps -ef|grep "${PIDS}"|grep -v grep|cut -c 9-15|xargs kill
if [ "$?" != "0" ]; then
echo -e "\033[31m fail to kill process[${PIDS}]!!! \033[0m"
exit 1
fi
fi

}

#构建
build(){
cd $WORKSPACE/$GIT_PROJECT_PATH
echo -e "\033[32m mvn install .......... \033[0m"
mvn clean package -e -U -q
if [ ! -f $WORKSPACE/$GIT_PROJECT_PATH/target/${PROJECT}.jar ]; then
echo -e "\033[31m maven build fail,jar is not found! ($WORKSPACE/$GIT_PROJECT_PATH/target/${PROJECT}.jar) \033[0m"
exit 1
fi
}

#部署服务
deloyService(){
if [ ! -d ${DEPLOY_DIR} ]; then
mkdir -p "${DEPLOY_DIR}"
fi
cp -f $WORKSPACE/$GIT_PROJECT_PATH/target/${PROJECT}.jar ${DEPLOY_DIR}
chmod a+rw ${DEPLOY_DIR} -R
}

#启动服务
startService(){
BUILD_ID=dontKillMe nohup java -server -jar ${DEPLOY_DIR}/${PROJECT}.jar --spring.profiles.active=${START_ENV} --eureka.instance.metadata-map.sv=${SERVER_VERSION} &
}


#回滚
rollback(){
if [ ! -f ${BAK_DIR}/${PROJECT}_${ROLLBACK_NUMBER}".jar" ]; then
echo -e "\033[31m bakup not found (${BAK_DIR}/${PROJECT}_${ROLLBACK_NUMBER}.jar) \033[0m"
exit 1
fi
cp -f ${BAK_DIR}/${PROJECT}_${ROLLBACK_NUMBER}.jar ${DEPLOY_DIR}/${PROJECT}.jar
startService
}

#删除历史备份
deleteHistoryBackUp(){
#当前备份总数
backUpCount=`ls -l ${BAK_DIR}| grep "^-" | wc -l`
#要删除的备份数量
deleteBakNum=`expr $backUpCount - $MAX_BAK_NUM `
if [ $deleteBakNum -gt 0 ]; then
echo `ls -lt ${BAK_DIR}| tail -f -n ${deleteBakNum} |awk '{print $9}'`
echo "delete ${deleteBakNum} backUp file"
ls -lt ${BAK_DIR}| tail -f -n ${deleteBakNum} |awk '{print $9}' | xargs -I {} rm -f "${BAK_DIR}/{}"
fi
}

####################发布流程开始#########################
echo -e "\033[32m deploy_env:${DEPLOY_ENV} rollback number:${ROLLBACK_NUMBER} .\033[0m"
#获取ip
getIp

#判断参数是否正确
if [ "$DEPLOY_ENV" = "rollback" ]; then
if [[ ! "$ROLLBACK_NUMBER" =~ ^[0-9]+$ ]]; then
echo -e "\033[31m ROLLBACK_NUMBER is illegal!!!!! \033[0m"
exit 1
fi
fi

if [[ "$DEPLOY_ENV" = "new_deploy" ]] || [[ "$DEPLOY_ENV" = "hotfix_deploy" ]]; then
if [[ ! "$SERVER_VERSION" =~ .+ ]]; then
echo -e "\033[31m SERVER_VERSION is illegal!!!!! \033[0m"
exit 1
fi
fi

#如果是发布,则先构建(怕构建失败)
if [[ "$DEPLOY_ENV" = "new_deploy" ]] || [[ "$DEPLOY_ENV" = "hotfix_deploy" ]] ; then
#构建
build
#备份
backUp
fi

#停服务(如果启动了)
shutdownService

testCount=1

#循环看是否停服务成功
while [[ "$PIDS" != "" ]]; do
echo -e "\033[33m Service is shutting down... \033[0m"
sleep 2
isProcessRun
let "testCount++"
if [ $testCount -eq 60 ] ; then
echo -e "\033[31m wait too long to kill process \033[0m"
exit 1
fi
done

if [[ "$DEPLOY_ENV" = "new_deploy" ]] || [[ "$DEPLOY_ENV" = "hotfix_deploy" ]] ; then
#部署
deloyService
#启动
startService
elif [ "$DEPLOY_ENV" = "rollback" ] ; then
#回滚
rollback
else
echo -e "\033[31m $DEPLOY_ENV is illegal!!!!! \033[0m"
fi


checkCount=1
#循环看是否启动服务成功
echo -e "\033[32m checking service is online.... \033[0m"
echo -e "\033[32m check url:${EUREKA_URL}/instances/${IP}:${APP_NAME}:${APP_PORT} \033[0m"
while [[ "$IS_ONLINE" = "0" ]]; do
sleep 2
let "checkCount++"
isRemoteAlive
if [ $checkCount -eq 60 ] ; then
echo -e "\033[31m wait too long to check service online status \033[0m"
exit 1
fi
done

#上线服务
changeStatusUp

#删除备份
deleteHistoryBackUp

echo -e "\033[32m !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!deploy success!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! \033[0m"

####################发布流程结束#########################


原文链接:https://www.jdkdownload.com/jenkins_springcloud.html