手把手教你 Jenkins 自动部署 SpringBoot 多模块应用

手把手教你 Jenkins 自动部署 SpringBoot 多模块应用_第1张图片

一周时间里,也有不少朋友通过微信在和我交流Jenkins的一些问题,期间有一个朋友反馈到多模块部署的一个问题,说我上文中写的jenkins_restart.sh脚本,在多模块部署的时候,没办法检测到未更新的模块

什么意思呢?

举个例子,假如一个项目,分了10个小模块,类似于下图:

手把手教你 Jenkins 自动部署 SpringBoot 多模块应用_第2张图片

本次修改,只是模块①修复了1个Bug,其他9个都没有变动,那么编译打包整个项目之后,也只需要更新模块①即可,其他的9个模块完全可以不做任何操作,要做到这一需求,就需要在这10个模块中找出那些模块更新了,那些没有更新;上篇文章中采用的方案是:计算 jar 包的MD5,如果MD5值一样,说明没有更新

但这是一个方案,是有问题的,下面就一起来分析一下问题点和原因;

以及如何解决多模块的自动部署问题?

我向来是比较严谨的,前文中写的脚本,是经过仔细测试的,当这位朋友说到这个问题时,我还很自信的说没有问题,能检测到;经过反复沟通之后,让我有点不自信;

手把手教你 Jenkins 自动部署 SpringBoot 多模块应用_第3张图片

按着这位朋友说的问题点,测试了一番;确实存在个问题,就算代码没有做任何的改动,Maven每次打出来的Jar包MD5值确实都不一样,只是当时脚本测试的策略有问题;就只在第一次编译的时候打了10个模块的包,之后只是测试脚本,为了追求速度,就没有再去编译各个模块了,导致后面所有的脚本测试,其实都是用的第一次打出来的Jar,所以MD5值都一样;因此整个过程,丝毫没发现有啥问题;一旦重新编译,MD5值就变了。

手把手教你 Jenkins 自动部署 SpringBoot 多模块应用_第4张图片

下面就一起来把之前的那个教程再完善一下!

问题复现

MD5 判断文件是否改变,这思路似乎没有任何问题;代码既然没做任何改变,所有文件结构目录也相同,那按理说打出来的Jar包的MD5值应该是一样的,但为什么会有问题呢?为了验证这个问题,对项目连续打两次包,分别得到两个相同大小的a.jarb.jar;然后做了MD5计算,发现确实不一样:

手把手教你 Jenkins 自动部署 SpringBoot 多模块应用_第5张图片

然后Beyound对两个包进行比较,发现除了修改时间不同,文件内容也都是一摸一样的

手把手教你 Jenkins 自动部署 SpringBoot 多模块应用_第6张图片

手把手教你 Jenkins 自动部署 SpringBoot 多模块应用_第7张图片

这是为啥呢?

Zip测试及原因分析

Java打出来的Jar包格式是以zip文件格式作为基础,为了方便,我们用Zip包做一下测试;

准备了2个相同内容的测试文件a.txtb.txt,里面保存相同的内容:123;先对txt文件进行MD5值计算,然后将两个文件打包成zip之后,再计算MD5值;

手把手教你 Jenkins 自动部署 SpringBoot 多模块应用_第8张图片

可以看出,不压缩前,a.txtb.txt的MD5是一样的;压缩之后的zip包,MD5值就不同了;同时我们再看一下文件大小,不压缩前,文件只有4字节大小,可压缩之后反而变成更大的164字节;只能说明压缩的时候,还被添加了其他的信息

经过查阅,在这篇文章中找到了原因:https://adoyle.me/blog/why-zip-file-checksum-changed.html

Zip在压缩的时候,会将将文件的access time写入到压缩包中,压缩包里面虽然保存的文件内容虽然是一致的,但由于时间不同,导致最终压缩包的MD5值也就不一致;因此,jar 包所面临的问题就属于类似的情况。

解决方案

既然知道包里面的文件都是一样的,只是由于压缩带来的问题,我们完全可以换个思路来解决,将Jar包解压之后,判断各个文件是否发生变化,同样也能够校验出来,过程如下:

  1. 先直接校验Jar的MD5

    如果Jar都没有重新编译打包,那不用说,MD5值肯定相同;

  2. 使用unzip命令解压Jar包

    如果直接校验Jar没通过,就继续以解压校验文件详情的方式进行校验;

    unzip app.jar -d /tmp/jar_unzip_tmp
    

    手把手教你 Jenkins 自动部署 SpringBoot 多模块应用_第9张图片

  3. 通过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就会不同,如果是结构/目录发生变化,右侧的详细路径就会不一样;

    手把手教你 Jenkins 自动部署 SpringBoot 多模块应用_第10张图片

  4. 计算详情列表(jar_files)对应的MD5值

    如果代码发生变化、目录结构发生变化,得到的文件详情列表就是产生差异,那根据详情列表得到的MD5值也就不同了

    • 没有或者与前一次不一样MD5文件

      说明发生变化,需要更新重启;Docker的方式,需要构建镜像上传

    • MD5校验一致

      未发生变化,跳过

Jenkins 多模块自动构建

本文的主要目的是:优化多模块的自动化构建,能感知变化,只自动部署已经修改的模块

通过上面的原因分析以及解决方案梳理,需要调整一下相关的脚本;

以下的内容是基于上一篇文章《手把手教你 Jenkins 自动化部署SpringBoot》的改进:

  • SSH的方式主要是修改jenkins_restart.sh脚本

  • Docker 的方式构建,主要是修改docker-image-build.sh

把这两个脚本的修改带入到前文的对应的地方,就能正常使用了;如果还没有看过前文,麻烦稍微花点时间阅读一下,再继续往下看;

SSH方式优化

主要的修改是在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
      
    • 剩下的就是重启的逻辑了

  • 测试

    手把手教你 Jenkins 自动部署 SpringBoot 多模块应用_第11张图片

Docker 方式优化

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的配置目录下;

    手把手教你 Jenkins 自动部署 SpringBoot 多模块应用_第12张图片

    • 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
      
  • 测试

    • 第一次构建

      手把手教你 Jenkins 自动部署 SpringBoot 多模块应用_第13张图片

    • 未更新

      手把手教你 Jenkins 自动部署 SpringBoot 多模块应用_第14张图片

    • 已更新

      手把手教你 Jenkins 自动部署 SpringBoot 多模块应用_第15张图片

至此,多模块优化就已经弄好了,文中是根据个人的小项目在做演示,思路在文中已经说清楚了,大家可以根据自己实际的业务需求,进行适当调整,以满足自己实际的项目需求。

你可能感兴趣的:(Jenkins,jenkins,spring,boot,java)