# 讓開發自動化: 持續集成反模式
_通過避免反模式輕松實現持續集成_
盡管持續集成(Continuous Integration,CI)可以非常有效地減少項目的風險,但是它對與編程相關的日常活動提出了很高的要求。在這一期 [_讓開發自動化_](http://www.ibm.com/developerworks/cn/java/j-ap/)中,自動化專家和 [_Continuous Integration: Improving Software Quality and Reducing Risk_](http://www.amazon.com/gp/product/0321336380/?tag=integratecom-20)的作者之一 Paul Duvall 列舉了一系列 CI 反模式并解釋了如何避免它們。
在我的職業生涯中經常發現,通過了解在特定情況下 _不應該_做什么,可以學到更多知識。例如,在我職業生涯的早期,由于需要快速發布軟件,我省略了單元測試,因為我認為不值得做這些工作。幸運的是,我已經學到 _絕不應該_將未經測試的代碼投入生產;因此開始堅持編寫單元測試。
整個 IT 行業似乎都主要采用這種學習方式;實際上,我們甚至專門創建了 _反模式(anti-pattern)_這個詞,表示在特定環境中不應該采用的做法。反模式是看起來似乎有好處,但是最終可能產生嚴重影響的解決方案。
## 看似真實的假象
遺憾的是,我發現當缺少經驗的團隊試圖采用 CI 時,他們很可能錯誤地采用許多反模式,這最終導致他們不但沒有獲得預期的好處,反而遇到一大堆麻煩。不幸的是,在這種情況下,團隊常常將麻煩歸罪于 CI 本身。因此,我常常聽到 “CI 不適合大項目” 或 “我們的項目 _太特殊_,不適合采用 CI” 這樣的說法,實際上 CI 根本不是問題的原因 —是某些做法的不恰當應用或者缺少某些方法導致了這些麻煩。
## 關于本系列
作為開發人員,我們的工作就是為終端用戶實現過程自動化;然而,很多開發人員卻忽略了將自己的開發過程自動化的機會。為此,我編寫了 [_讓開發自動化_](http://www.ibm.com/developerworks/cn/java/j-ap/)這個系列的文章,專門探討軟件開發過程自動化的實際應用,并教您 _何時_以及 _如何_成功地應用自動化。
在本文中,我要描述與 CI 相關的六個反模式:
* **簽入不夠頻繁**,這會導致集成被延遲
* **破碎的構建**,這使團隊無法轉而執行其他任務
* **反饋太少**,這使開發人員無法采取糾正措施
* 接收 **垃圾反饋**,這使開發人員忽視反饋消息
* 所擁有的 **機器緩慢**,這導致延遲反饋
* 依賴于 **膨脹的構建**,這會降低反饋速度
如果您采用 CI 的時間足夠長,那么幾乎肯定體驗過這些反模式的效果。這沒關系,但是如果它們發生得太頻繁,就會大大限制 CI 的好處。因此,如果您希望避免這些反模式并控制它們的負面影響,那么本文正適合您。
* * *
## 由于簽入不夠頻繁導致的延遲集成
**名稱:**簽入不夠頻繁
**反模式:**由于所需的修改太多,源代碼長時間簽出存儲庫。
**解決方案:**頻繁地提交比較小的代碼塊。
實施 CI 的前提是團隊可以快速獲得關于當前開發的代碼的反饋;而且,與傳統的集成相比,這種頻繁的軟件集成風格會減少集成花費的時間(和麻煩)。但是,有效的 CI 假設修改會頻繁地發生(所以可以頻繁地執行構建!)。如果代碼長期留在開發人員的桌面(而不是存儲庫)中,那么就會出現糟糕的情況,因為在系統的不同部分中會出現其他修改。
## 每天提交一次,成功實現集成
一條常用的經驗規則是 _至少_每天簽入一次代碼。我使用一種有效的技術:如果我覺得需要把工作停一下,就會先看看目前是否可以運行本地構建,然后提交代碼。然后我才會暫停工作。
從本質上說,如果不頻繁地提交修改,集成就會延遲;延遲越長,消除其嚴重影響就越困難(比如其他人的修改可能會影響您的代碼)。對于使用 CI 的項目,我建議開發人員 _至少_每天簽入一次代碼,但是我相信最好是每天簽入多次。
### 任務越小,工作越輕松
我常常聽到一些開發人員抱怨說,他們要忙于修改那么多文件,哪有精力每天簽入代碼。實際上,這正是我要說的要點 —為了每天提交源代碼修改,需要將任務劃分得更小。實際上,需要將編程任務劃分成小塊,這樣修改也會更小。
不要在一個大任務中實現一個業務對象上的所有特性,例如編寫 `read()`、`write()`、`update()`和 `delete()`方法的原型;而是應該首先編寫 `read()`方法(以及對應的測試),然后簽入這個類,從而與整個代碼基集成。接下來,可以實現另一個方法,再次執行簽入,直到完成整個任務。這樣的話,就可以讓 CI 的好處最大化,而且會讓您確信自己的代碼可以與 _別人的代碼_相互配合。
請記住,即使您和您的團隊正確地執行許多 CI 實踐,如果團隊成員不堅持至少每天簽入一次源代碼修改,那么 CI 的好處會大打折扣。這常常會讓人誤以為 CI 是無效的,這種想法實在大錯特錯。
* * *
## 破碎的構建減慢了開發的節奏
**名稱:**破碎的構建
**反模式:**構建長時間破碎,導致開發人員無法簽出可運行的代碼。
**解決方案:**在構建破碎時立即通知開發人員,并以最高優先級盡快修復破碎的構建。
無論您信不信,構建破碎的時間越長,就越麻煩。這是因為文件、修改和依賴項越多,隔離缺陷就越困難。因此,當通知某人構建破碎時(通過電子郵件、RSS 或其他機制),他應該優先解決這個問題;否則,構建破碎的時間越長(尤其是對于頻繁修改代碼的團隊),就越難糾正。
破碎的構建不總是壞事。實際上,破碎的構建會讓您迅速地意識到軟件出了問題。當構建 _頻繁地_破碎或長時間破碎時,破碎的構建就會成為問題。在構建破碎的情況下,絕不應該置之不理。
## 不存在永不破碎的構建
我曾經聽到一些開發人員說,“永遠不要出現破碎的構建!” 這是個糟糕的建議。我們希望避免許多常見的構建錯誤,比如缺少文件或破碎的測試;但是,不破碎的構建也可能反映一些問題。構建可能只做少量工作(可能只是執行一個編譯和幾個單元測試)。我稱之為 “持續忽視(Continuous Ignorance)”,這種情況有時候比頻繁的破碎構建更糟糕。
### 用私有構建減少破碎的構建
防止破碎構建的有效技術之一是,在將代碼提交到存儲庫之前,運行 _私有構建(private build)_。執行私有構建的步驟如下:
1. 從存儲庫簽出代碼。
2. 在本地修改代碼。
3. 用存儲庫執行更新,從而集成其他開發人員所做的修改。
4. 運行本地構建。
5. 構建成功之后,將修改提交到存儲庫。
圖 1 說明了這種做法。注意,這個工作流強調頻繁地與存儲庫執行同步,從而保證定期簽入并限制破碎的構建 —這真是一舉兩得!
##### 圖 1\. 運行私有構建減少破碎的集成構建

通過在簽入源代碼之前執行私有構建(當然是頻繁地執行),可以避免許多導致構建破碎的典型錯誤;因此,可以節省時間和減少麻煩。
* * *
## 由于反饋太少,無法采取糾正措施
**名稱:**反饋太少
**反模式:**團隊沒有把構建狀態通知發送給團隊成員;因此,開發人員不知道構建已失敗。
**解決方案:**使用各種反饋機制傳播構建狀態信息。
在設置 CI 系統時,團隊常常認為接收電子郵件是浪費時間;因此,他們決定不發布通知。但是,如果沒有對構建的反饋,就無法采取糾正措施。實際上,反饋是 CI 最重要的方面之一;因此,反饋是否 _有效_也非常關鍵。
如果希望擴展將構建狀態信息發布給團隊成員的機制,那么使用視覺和聲音設備可能很有幫助,對于集中工作的團隊尤其如此。Ambient Orb 這樣的設備可以幾乎實時地反映構建的狀態。例如,當構建失敗時,orb 可以顯示紅色;當構建通過時,orb 顯示綠色。另外,orb 還可以傳播其他信息,例如通過改變顏色表示代碼庫的復雜性是增長還是下降(比如綠色表示良好,黃色表示糟糕)。
### 發揮創造力
設置 Ambient Orb 非常容易。清單 1 演示如何使用 Quality Lab 的開放源碼軟件 `OrbTask`在 Ant 中設置 Ambient Orb:
##### 清單 1\. 使用 Ambient Orb Ant 任務
```
<target name="notifyOrb" >
<taskdef classname="org.qualitylabs.ambientorb.ant.OrbTask"
name="orb" classpathref="orb.class.path"/>
<orb query="http://myambient.com:8080/java/my_devices/submitdata.jsp"
deviceId="AAA-9A9-AA9"
colorPass="green"
colorFail="red"
commentFail="Code+Duplication+Threshold+Exceeded" />
</target>
```
清單 1 中的任務對于通過狀態將 orb 改為綠色,對于失敗狀態顯示紅色。圖 2 顯示綠色的 orb,這表示最近的構建狀態是成功:
##### 圖 2\. 成功的構建!

團隊可以創造性地使用各種反饋機制,讓團隊成員不會忽視構建狀態消息。另外,這些技術也會讓 CI 變得生動有趣,讓人們更容易注意到需要采取措施的問題。
其他通知機制包括:
* RSS feed
* 任務欄監視器,比如 CCTray(用于 CruiseControl)
* X10 設備(比如 LavaLamps)
* 通過 Jabber 等發送的即時消息
* SMS(Text Messages)
警告:需要在信息過多和信息過少之間找到一個平衡點。反饋機制應該隨著工作環境定期調整。例如,對于集中工作的團隊,聲音提示可能是有效的(比如在構建失敗時發出火災警報);但是,其他團隊可能更喜歡 Ambient Orb(它不會在您陷入沉思時嚇著您)。
* * *
## 垃圾反饋
**名稱:**垃圾反饋
**反模式:**團隊成員很快被構建狀態消息淹沒(成功、失敗或界于這兩者之間的各種消息),所以開始忽視這些消息。
**解決方案:**反饋要目標明確,使人們不會收到無關的信息。
與 “反饋太少” 反模式相反,我常常發現團隊天真地認為,當 CI 服務器做任何事情時,_每個人_都應該接到反饋(比如電子郵件)。信息一旦泛濫,人們就會忽視它們;如果反饋太多,團隊很快就會將 CI 反饋看做垃圾。所以,當發生真正嚴重的問題(比如構建真的破碎了)時,可能無法引起注意。
### 通過精確地確定反饋的目標,盡可能減少垃圾反饋
清單 2 給出一個 CruiseControl 配置文件示例,演示如何有效地使用電子郵件通知。在這個示例中,無論構建是成功還是失敗,技術主管都會收到電子郵件,項目經理只在構建失敗時收到電子郵件,最近向存儲庫提交源代碼修改的開發人員也會收到通知。
##### 清單 2\. 使用 CruiseControl 發送電子郵件通知
```
<project name="brewery">
...
<publishers>
<htmlemail
css="./webapps/cruisecontrol/css/cruisecontrol.css"
mailhost="localhost"
xsldir="./webapps/cruisecontrol/xsl"
returnaddress="cruisecontrol@localhost"
buildresultsurl="http://localhost:8080"
mailport="25"
defaultsuffix="@localhost" spamwhilebroken="false">
<always address="techlead@localhost"/>
<failure address="pm@localhost" reportWhenFixed="true"/>
</htmlemail>
</publishers>
...
```
反饋是 CI 系統最重要的方面之一,值得好好討論一下。_反饋太少_和 _垃圾反饋_是兩個極端,要在它們之間找到一個適當的平衡點。當構建破碎時,必須及時地將反饋發送給適當的人,而且必須提供采取糾正措施所需的信息。如果構建是成功的,那么應該只向少數人發送反饋,包括最近提交修改的開發人員以及希望掌握最新情況的技術領導。如果不加區分地把所有狀態消息發送給所有人,肯定會大大損害 CI 過程的效果。
* * *
## 不要讓緩慢的機器導致反饋延遲
**名稱:**緩慢的機器
**反模式:**用一臺資源有限的工作站執行構建,導致構建時間太長。
**解決方案:**增加構建機器的磁盤速度、處理器和 RAM 資源,從而提高構建速度。
幾年前,我參與了一個相當大的項目,它有超過一百萬行代碼,編譯需要花兩個小時以上。當我們試圖更頻繁地執行集成時,等待持續管理團隊執行集成的時間越來越長,讓人很不耐煩。當然,兩個小時其實是最好的情況,因為構建常常失敗,所以這個過程常常要花幾 _天_(真是痛苦啊!)。這種情況持續幾周之后,解決方案就非常明確了:必須購買一臺更強大的機器,它的磁盤空間必須足以容納所有要簽出的文件和構建生成的文件,它的處理器速度更快,可以處理許多指令,RAM 數量要足以運行測試和其他需要大量內存的進程。
### 您覺得需要提高速度嗎?
有了這臺強大的機器,我們將構建時間從兩個小時降低到了 30 分鐘;所以,通過花費額外資金購買最新的機器,我們節省了大量時間和金錢,而且最終能夠更快地集成軟件(這意味著更快地發現問題!)。
用更強大的工作站執行集成構建總是沒錯的;但是,這個故事的要旨在于,如果發現構建機器在速度、內存或硬盤方面無法讓人滿意,就應該認真考慮進行升級;加快構建可以幫助我們更快地獲得反饋,快速糾正問題,更快地轉到下一個開發任務。
* * *
## 膨脹的構建導致反饋延遲
**名稱:**膨脹的構建
**反模式:**把太多的任務添加到提交構建過程中,比如運行各種自動檢查工具或運行負載測試,從而導致反饋被延遲。
**解決方案:**一個構建 _管道(pipeline)_可以運行不同類型的構建。
一些開發團隊喜歡把能添加的所有過程都添加到自動構建中,但是他們忘了執行這些操作是要花 _時間_的。還記得嗎?我遇到的那個項目的編譯要花兩個小時。想像一下,如果把執行測試添加到構建過程中,會怎么樣呢?對于一百萬行代碼,您認為運行靜態分析工具要花多長時間?如果您認為耗時八小時的構建過程是難以令人置信的,那么再想想吧。我經常遇到這種情況。
為了向團隊成員提供更多的構建信息,團隊往往會逐漸增加構建過程的內容。要向開發團隊提供快速反饋,還要從 CI 構建過程提供有用的信息,必須在這兩個目標之間取得平衡。
### 通過構建管道提高效率
如果發現構建過程耗時太長,而且已經實現了其他改進技術(比如改用更快的機器)并優化了測試執行時間,那么就有必要考慮創建所謂的 _構建管道(build pipeline)_。構建管道的用途是異步地執行長時間運行的過程,這樣的話,開發人員簽入代碼之后,不需要長時間等待反饋。
例如,如果執行一個構建過程要花 10 分鐘以上,那么可以創建一個構建管道,在某人將代碼提交到存儲庫之后,它會運行一個初步的輕型構建。這個 “提交” 構建由編譯和運行快速單元測試等輕型過程組成。如果這個初步構建成功了,就可以運行第二個構建,它執行長時間運行的測試、軟件檢查,甚至包括部署到應用服務器上。
例如,在清單 3 中,讓 CruiseControl 檢查存儲庫中的修改。當發現修改時,CruiseControl 運行一個所謂的 _委派(delegating)_構建,它調用項目的主構建文件(在使用 Ant 時是 build.xml)。但是,特殊之處是 CruiseControl 執行另一個目標,這個目標執行一些輕型過程,比如編譯和細粒度的單元測試。
##### 清單 3\. 檢查修改的 CruiseControl 配置
```
<project name="brewery-commit">
...
<modificationset quietperiod="120">
<svn RepositoryLocation="http://brewery-ci.googlecode.com/svn/trunk"/>
</modificationset
...
```
在清單 4 中,CruiseControl 檢查對 `brewery-commit`項目的修改(這個項目不在存儲庫中 —它實際上查看一個日志文件)。當發現修改時,CruiseControl 運行另一個委派構建。這個構建調用相同的構建文件,但是執行另一個目標。這個目標可能執行長時間運行的過程,比如功能測試、軟件檢查等等。
##### 清單 4\. 執行長時間運行的構建的 CruiseControl 配置
```
<project name="brewery-secondary">
...
<modificationset quietperiod="120">
<buildstatus logdir="logs/brewery-commit"/>
</modificationset>
...
```
_膨脹的構建_反模式是導致 CI 無法實施的最常見原因。但是,正如您看到的,使用構建管道可以避免這種情況。有效的構建管道應該充分利用 “80/20” 規則:百分之 20 的構建時間花費在導致百分之 80 的構建錯誤(比如缺少文件、破碎的編譯和測試失敗)的部分上。完成這個過程之后,開發人員接到反饋,然后運行第二個構建過程,這個過程的運行時間比較長,但是只產生百分之 20 的構建錯誤。
* * *
## 反模式是可以糾正的
CI 反模式會妨礙團隊從持續集成實踐中獲得最大的收益;但是,本文描述的技術有助于限制這些反模式發生的頻率。這些技術包括:
* 經常提交代碼,可以防止集成變得復雜。
* 在提交源代碼之前運行私有構建,可以避免許多破碎的構建。
* 使用各種反饋機制避免開發人員忽視構建狀態信息。
* 有針對性地向可以采取措施的人發送反饋,這是將構建問題通知團隊成員的好方法。
* 花費額外資金購買更強大的構建機器,從而加快向團隊成員提供反饋的速度。
* 創建構建管道來緩解構建膨脹。
本文描述了我最常遇到的一些反模式,但是還有其他反模式,包括:
* **持續忽視(Continuous Ignorance)**,也就是構建過程只包含很少的過程,導致構建總是成功。
* 構建 **只在您的機器上執行**,這會延長引入缺陷和糾正缺陷之間的時間。
* **瓶頸提交(Bottleneck Commits)**,這會導致破碎的構建,讓團隊成員無法回家。
* 運行 **間歇構建(intermittent build)**,這使反饋延遲。
明年我會討論其他影響持續集成效果的 CI 反模式,請保持關注。
- 讓開發自動化
- 讓開發自動化: 部署自動化模式,第 2 部分
- 讓開發自動化: 部署自動化模式,第 1 部分
- 讓開發自動化: 使用基于向導的安裝程序
- 讓開發自動化: 針對廣大開發人員的并行開發
- 讓開發自動化: 實現自動化數據庫遷移
- 讓開發自動化: 持續重構
- 讓開發自動化: 文檔化一鍵通
- 讓開發自動化: 利用 Ivy 管理依賴項
- 讓開發自動化: 自動負載測試
- 讓開發自動化: 使用自動化加速部署
- 讓開發自動化: 持續集成反模式
- 讓開發自動化: 斷言架構可靠性
- 讓開發自動化: 持續測試
- 讓開發自動化: 用 Eclipse 插件提高代碼質量
- 讓開發自動化: 除掉構建腳本中的氣味
- 讓開發自動化: 選擇持續集成服務器
- 讓開發自動化: 持續檢查