Jenkins自動化部署SpringBoot多模組專案

ehang發表於2023-11-19

大家好,我是一航;

上週的時候,跟大家分享了透過Jenkins 一鍵部署SpringBoot專案: 還在手動發包?手把手教你 Jenkins 自動化部署SpringBoot

這一週時間,也有不少朋友透過微信在和我交流Jenkins環境搭建的一些問題,期間就有一個朋友反饋到多模組部署的一個問題,說我寫的 jenkins_restart.sh指令碼, 在多模組部署的時候,沒辦法檢測到未更新的模組

什麼意思呢?

舉個例子,加入一個專案,分了10個小模組,類似於下圖:

本次修改,只是模組①修復了1個Bug,其他9個都沒有變動,那麼編譯打包整個專案之後,也只需要更新模組①即可,其他的9個模組完全可以不做任何操作,要做到這一需求,就需要在這10個模組中找出那些模組更新了,那些沒有更新;上篇文章中採用的方案是: 計算 jar 包的MD5,如果MD5值一樣,說明沒有更新

但這是一個方案,是有問題,下面就一起來分析一下問題原因;

以及如何解決多模組的自動部署問題?

前文中我寫的指令碼,是經過仔細測試的,但這位朋友說到這個問題時,我還很仔細的說沒有問題,能檢測到;

經過反覆溝通之後,讓我有點不自信;按著這位朋友說的問題點,測試了一番;確實存在這個問題, 就算程式碼沒有做任何的改動,Maven打出來的Jar包MD5值都不一樣,只是當時指令碼測試的策略不對;我就只在第一次編譯的時候打了10個模組的包,之後只是測試指令碼,為了追求速度,就沒有再去編譯各個模組了,導致後面所有的指令碼測試,都是用的第一次打出來的Jar,所以MD5值都一樣;因此整個過程,絲毫沒發現有啥問題。

1 問題復現

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 包所面臨的問題就屬於類似的情況。

2 解決方案

既然知道包裡面的檔案都是一樣的,只是由於壓縮帶來的問題,我們完全可以換個思路來解決,將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校驗一致

      未發生變化,跳過

3 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構建壓縮時用到;本文忽略

  • 測試

    • 第一次構建

    • 未更新

    • 已更新


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70035356/viewspace-2996035/,如需轉載,請註明出處,否則將追究法律責任。

相關文章