一周时间里,也有不少朋友通过微信在和我交流Jenkins的一些问题,期间有一个朋友反馈到多模块部署的一个问题,说我上文中写的jenkins_restart.sh
脚本,在多模块部署的时候,没办法检测到未更新的模块;
什么意思呢?
举个例子,假如一个项目,分了10个小模块,类似于下图:
本次修改,只是模块①修复了1个Bug,其他9个都没有变动,那么编译打包整个项目之后,也只需要更新模块①即可,其他的9个模块完全可以不做任何操作,要做到这一需求,就需要在这10个模块中找出那些模块更新了,那些没有更新;上篇文章中采用的方案是:计算 jar 包的MD5,如果MD5值一样,说明没有更新;
但这是一个方案,是有问题的,下面就一起来分析一下问题点和原因;
以及如何解决多模块的自动部署问题?
我向来是比较严谨的,前文中写的脚本,是经过仔细测试的,当这位朋友说到这个问题时,我还很自信的说没有问题,能检测到;经过反复沟通之后,让我有点不自信;
按着这位朋友说的问题点,测试了一番;确实存在个问题,就算代码没有做任何的改动,Maven每次打出来的Jar包MD5值确实都不一样,只是当时脚本测试的策略有问题;就只在第一次编译的时候打了10个模块的包,之后只是测试脚本,为了追求速度,就没有再去编译各个模块了,导致后面所有的脚本测试,其实都是用的第一次打出来的Jar,所以MD5值都一样;因此整个过程,丝毫没发现有啥问题;一旦重新编译,MD5值就变了。
下面就一起来把之前的那个教程再完善一下!
MD5 判断文件是否改变,这思路似乎没有任何问题;代码既然没做任何改变,所有文件结构目录也相同,那按理说打出来的Jar包的MD5值应该是一样的,但为什么会有问题呢?为了验证这个问题,对项目连续打两次包,分别得到两个相同大小的a.jar
和b.jar
;然后做了MD5计算,发现确实不一样:
然后Beyound对两个包进行比较,发现除了修改时间不同,文件内容也都是一摸一样的;
这是为啥呢?
Java打出来的Jar包格式是以zip文件格式作为基础,为了方便,我们用Zip包做一下测试;
准备了2个相同内容的测试文件a.txt
和b.txt
,里面保存相同的内容:123
;先对txt文件进行MD5值计算,然后将两个文件打包成zip之后,再计算MD5值;
可以看出,不压缩前,a.txt
和b.txt
的MD5是一样的;压缩之后的zip包,MD5值就不同了;同时我们再看一下文件大小,不压缩前,文件只有4字节大小,可压缩之后反而变成更大的164字节;只能说明压缩的时候,还被添加了其他的信息;
经过查阅,在这篇文章中找到了原因:https://adoyle.me/blog/why-zip-file-checksum-changed.html
Zip在压缩的时候,会将将文件的access time写入到压缩包中,压缩包里面虽然保存的文件内容虽然是一致的,但由于时间不同,导致最终压缩包的MD5值也就不一致;因此,jar 包所面临的问题就属于类似的情况。
既然知道包里面的文件都是一样的,只是由于压缩带来的问题,我们完全可以换个思路来解决,将Jar包解压之后,判断各个文件是否发生变化,同样也能够校验出来,过程如下:
先直接校验Jar的MD5
如果Jar都没有重新编译打包,那不用说,MD5值肯定相同;
使用unzip
命令解压Jar包
如果直接校验Jar没通过,就继续以解压校验文件详情的方式进行校验;
unzip app.jar -d /tmp/jar_unzip_tmp
通过find
命令查找解压目录下的所有文件并计算MD5值
find /tmp/jar_unzip_tmp -type f -print | xargs md5sum > ./jar_files
# 上面的这条命令等价于下面这个for循环
#for file in `find /tmp/jar_unzip_tmp`
#do
# if [ -f $file ];then
# echo $file
# `md5sum $file >> ./jar_files`
# fi
#done
得到的jar_files
;左侧表示文件的MD5值,右侧为文件的路径;如果文件内容发生变化,左侧MD5就会不同,如果是结构/目录发生变化,右侧的详细路径就会不一样;
计算详情列表(jar_files)对应的MD5值
如果代码发生变化、目录结构发生变化,得到的文件详情列表就是产生差异,那根据详情列表得到的MD5值也就不同了
没有或者与前一次不一样MD5文件
说明发生变化,需要更新重启;Docker的方式,需要构建镜像上传
MD5校验一致
未发生变化,跳过
本文的主要目的是:优化多模块的自动化构建,能感知变化,只自动部署已经修改的模块;
通过上面的原因分析以及解决方案梳理,需要调整一下相关的脚本;
以下的内容是基于上一篇文章《手把手教你 Jenkins 自动化部署SpringBoot》的改进:
SSH的方式主要是修改jenkins_restart.sh
脚本
Docker 的方式构建,主要是修改docker-image-build.sh
把这两个脚本的修改带入到前文的对应的地方,就能正常使用了;如果还没有看过前文,麻烦稍微花点时间阅读一下,再继续往下看;
主要的修改是在jenkins_restart.sh
脚本上,当Jar被传到运行服务,执行jenkins_restart.sh
脚本启动各个模块的时候,解压检测,变化的就重启,没变的就跳过
脚本
脚本的每行都加了注释,没什么特别的地方,实现的也就是上面解决方案的步骤
脚本地址:
https://github.com/vehang/ehang-spring-boot/blob/main/script/jenkins/jenkins_restart.sh
稍微有点点长,这里分段来说明一下,详细的细节,可以查看对应的注释;完整的脚本,可点击上面的链接查看;
公共方法:直接通过Jar的MD5值检测
# 直接通过jar校验
jar_check_md5() {
# jar 包的路径
JAR_FILE=$1
if [ ! -f $JAR_FILE ]; then
# 如果校验的jar不存在 返回失败
return 1
fi
JAR_MD5_FILE=${JAR_FILE}.md5
echo "Jenkins Docker镜像构建校验 JAR的MD5文件:"$JAR_MD5_FILE
if [ -f $JAR_MD5_FILE ]; then
md5sum --status -c $JAR_MD5_FILE
RE=$?
md5sum $JAR_FILE > $JAR_MD5_FILE
return $RE
else
md5sum $JAR_FILE > $JAR_MD5_FILE
fi
return 1
}
公共方法:通过解压Jar,根据文件详情的MD5值检验是否改变
# 将Jar解压之后校验
jar_unzip_check_md5() {
# jar 包的路径
UNZIP_JAR_FILE=$1
if [ ! -f $UNZIP_JAR_FILE ]; then
# 如果校验的jar不存在 返回失败
return 1
fi
# jar的名称
UNZIP_JAR_FILE_NAME=`basename -s .jar $UNZIP_JAR_FILE`
echo "Jenkins Docker镜像构建校验 JAR包名称:"$UNZIP_JAR_FILE_NAME
# jar所在的路径
UNZIP_JAR_FILE_BASE_PATH=${UNZIP_JAR_FILE%/${UNZIP_JAR_FILE_NAME}*}
echo "Jenkins Docker镜像构建校验 JAR包路径:"$UNZIP_JAR_FILE_BASE_PATH
# 解压的临时目录
JAR_FILE_UNZIP_PATH=${UNZIP_JAR_FILE_BASE_PATH}/jar_unzip_tmp
echo "Jenkins Docker镜像构建校验 解压路径:"$JAR_FILE_UNZIP_PATH
# 用于缓存解压后文件详情的目录
UNZIP_JAR_FILE_LIST=${UNZIP_JAR_FILE_BASE_PATH}/${UNZIP_JAR_FILE_NAME}.files
echo "Jenkins Docker镜像构建校验 jar文件详情路径:"$UNZIP_JAR_FILE_LIST
# 缓存解压后文件详情的MD5
UNZIP_JAR_FILE_LIST_MD5=${UNZIP_JAR_FILE_BASE_PATH}/${UNZIP_JAR_FILE_NAME}.files.md5
echo "Jenkins Docker镜像构建校验 jar文件详情MD5校验路径:"$UNZIP_JAR_FILE_LIST
rm -rf $JAR_FILE_UNZIP_PATH
mkdir -p $JAR_FILE_UNZIP_PATH
# 解压文件到临时目录
unzip $UNZIP_JAR_FILE -d $JAR_FILE_UNZIP_PATH
# 遍历解压目录,计算每个文件的MD5值及路径 输出到详情列表文件中
find $JAR_FILE_UNZIP_PATH -type f -print | xargs md5sum > $UNZIP_JAR_FILE_LIST
rm -rf $JAR_FILE_UNZIP_PATH
if [ ! -f $UNZIP_JAR_FILE_LIST_MD5 ]; then
# 如果校验文件不存在 直接返回校验失败
md5sum $UNZIP_JAR_FILE_LIST > $UNZIP_JAR_FILE_LIST_MD5
return 1
fi
# 根据上一次生成的MD5校验
md5sum --status -c $UNZIP_JAR_FILE_LIST_MD5
RE=$?
# 生成最新的文件列表的MD5
md5sum $UNZIP_JAR_FILE_LIST > $UNZIP_JAR_FILE_LIST_MD5
# 返回校验结果
return $RE
}
公共方法:check_md5
汇总了前面两种校验方式
check_md5() {
# jar 包的路径
JAR_FILE=$1
if [ -f $JAR_FILE ]; then
# 直接通过jar校验
jar_check_md5 $JAR_FILE
if [ $? = 0 ];then
echo "Jenkins Docker镜像构建校验 通过Jar的MD5校验成功"
return 0
else
echo "Jenkins Docker镜像构建校验 通过Jar的MD5校验失败"
fi
# 通过解压jar 校验是否更新
jar_unzip_check_md5 $JAR_FILE
if [ $? = 0 ];then
echo "Jenkins Docker镜像构建校验 通过解压的MD5校验成功"
return 0
else
echo "Jenkins Docker镜像构建校验 通过解压的MD5校验失败"
fi
fi
return 1
}
判断Jar是否更新
check_md5 $JAR_FILE
if [ $? = 0 ];then
echo "Jenkins Docker镜像构建校验lib!成功,没有发生变化"$JAR_FILE
else
APP_UPDATE=true
echo "Jenkins Docker镜像构建校验lib!失败,已经更新"$JAR_FILE
fi
判断进程是否存在
PROCESS_ID=`ps -ef | grep $JAR_FILE | grep -v grep | awk '{print $2}'`
# 如果不需要重启,但是进程号没有,说明当前jar没有启动,同样也需要启动一下
if [ $RESTART == false ] && [ ${#PROCESS_ID} == 0 ] ;then
echo "没有发现进程,说明服务未启动,需要启动服务"
RESTART=true
fi
剩下的就是重启的逻辑了
测试
Docker 的镜像 和 ZIP压缩包有着类似的问题,就算是同一个jar、同一个Dockerfile,连续两次执行
docker build
构建出来的镜像,他的镜像ID也是不一样的;
Docker相比于SSH方式,在操作步骤上,就会存在一些差异,SSH方式,是在上传到服务器之后,启动Jar之前去做校验;但使用Docker的话,在Jenkins编译完之后,构建镜像之前,就需要判断那些Jar发生了变化,然后只对有变化的Jar包去构建镜像,没有改变的,跳过镜像构建;因此,Docker方式主要调整的就是镜像构建的脚本docker-image-build.sh
;其他脚本和前文的一样;
脚本调整
脚本查看地址:
https://github.com/vehang/ehang-spring-boot/blob/main/spring-boot-001-hello-world/docker/docker-image-build.sh
由于这里存在多个方案,为了方便测试,所以,就没有进一步抽象
实际业务中,对这个脚本可以再进一步抽象,将Jar路径、镜像名称等信息以参数的形式传递,实现脚本公用
这是一段Maven构建完之后,用于检测Jar是否发生更新的脚本;
稍微有点点长,我们来简单分块解读一下
在Jenkin构建镜像之前,将lib
、安装包
都拷贝到docker的配置目录下;
xxx.jar
最新Jar包,以及其对应的MD5校验文件
Dockerfile
构建当前模块镜像的Dockerfile
docker-image-build.sh
构建镜像并推送到远端镜像仓库的脚本,主要脚本之一
jar_files
缓存本次jar中文件列表信息(MD5、文件路径)
jar_files.md5
缓存上一个jar包的jar_files
对应的MD5值信息,校验是否发生变化的重要文件
jar_unzip_tmp
app.jar 解压保存的临时文件,主要为了方便输出jar_files
,用完就删掉了
lib
将会在下一篇文章讲解Maven构建压缩时用到;本文忽略
公共方法和前面是一样的
jar_check_md5()
jar_unzip_check_md5()
check_md5()
判断是否更新的逻辑都是一样的,唯一就是多了构建镜像的过程;
构建镜像
如果更新了,构建镜像
if [ $APP_UPDATE = true ]; then
# 构建镜像
docker build -t registry.cn-guangzhou.aliyuncs.com/ehang_jenkins/${MODULE_DOCKER_IMAGE_NAME}:latest ${MODULE_DOCKER_CONFIG_PATH}/.
# 将镜像推送到阿里云
docker push registry.cn-guangzhou.aliyuncs.com/ehang_jenkins/${MODULE_DOCKER_IMAGE_NAME}:latest
fi
测试
至此,多模块优化就已经弄好了,文中是根据个人的小项目在做演示,思路在文中已经说清楚了,大家可以根据自己实际的业务需求,进行适当调整,以满足自己实际的项目需求。