使用 Jenkins 執行持續集成的幾個實用經驗分享

目錄

衆所周知,持續構建與發佈是我們日常工作中要面對的的一個重要環節,目前很多公司都採用 Jenkins 來搭建符合需求的 CI/CD 流程,作爲一個持續集成的開源工具,它以安裝啓動方便,配置簡單,上手容易的特點,深受廣大用戶的歡迎,通過筆者這幾年在公司使用 Jenkins 集羣,來完成公司日常各項目組持續集成和發佈流程的經驗,給大家分享幾個實用的經驗。

1、 修改 JVM 的內存配置

Jenkins 啓動方式有兩種方式,一種是以 Jdk Jar 方式運行,一種是將 War 包放在 Tomcat 容器下運行。不管何種方式運行,都會存在一個問題就是,默認 JVM 內存分配太少,導致啓動或者運行一段時間後內存溢出報錯 java.lang.OutOfMemoryError: PermGen space。所以,需要在啓動前修改 JVM 內存配置。以 Tomcat 容器方式啓動 Jenkins 爲例配置如下:

# 進入到 Jenkins 運行所在 Tomcat conf 目錄
$ vim catalina.sh
# 在 #JAVA_OPTS="$JAVA_OPTS -Dorg.apache.catalina.security.SecurityListener.UMASK=`umask`" 行下增加修改配置 JVM 內存配置大小,例如下邊配置:
JAVA_OPTS="$JAVA_OPTS -server -Xms512m -Xmx1024m -XX:PermSize=256M -XX:MaxPermSize=512m"
  • 1
  • 2
  • 3
  • 4
  • 5

注意:這裏的幾個 JVM 參數含義如下:

  • -Xms: 使用的最小堆內存大小
  • -Xmx: 使用的最大堆內存大小
  • -XX:PermSize: 內存的永久保存區域大小
  • -XX:MaxPermSize: 最大內存的永久保存區域大小

這幾個參數也不是配置越大越好,具體要根據所在機器實際內存和使用大小配置。

2、修改 Jenkins 主目錄

Linux 下 Jenkins 默認安裝目錄爲 /var/lib/jenkins/,這個目錄磁盤空間有限,長時間使用會導致磁盤空間不夠,建議修改爲其他大磁盤空間目錄。這裏修改安裝目錄有兩種方式,一種是配置爲系統環境變量中,一種是配置到 Tomcat 容器環境變量中。

配置 JENKINS_HOME 到系統環境變量裏面

# 注意:如果一臺機器只安裝一個 Jenkins 時,可以配置如下。
$vim /etc/profile
...
export JENKINS_HOME=/data0/jenkins
export PATH=$PATH:$JENKINS_HOME
# 使配置生效
$ source /etc/profile
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

配置 JENKINS_HOME 到該 Jenkins 啓動的 Tomcat 容器環境變量中

# 注意:如果一臺機器上邊安裝多個 Jenkins 時,不能配置 JENKINS_HOME 到系統環境變量裏面,
# 需要配置 JENKINS_HOME 到該 Jenkins 啓動的 Tomcat 容器配置裏面,這樣可以區分不同的 Jenkins 目錄。
$ vim /data0/scm/apache-tomcat-7.0.85/conf/context.xml
<Context>
    ...
    # 增加以下配置,優先獲取該配置路徑。
    <Environment name="JENKINS_HOME" value="/data0/jenkins" type="java.lang.String"/>
</Context>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

這裏要說明一下,如果一臺機器上只安裝了一個 Jenkins 服務時,可以配置 JENKINS_HOME 到系統環境變量裏面,如果安裝了多個 Jenkins 服務時,不能這麼配置,因爲 Jenkins 會讀取系統環境變量中 JENKINS_HOME 作爲主目錄安裝,那樣會存在配置覆蓋的問題。此時應該採用第二種方式,各自配置 JENKINS_HOME 到自己啓動的 Tomcat 容器環境變量中,Jenkins 會優先讀取該容器環境變量作爲各自的主目錄安裝。

附 Jenkins 尋找 JENKINS_HOME 環境變量的順序爲:首先讀取容器環境變量,如果沒有,則讀取系統環境變量,如果還沒有,則使用默認路徑安裝。

3、配置優化減少磁盤空間佔用

Jenkins 運行 Job 構建比較多時,如果沒有配置好清理策略的話,會導致佔用磁盤空間比較大,最終由於磁盤空間不夠導致構建失敗的問題。

3.1、丟棄舊的構建配置

我們可以在 Job 中配置丟棄舊的構建,通過設置 “保持構建的天數” 和 “保持構建的最大個數” 兩個參數,控制該 Job 最大保存構建數量。
這裏寫圖片描述

這裏寫圖片描述

如上圖所示,我配置了最大保持 3 天之內的構建,如果超過 3 天的構建,則會在Job 執行前被清理掉。同時配置了最大保持構建數量爲 10 個,意思就是如果 3 天內構建次數如果超過 10 次,則最多保留最近執行的 10 個構建。這樣配置的好處,除了能夠自動清理一些 Build 之外,還能夠爲我們代碼執行遠程停止 Job Build 時,縮短停止時間,下邊會講到。

3.2、修改工作空間和構建記錄根目錄

Jenkins 工作主要分爲安裝主目錄,工作空間目錄以及構建記錄目錄,默認配置路徑如下圖所示:
這裏寫圖片描述

如果我們修改了 Jenkins 安裝主目錄之後,因爲工作空間目錄是在安裝主目錄下的 workspace 目錄,構建目錄在安裝主目錄下的 builds 目錄,這樣運行 Job 執行構建比較多時,還會存在磁盤空間不夠的問題,那麼此時可以在 “系統管理” —> “系統配置” —> “高級” 分別修改工作空間根目錄和構建記錄根目錄,指向其他磁盤空間即可。

4、設置全局屬性

適當設置全局屬性,可以避免在 Job 中重複寫一些固定值,例如輸出日誌地址、接口請求地址等等,而且當固定值需要修改時,只需要修改一次即可,不用去每個 Job 裏面修改,方便維護。我們可以去 “系統管理” —> “系統配置” —> “全局屬性” 下增加 Environment variables 鍵值對,例如如下圖:
這裏寫圖片描述

那麼,在 Job 構建時執行 “ Execute Shell” 使用時,可以直接應用即可,例如如下代碼:
這裏寫圖片描述

5、JDK/Maven/Gradle 等軟件多版本安裝

對於一些常用的軟件,比如 Jdk、Maven、Gradle等,可能每個項目對軟件依賴版本不一樣,有的項目依賴 Jdk7,有的依賴 Jdk8,所以爲了更好的適配各個項目,可以指定安裝多個版本軟件,然後 Job 創建時選擇其中一個版本使用。這裏以 Jdk 爲例,去 “系統管理” —> “Global Tool Configuration” —> “JDK” 分別安裝 jdk6、Jdk7、Jdk8。
這裏寫圖片描述

然後,在創建 Job 時,選擇項目需要的一個版本即可。
這裏寫圖片描述

6、設置構建超時時間

有些 Job 在執行構建時,由於某些原因導致構建掛起,耗時比較長,而這些長時間掛起的 Job 會導致 Jenkins 內存佔用比較大,性能下降,嚴重的會直接導致 Jenkins 掛掉。所以,我們需要設置構建超時時間來預防這種事情發生,一旦超過一定的時間,要讓 Job 自動停止掉。例如,這裏我設置構建超過 30 分鐘則將本次 Build 置爲失敗。
這裏寫圖片描述

7、配置視圖分類管理 Job

Jenkins 默認視圖爲 ALL 顯示所有 Job 列表,如果 Job 比較多的話,找某個 Job 會不太方便,雖然有 Search 搜索功能,畢竟還是不太方便。這時候,我們可以通過新建視圖方式,對 Job 進行分門別類,這樣管理和查找起來就方便多啦!例如這裏我新建 “List View" 類型視圖 “wy”,然後選擇該視圖所關聯的 Job 就可以了。
這裏寫圖片描述

這裏寫圖片描述

這樣通過視圖切換,查找相關 Job 就方便多了。

8、配置多節點管理

一般我們會使用 Jenkins Slave 集羣管理來完成日常持續集成操作,使用 Jenkins Slave 一主多從方式,可以將 Job 調度到對應的 Slave 機器上執行,能夠大大提高系統併發執行效率。我們可以從 “系統管理” —> “管理節點” —> “新建節點”,設置節點類型爲 “Permanent Agent” 名稱 “wy_slave0” 的一個從節點,當然有多個節點時,可以創建多個。創建完畢之後,此時插件還屬於不可用狀態,因爲還沒有執行關聯,具體關聯方式可以參照 Jenkins 上節點關聯說明(如下圖,這裏忽略操作),關聯完畢之後,我們就可以在新建 Job 中配置指定那個 Slave 節點運行了。
這裏寫圖片描述

這裏寫圖片描述

這裏寫圖片描述

這裏,有兩種方式指定 Job 在那個 Slave 節點運行,一種是對於自由風格類型的 Job,我們可以通過在 “Restrict where this project can be run” 選項下指定 “Label Expression” 標籤指定節點標籤即可。
這裏寫圖片描述

另一種是對於多配置項目類型的 Job,我們可以通過在 “Configuration Matrix” 先配置 “Slave” 選擇 Node/Label 勾選指定一個或多個 Slave 執行。
這裏寫圖片描述

9、一些實用插件

Jenkins 的基礎配置就能夠滿足我們日常的基本工作,但是爲了提高構建效率和方便維護,Jenkins 上提供了很多實用的插件,使用這些插件,我們可以更加輕鬆、更加簡便、更加高效的執行持續集成和發佈流程。下邊,我就簡單介紹幾個我使用的插件。

9.1、Locale 插件控制 Jenkins 頁面國際化

默認 Jenkins 頁面顯示語言爲英文,對於日常使用語言非英文的用戶來說,就顯得有點操作不便,該插件提供用戶配置 Jenkins 頁面國際化語言,例如中文、法語、泰文等等。首先我們需要去 “Manage Jenkins” —> “Manage Plugins” —> “Available” 中選擇 “Locale” 插件,點擊 “Install without restart”,安裝完不需要重啓。
jenkins-locale
安裝完畢後,可以從 “Manage Jenkins” —> “Configure System” —> “Locale” 下 “Default Language” 輸入框輸入對應的語言英文縮寫碼,例如這裏我想要簡體中文,那麼就需要填寫 zh_CN,並勾選 “Ignore browser preference and force this language to all users” 複選框,來強制所有用戶使用該配置。
jenkins-locale
點擊 “Save”,再次查看頁面,頁面大部分都已經顯示爲對應的中文簡體了,包括各個 Job 配置頁面部分也顯示爲中文簡體了,這下很直觀了。
jenkins-locale
jenkins-locale

9.2、Build Name Setter & Description Setter 插件支持自定義 Build 名稱和描述

Build Name Setter 和 Description Setter 插件可以支持修改每次構建的 Name 名稱和 Description 描述信息,默認 Jenkins 每次構建名稱爲 #ID + 構建時間(ID 自增),且無描述信息。這樣的弊端就是當某次構建出現了錯誤或者要排查某次構建信息,除了按構建時間區間挨個去點開日誌,別無他法,而該插件可以很好的解決該問題。使用該插件,我們可以爲每次構建設置自定義名稱和描述信息,名稱支持從文件中讀取和 macro 模板,描述信息還支持 RegEx 表達式來從構建日誌中提取信息。首先我們需要去 “系統管理” —> “管理插件” —> “可選插件” 中選擇 “Build Name Setter” 和 “Description Setter” 插件,安裝完不需要重啓。
jenkins-build-setter
接下來我們創建一個簡單的示例 Job 來使用一下這兩個插件。新建名稱爲 “wy_tt” 的 Job,接收兩個參數 versionbranch,然後配置 Build Name 爲 # ${BUILD_NUMBER}-${branch},Build Description 爲 構建版本:${version}
jenkins-build-setter
執行幾次構建,每次修改傳遞的參數,此時去 Job 左側構建歷史裏面,就可以看到每次構建的一些重要的輔助信息,這樣將大大方便我們排查問題了。當然,我們還可以更高級的使用該插件,比如從文件中讀取參數,使用表達式從構建日誌中匹配 Key 值,這樣功能將更加強大了。
jenkins-build-setter

9.3、Managed Script 插件管理腳本文件

該插件是爲了在管理文件時創建 Script 腳本文件,然後在 Job 中配置直接使用,方便腳本的統一管理和維護。首先我們需要去 “系統管理” —> “管理插件” —> “可選插件” 中選擇 “Managed script” 插件,安裝重啓即可。
這裏寫圖片描述

安裝完畢後,可以從 “系統管理” —> “Managed files” —> “Add a new Config” 選擇 “Managed script file” 類型,創建一個新的 shell 腳本文件,然後輸入我們要執行的腳本代碼。這裏我創建了兩個腳本,分別爲 before-build-step-shellafter-build-step-shell,意思很明確了,前者在構建前執行的一些操作,後者在構建後執行的一些操作。
這裏寫圖片描述

這裏寫圖片描述

注意: 這裏的腳本可以使用一些 Jenkins 系統的環境變量參數、參數化構建時傳遞的參數以及系統命令哦。

創建完畢後,我們在 Job 中構建處選擇 “Execute managed script” 就可以使用這些腳本了。
這裏寫圖片描述

9.4、PostBuildScript 插件根據 Build 狀態執行腳本

推薦安裝 PostBuildScript 插件,該插件可以在構建後操作中,根據構建結果狀態,執行對應的腳本操作,很實用的一個插件。同上安裝該插件,重啓 Jenkins 完畢插件生效後,Job 中構建後操作處選擇 “Execute Scripts” ,然後在 “Add build step” 中選擇 “Execute shell” 等選項(當然也可以配合上一個插件,那麼這裏就選擇 “Execute managed script”),下邊選擇一個 build 狀態條件值,如果選擇 SUCCESS 狀態,那麼該腳本只有在 Build 成功時纔會執行,其他狀態依次類推,狀態可以多選哦,多選代表多種狀態都能下觸發。
這裏寫圖片描述

9.5、Jenkins2.0 Pipeline 插件執行持續集成發佈流程

Jenkins 2.0 的精髓是 Pipeline as Code,是幫助 Jenkins 實現 CI 到 CD 轉變的重要角色。Pipeline是一套運行於 Jenkins 上的工作流框架,將原本獨立運行於單個或者多個節點的任務連接起來,實現單個任務難以完成的複雜發佈流程。Pipeline 中任何發佈流程都可以表述爲一段 Groovy 腳本,並且 Jenkins 支持從代碼庫直接讀取腳本。使用 Pipeline 執行持續集成發佈流程好處是顯而易見的,它可以把以前需要多個節點上多個 Job 使用一段腳本來替代,而且腳本語言功能強大,可以很好的完成一些複雜的流程操作,推薦大家嘗試使用。這裏就不多說了,詳細可以參考之前文章 初試Jenkins2.0 Pipeline持續集成

9.6、Kubernetes Plugin 插件動態創建 Jenkins Slave

我們知道傳統的 Jenkins Slave 一主多從方式會存在一些痛點,比如 Master 單點故障,Slave 配置環境差異,資源分配不均衡等導致可靠性和可維護性比較差,而使用 Kubernetes Plugin 插件可以動態的創建和刪除 Jenkins Slave 節點,使用它可以很好的保證服務高可用,動態伸縮合理使用資源,以及良好的擴展性。使用該插件後,它的工作流程大致爲:當 Jenkins Master 接受到 Build 請求時,會根據配置的 Label 動態創建一個運行在 Docker Container 中的 Jenkins Slave 並註冊到 Master 上,當運行完 Job 後,這個 Slave 會被註銷並且 Docker Container 也會自動刪除,恢復到最初狀態。這裏也不多說了,詳細可以參考之前文章 初試 Jenkins 使用 Kubernetes Plugin 完成持續構建與發佈

10、JAVA 代碼觸發 Jenkins Job 創建、刪除、停止等操作。

Jenkins Job 創建、刪除、構建等操作,除了在頁面手動操作外,我們還可以通過 Jenkins API 接口執行對應操作,詳細接口可參考 Jenkins
REST API 文檔地址:http://<jenkins_url>/api。這裏我要演示的是使用 Jenkins-client.jar 包,使用 JAVA 代碼操作如何創建、刪除、停止、觸發構建等,使用代碼觸發 jenkins 相關操作,好處就是自己可控,這樣可以配合自己的業務需要,隨時啓動或者新建 Job 啦。


public class JenkinsUtils {
private static String jenkins_url = "http://127.0.0.1/jenkins/";
private static String jenkins_user = "admin";
private static String jenkins_token = "1b356d175432ed0d34c440d68d00fe49";

/**
 * 通過模板創建 Job
 * @param jobName
 * @param jobTemplate
 * @return
 */
public static boolean createJobFromTemplate(String jobName, String jobTemplate) {
    try {
        URI uri = new URI(jenkins_url);            
        JenkinsServer jenkins = new JenkinsServer(uri, jenkins_user, jenkins_token);
        String template = jenkins.getJobXml(jobTemplate);
        Document doc = DocumentHelper.parseText(template);
        String newConfigXml = doc.asXML();
        jenkins.createJob(jobName, newConfigXml, false);
    }catch (Exception e){
        e.printStackTrace();
        return false;
    }
    return true;
}

/**
 * 刪除/禁用 Job
 * @param jobName
 * @return
 */
public static boolean deleteJob(String jobName){
	try {
		URI uri = new URI(jenkins_url);            
        JenkinsServer jenkins = new JenkinsServer(uri, jenkins_user, jenkins_token);
        jenkins.deleteJob(jobName, false);
        jenkins.disableJob(jobName, false);
	} catch (Exception e) {
		e.printStackTrace();
		return false;
	}
	return true;
}

/**
 * 啓動 Job 構建
 * @param jobName
 * @param params
 * @return
 */
public static boolean startJob(String jobName, String params){
	try {
		String buildUrl = jenkins_url + jobName + "/buildWithParameters?" + params;
		HttpUtils.HttpGet(buildUrl);
	} catch (Exception e) {
		e.printStackTrace();
		return false;
	}
	return true;
}

/**
 * 停止正在構建中的 Job,先清除等待隊列中的 build,在停止運行中的 build
 * @param jobName
 * @return
 */
public static boolean stopJob(String jobName){
	try {
		URI uri = new URI(jenkins_url);
        JenkinsServer jenkins = new JenkinsServer(uri, jenkins_user, jenkins_token);
        // 先 kill 掉 queue 裏面的 build
        QueueItem qi = jenkins.getJob(jobName).getQueueItem();
        if(qi != null){
			HttpUtils.HttpPost(jenkinsUrl + "queue/cancelItem?id=" + qi.getId());
        }
        // 在 kill 掉正在運行中的 build
        List&lt;Build&gt; bulidsList = jenkins.getJob(jobName).getAllBuilds();
        for(Build b:bulidsList){
        	if(b.details().isBuilding()){
        		try{
        			b.Stop();
        		}catch(Exception ee){
        			ee.printStackTrace();
        			return false;
        		}
        	}
        }
	} catch(Exception e) {
		e.printStackTrace();
		return false;
	}
	return true;
}

}

  • 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

這裏有一個地方要注意,在停止構建中的 Job 時,這裏是遍歷所有 Build,然後在 Kill 掉運行中的 Build,如果 Build 歷史比較多的時候,會耗時比較久,這將會導致立馬重新執行該 Job Build 時, Build 會被異常 Abort 掉。。。 也嘗試過獲取最後一次 Build 執行 Stop 操作,好像也不太好使。所以這裏大家可以通過上邊 3.1、丟棄舊的構建配置 中的操作,減少構建歷史記錄,這樣就可以很快執行完畢,就不會出現上述問題了。

                                </div>
            <link href="https://csdnimg.cn/release/phoenix/mdeditor/markdown_views-b6c3c6d139.css" rel="stylesheet">
                                            <div class="more-toolbox">
            <div class="left-toolbox">
                <ul class="toolbox-list">
                    
                    <li class="tool-item tool-active is-like "><a href="javascript:;"><svg class="icon" aria-hidden="true">
                        <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#csdnc-thumbsup"></use>
                    </svg><span class="name">點贊</span>
                    <span class="count">7</span>
                    </a></li>
                    <li class="tool-item tool-active is-collection "><a href="javascript:;" data-report-click="{&quot;mod&quot;:&quot;popu_824&quot;}"><svg class="icon" aria-hidden="true">
                        <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#icon-csdnc-Collection-G"></use>
                    </svg><span class="name">收藏</span></a></li>
                    <li class="tool-item tool-active is-share"><a href="javascript:;" data-report-click="{&quot;mod&quot;:&quot;1582594662_002&quot;}"><svg class="icon" aria-hidden="true">
                        <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#icon-csdnc-fenxiang"></use>
                    </svg>分享</a></li>
                    <!--打賞開始-->
                                            <!--打賞結束-->
                                            <li class="tool-item tool-more">
                        <a>
                        <svg t="1575545411852" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5717" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M179.176 499.222m-113.245 0a113.245 113.245 0 1 0 226.49 0 113.245 113.245 0 1 0-226.49 0Z" p-id="5718"></path><path d="M509.684 499.222m-113.245 0a113.245 113.245 0 1 0 226.49 0 113.245 113.245 0 1 0-226.49 0Z" p-id="5719"></path><path d="M846.175 499.222m-113.245 0a113.245 113.245 0 1 0 226.49 0 113.245 113.245 0 1 0-226.49 0Z" p-id="5720"></path></svg>
                        </a>
                        <ul class="more-box">
                            <li class="item"><a class="article-report">文章舉報</a></li>
                        </ul>
                    </li>
                                        </ul>
            </div>
                        </div>
        <div class="person-messagebox">
            <div class="left-message"><a href="https://blog.csdn.net/aixiaoyang168">
                <img src="https://profile.csdnimg.cn/5/0/8/3_aixiaoyang168" class="avatar_pic" username="aixiaoyang168">
                                        <img src="https://g.csdnimg.cn/static/user-reg-year/1x/9.png" class="user-years">
                                </a></div>
            <div class="middle-message">
                                    <div class="title"><span class="tit"><a href="https://blog.csdn.net/aixiaoyang168" data-report-click="{&quot;mod&quot;:&quot;popu_379&quot;}" target="_blank">哎_小羊_168</a></span>
                                        </div>
                <div class="text"><span>發佈了76 篇原創文章</span> · <span>獲贊 269</span> · <span>訪問量 77萬+</span></div>
            </div>
                            <div class="right-message">
                                        <a href="https://bbs.csdn.net/topics/395528954" target="_blank" class="btn btn-sm btn-red-hollow bt-button personal-messageboard">他的留言板
                    </a>
                                                        <a class="btn btn-sm  bt-button personal-watch" data-report-click="{&quot;mod&quot;:&quot;popu_379&quot;}">關注</a>
                                </div>
                        </div>
                </div>
</article>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章