Jenkins自动化部署SpringBoot多模块项目

  |   0 评论   |   0 浏览

大家好,我是一航;

上周的时候,跟大家分享了通过Jenkins 一键部署SpringBoot项目:还在手动发包?手把手教你 Jenkins 自动化部署SpringBoot

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

什么意思呢?

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

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

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

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

前文中我写的脚本,是经过仔细测试的,但这位朋友说到这个问题时,我还很仔细的说没有问题,能检测到;

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

问题复现

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

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

这是为啥呢?

Zip测试及原因分析

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

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

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

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

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

解决方案

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

  • 只用 unzip命令解压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校验一致

      未发生变化,跳过

Jenkins 多模块自动构建

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

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

以下的内容是基于上一篇文章《还在手动发包?手把手教你 Jenkins 自动化部署SpringBoot》的改进,如果还没有看过前文,麻烦稍微花点时间阅读一下,再继续往下看;

SSH方式优化

SSH的方式,主要的修改是在 jenkins_restart.sh脚本上,当Jar被传到运行服务,执行 jenkins_restart.sh脚本启动各个模块的时候,解压检测,变化的就重启,没变的就跳过

  • 脚本

    脚本的每行都加了注释,没什么特别的地方,实现的也就是上面解决方案的四个步骤

    #!/bin/sh
    
    # JDK的环境变量
    export JAVA_HOME=/usr/local/jdk-11.0.14
    export PATH=$JAVA_HOME/bin:$PATH
    
    # 基础路径,由参数传入
    # 多模块的时候,需要在路径中使用*统配一下多模块
    # 比如/opt/ehang-spring-boot是多模块,下面由module1和module2
    # 那么执行shell的时候使用:sh restart.sh /opt/ehang-spring-boot/\*  注意这里的*需要转义一下
    JAR_BATH=$1
    echo "基础路径:"$JAR_BATH
    JAR_PATH=${JAR_BATH}/target/*.jar
    
    # 临时的解压目录
    JAR_UNZIP_PATH=/tmp/jar_unzip_tmp
    
    # 获取所有的JAR 开始遍历
    for JAR_FILE in $JAR_PATH
    do
    if [ -f $JAR_FILE ]
    then
      echo ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"
      echo "JAR路径:"$JAR_FILE
      #JAR_FILE_MD5=${JAR_FILE}.md5
    
      # 解压目录的文件列表详情及MD5
      JAR_FILES_INFO=${JAR_FILE}_files
      # 详情列表文件的MD5详细
      JAR_FILES_INFO_MD5=${JAR_FILES_INFO}.md5
    
      # 删除解压后的临时文件夹 避免之前的缓存导致解压失败
      rm -rf $JAR_UNZIP_PATH
      # 解压文件
      unzip $JAR_FILE -d $JAR_UNZIP_PATH
      # 遍历解压目录,计算每个文件的MD5值及路径 输出到详情列表文件中
      find $JAR_UNZIP_PATH -type f -print | xargs md5sum > $JAR_FILES_INFO
      # 上面的这条命令等价于下面这个for循环
      #for file in `find $JAR_UNZIP_PATH`
      #do
      #  if [ -f $file ];then
      #    echo $file
      #    `md5sum $file >> $JAR_FILES_INFO`
      #  fi
      #done
    
    
      # 用于标记是否需要重启的标识
      RESTART=false
    
      # 判断MD5文件是否存在,存在就校验MD5值
      if [ -f $JAR_FILES_INFO_MD5 ]; then
        # 校验MD5
        md5sum --status -c $JAR_FILES_INFO_MD5
        # = 0表示校验成功 =1 表示校验失败
        if [ $? = 1 ];then
          echo "MD5校验失败,安装包已经更新!"
          RESTART=true
        else
          echo "与前一次的MD5匹配成功,说明安装包没有更新!"
        fi
      else
        echo "没有MD5值,说明是第一次启动"
        RESTART=true
      fi
    
      # 获取进程号 判断当前服务是否启动;如果Jar没变,但是服务未启动,也需要执行启动脚本
      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
    
      # 如果是需要启动
      if [ $RESTART == true ]; then
          # kill掉原有的进程
          ps -ef | grep $JAR_FILE | grep -v grep | awk '{print $2}' | xargs kill -9
    
          #如果出现Jenins Job执行完之后,进程被jenkins杀死,可尝试放开此配置项
          #BUILD_ID=dontKillMe
          #启动Jar
          nohup java -jar $JAR_FILE > ${JAR_FILE}.log 2>&1 &
          # =0 启动成功 =1 启动失败
          if [ $? == 0 ];then
              echo "restart success!!! process id:" `ps -ef | grep $JAR_FILE | grep -v grep | awk '{print $2}'`
          else
              echo "启动失败!"
          fi
    
          # 将最新的MD5值写入到缓存文件
          md5sum $JAR_FILES_INFO > $JAR_FILES_INFO_MD5
      fi
      echo "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"
      echo ""
    fi
    done
    
  • 测试

Docker 方式优化

Docker 的镜像 和 ZIP压缩包有着类似的问题,就算是同一个jar、同一个Dockerfile,连续两次执行 docker build构建出来的镜像,他的镜像ID也是不一样的;

Docker相比于SSH方式,在操作步骤上,就会存在一些差异,SSH方式是在启动Jar之前去做校验;但如果使用Docker的方式,在Jenkins镜像构建之前,就需要判断那些Jar发生了变化,然后只对有变化的Jar包去构建镜像,没有改变的,跳过镜像构建;因此,Docker方式主要调整的就是镜像构建的脚本 docker-image-build.sh;其他脚本和前文的一样;

  • 脚本调整

    # 该脚本是用于单jar(业务和依赖绑定)的检测,打包,部署
    
    # docker的名称
    MODULE_DOCKER_IMAGE_NAME=ehang-sping-boot-hello-world
    # 项目目录
    MODULE_BATH_PATH=./spring-boot-001-hello-world
    # docker的配置文件目录
    MODULE_DOCKER_CONFIG_PATH=${MODULE_BATH_PATH}/docker
    
    # jar_check_md5 通过jar的md5值直接检测
    # jar_unzip_check_md5 通过对jar包解压 校验文件详情的MD5
    # check_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解压之后校验
    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() {
      # 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
    }
    
    \cp -r ${MODULE_BATH_PATH}/target/*.jar ${MODULE_DOCKER_CONFIG_PATH}
    
    APP_UPDATE=false
    for APP_JAR_FILE in ${MODULE_DOCKER_CONFIG_PATH}/*.jar
    do
      echo $APP_JAR_FILE
      if [ -f $APP_JAR_FILE ];then
        echo "Jenkins Docker镜像构建校验lib 依赖Jar:"$APP_JAR_FILE
        check_md5 $APP_JAR_FILE
        if [ $? = 0 ];then
          echo "Jenkins Docker镜像构建校验lib!成功,没有发生变化"$APP_JAR_FILE
        else
          APP_UPDATE=true
          echo "Jenkins Docker镜像构建校验lib!失败,已经更新"$APP_JAR_FILE
        fi
      fi
    done
    
    
    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
    

    这是一段Maven构建完之后,用于检测Jar是否发生更新的脚本,稍微有点点长,我们来简单的分块解读一下

    下面是几个公共方法:

    • 直接通过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
      }
      
    • 判断APP的jar是否更新了

      APP_UPDATE=false
      for APP_JAR_FILE in ${MODULE_DOCKER_CONFIG_PATH}/*.jar
      do
        echo $APP_JAR_FILE
        if [ -f $APP_JAR_FILE ];then
          echo "Jenkins Docker镜像构建校验lib 依赖Jar:"$APP_JAR_FILE
          check_md5 $APP_JAR_FILE
          if [ $? = 0 ];then
            echo "Jenkins Docker镜像构建校验lib!成功,没有发生变化"$APP_JAR_FILE
          else
            APP_UPDATE=true
            echo "Jenkins Docker镜像构建校验lib!失败,已经更新"$APP_JAR_FILE
          fi
        fi
      done
      
    • 构建镜像

      如果更新了,构建镜像

      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
      

    Jenkin是在构建之前,会在各个项目的目录下,创建了一个临时的 tmp文件夹,用来临时汇总配置、脚本、jar、解压目录等

    • app.jar

      当前模块的最新Jar包

    • 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构建压缩时用到;本文忽略

  • 测试

    • 第一次构建

    • 未更新

    • 已更新