持續性整合

Continuous Integration


原作者

Martin Fowler
Chief Scientist, ThoughtWorks

Matthew Foemmel
ThoughtWorks

翻譯

Areca Chen 2008/08/23

任何軟體開發程序中有一個很重要的部分就是增進軟體的可信度(reliable)。既使這是很重要的,我們一直感到很訝異其一直未被落實。本文中我們討論的程序是Matt在ThoughtWorks中 一個主要的專案中實施的,這個程序是逐步的使用於整個公司當中。這個程序強調完全的自動及可再生的(reproducible)建構, 這個程序包括測試,這些測試每天執行許多次。這種方式允許每一個開發者每天整合以便降低整合的問題。譯註1

ThoughtWorks已開放CruiseControl軟體的程式碼;這個軟體是作為自動持續性整合之用。我們也提供使用CruiseControl、Ant、及讓C.I.執行的顧問諮詢。你可聯繫Josh Mackenzie以獲得更多的資訊。

Japanese Translation

軟體開發充滿許多最佳的實務作法;這些實務作法一直被談論著但似乎鮮被落實。其中最基本最有價值的是完全自動建構及測試的程序;這個程序允許一個團隊每天建構及測試許多次。這個每日建構的概念已被廣泛的談論。McConnnell推薦這是最佳的實務作法而且長久以來大家都知道的 ;而且已是微軟的開發程序的特性。我們同意XP社群所說的每日建構是最低的限度。一個完全自動化的程序允許你一天之內建構許多次是可以達成的而且是值得努力的。

我們使用的術語『持續性整合』,是XP(Extreme Programming)實務作法的術語之一。不管如何我們認知到這個實務作法長久以來已經廣為週知了並且被許多人使用;而這些人從未考慮使用XP於其工作上。我們曾在我們的軟體開發過程中使用XP作為試金石(touchstone);而且XP影響了許多我們的術語(terminology)及實務作法。不管如何你可以使用持續性整合而無須使用XP其他的部分--確實我們認為持續性整合是任何軟體開發活動 內涵中最基本的部分。

這裡有許多達成每日建構自動化的作法。

所有這些作法需要相當數量的訓練(discipline)。我們發現這需要耗費許多精力導入一個專案之中。我們也發現一旦成功導入要維持只需一點點努力就可以達成。

持續性整合的益處

表達持續性整合最困難的事之一是根本的切換到整體開發樣式(whole development pattern),如果你從未在這種環境中實踐過則不是那麼容易瞭解。事實上多數人確實經歷這種氣氛;只要他曾單獨開發過--因為他只與他自己整合。對於大多數人 而言團隊開發只是加上一些特定問題;這個問題是範圍(territory)的一部份。持續性整合降低這些問題,所要的只是一些訓練的特定努力。

持續性整合基本上的益處是排除人們花時間在找尋臭蟲的會議上;其中一個人的工作需要跟隨其他人的腳步;然而卻沒有一個人瞭解到底發生了甚麼事。這些臭蟲很難尋找因為問題不只是屬於一個人的範圍, 而是介於兩個部分之間的互動。這個問題隨著時間惡化。一般整合產生的臭蟲在顯露之前可能已是幾週或幾個月,因此要找尋更需要花費許多功夫。

使用持續性整合大部分這類的臭蟲在其產生的同一天便會被發現。甚至;至多在互動中途便可立即的顯露。如此可以大大的降低搜尋臭蟲的範圍。而且如果你無法找到臭蟲,至少你可以避免把不當的程式碼放到產品中,因此在最差的狀況下你不會把有臭蟲的功能加入。(當然你可能想要功能甚於你仇恨臭蟲,但至少這種方式是一種有依據的(informed)選擇。)

現在並不保證你可以找到所有整合性的臭蟲。這個技術需要依賴測試,而且我們都知道測試並不證實沒有錯誤。關鍵點是持續性整合抓到的臭蟲量是物超所值的。

最終的結果是經由減少花費時間於整合性臭蟲而提升生產力。既使我們不知道有任何人於此提出任何科學性研究的證據是很強烈的。持續性整合可以大幅降低耗費於整合困境的所有時間,事實上它可以把 改變困境。

愈頻繁愈好

在持續性整合的核心有一個根本的非直覺效應。那就是整合愈頻繁愈好。對於那些實踐這個原則的人這個效應是平凡無奇的,但對於那些非實踐者這個效應在直覺的經驗上似乎是矛盾的。

如果你只是偶而整合,譬如一天不到一次,那麼整合是一種痛苦的經驗,那是耗費時間及精力的。確實那是十分棘手且是你最不想做的事:便是更頻繁的去做整合。我們最常聽到的抱怨是「這麼大的一個專案,你無法實踐每日建構。」

然而還是有專案這麼做。在一個50人實作20萬行程式碼的專案中我們每天整合好幾十次。微軟在幾千萬行程式碼的專案中實踐每日建構。

每日建構可行的原因在於『整合的投入』是與『介於整合之間的總時間』成指數比例關係的。雖然我們並不知道有任何這類的衡量,這表示每週整合一次並不代表是每日整合一次的五倍時間,而可能是25倍的時間。因此如果你的整合是痛苦的,你應該把這個痛苦歸因於你未能更頻繁的整合。你最好還是頻繁整合,愈頻繁的整合應該是輕鬆的而其結果是你花費愈少的時間執行整合。

頻繁整合的關鍵之一是自動化。多數的整合是可以自動化,而且應該是自動化。把程式碼、編譯、鍊結及有意義的測試都可以自動化。最後剩餘你所需要做的只是決定建構是否完成:是或否。如果是你可以不用管它,若否你應該能夠簡單的回復原狀並且確認最終的版本是可以運作的。對於可以完成的建構你不用思考太多。

使用這樣的一種自動化建構,你可以隨你喜歡頻繁的建構多少次都沒關係。唯一的限制是建構所需的總時間。

甚麼是稱得上成功的建構

有一件重要的事要決定的就是:甚麼因素可以使得建構成功。那似乎是很明顯的,但值得注意的是這有可能是變得模糊的,Martin曾經審核過一個專案。他問道這個專案是否每日建構而回答是肯定的。很幸運的;Ron Jeffries更進一步刺探。他問道「當建構發生錯誤時你們做甚麼事?」回答是「我們寄出一封eamil給相關人員。」事實上這個專案有幾個月都未成功的建構。那不是每日建構,那只是嘗試(attempt)每日建構。

我們對於所謂的成功的建構定義非常執著。

多數人認為編譯及鍊結就是一個建構。而我認為一個建構至少包括啟動應用程式並執行一些簡單的測試(McConnnell使用冒煙測試(smoke test)這個名詞:啟動它看看是否會冒煙)。更徹底的執行一組測試可以大大的提升持續性整合的價值,因此我們也比較偏好這麼做。

單一原始碼觀點  Single Source Point

為使整合更容易,任何開發者需要能夠很容易的得到目前最完整的原始碼。沒有比讓開發者到處向別的開發者要求最後開發的一部份然後複製到自己的機器更糟糕的事,你最好指定把最新的版本放在定點,目前最終的版本是你開發新的程式碼的一個起點。

這個準則非常簡單。任何人都可以使用一台乾淨的電腦,連接到網路,並使用一個指令下載所有開發所需的原始檔案。

這個顯而易見(我們希望)的解決方式是使用一個環境管理(configuration management)(原始碼控制(cource control))系統作為所有原始碼的來源。環境管理系統一般設計使用於網路並提供一些工具讓人們很容易的獲得資源。更進一步環境管理系統也包括版本管理以便你可以很容易找到以前的版本。成本 應該不是問題;像CVS是一個不錯的開放原始碼(open-source)的環境管理工具。

為達成目的所有的原始檔案都應該由環境管理系統保存。所謂「所有的」往往是超過人們的想像。這包括建構腳本(scripts)、屬性檔案(properties files)、資料庫綱要(schema)DDLs、安裝腳本(install scripts)、及在一個乾淨的機器中建構所需的任何東西。同樣的我們往往看到原始碼控制,但沒有其他一些必須提供的重要檔案。

嘗試讓所有的事物都放在環境管理系統的單一資源樹狀結構中。有些人在環境管理系統中個別的專案使用不同的元件。麻煩的是人們必須記住哪一些元件的哪些版本與其他元件的哪些版本合作。在某些情況下你必須區隔程式碼,不管如何這種情況是比你想像的還少。你可以從一個單一 原始碼樹狀結構建構多樣的元件,這樣的議題應該由建構腳本控制,而不是透過儲存結構控制。

自動建構腳本

如果你正在寫一個簡單的程式只有十幾個檔案,建構應用程式可能只是使用單一指令編譯:javac *.java。大的專案需要更多。此時你在許多目錄中有許多檔案。你必須確保產出的目標碼是存放在適當地方。同樣的編譯還需要鍊結的步驟。你有些程式碼是從其他的檔案產生;在編譯前這些程式碼需要是已經準備好的。測試需要自動的執行。

一個大的建構往往耗費時間,如果你只是做了一個小小的改變;你不會希望執行所有的步驟。因此一個好的建構工具可以分析哪些是需要改變以作為此程序的一部份。通常的方式是檢查 原始碼及目標檔的日期而只編譯程式碼的日期是最近的。相依度高是最難纏的:如果一個目標檔改變相依的檔案也需要重新建構。編譯器可能可以處理這類情況也可能不行。

依據你的需要,你可能需要不同種類的事物要去建構。你可以建構一個系統包含或不包含測試,或包含不同的測試組。有些元件可以獨立的建構。一個建構腳本應該允許你依據不同的案例建構各種替代的對象。

只要你一個簡單的指令列,腳本便可以處理。這些可能是腳本的介面工具(shell)或使用更精緻的腳本語言如Perl或python。但很快的設計一個環境來處理這種事情便可以實現了, 像在Unix中製作此類的工具。

在我們的Java開發環境我們很快的發現我們需要一個更嚴謹的解決方案。Matt確實花了一些時間開發一個建構工具稱為Jinx;那是為Enterprise Java工作所設計的。不管如何目前我們已切換到開放程式碼建構工具Ant。Ant的設計非常類似Jinx,Ant允許我們編譯Java檔案並且包裹成Jars。Ant同時讓我們方便的擴充Ant以便讓我們在建構當中執行其他的任務。

我們有許多人使用IDEs,而大多數IDEs之中有許多各式的建構管理程序。不管如何這些檔案總是IDE私有的而且是脆弱的。更甚者它們需要IDE才可以工作。IDE使用者設定他們自己 的專案檔案並在個別的開發中使用。不管如何我們信賴Ant作為主要建構器而且主要的建構是在伺服器上使用Ant。

自我測試程式碼

只是讓程式編譯是不夠的。當編譯器是屬於強型別語言(strongly typed language)可以指出許多問題,既使是成功編譯還是有太多的錯誤無法攔截。要追蹤這些錯誤我們特別強調自動測試的紀律--這是XP提倡的另一個實務作法。

XP區分測試成兩個部分:單元測試及驗收(acceptance)測試(也稱為功能(functional)測試)。單元測試是由開發者撰寫而且一般是測試個別的類別或一小群的類別。驗收測試一般由客戶或外部測試團隊(在開發者的幫助下)並且從頭到尾測試整個系統。我們採用這兩種測試,並且讓這兩種測試盡可能的自動化。

我們執行一個測試系列(suite)稱為『BVT(建構查核測試Build Verification Tests)』。在BVT中所有的測試必須依序通過;我們才能確認建構成功。所有XP風格的單元測試都包含在BVT中。因為本文是討論建構程序,我們主要討論的是BVT,請記住有第二層的測試包含於BVT,因此別單獨的判斷BVT整體的測試及品保效能。確實我們的品保團隊不會看到程式碼除非通過BVT;因為他們只是處理工作中的建構。

單元測試的基本原理是當開發者撰寫程式碼的同時也撰寫這些程式碼的測試。當他們完成一個任務,不只是檢查產出的程式碼,他們也檢查測試的結果。這些都是密切的遵循XP使用先寫測試的撰寫程式風格:除非有一個失敗的測試 否則不要寫新的程式碼。因此如果你要在系統中加入新功能,你先寫一個測試;這個測試只有在此功能存在時才能通過,然後你的工作就是讓這個測試通過。

我們以Java撰寫測試,Java也是我們開發所使用的語言。這樣使得撰寫程式及測試都是使用相同的語言。我們使用JUnit作為組織及撰寫測是的框架。JUnit是一個簡單的框架可以讓你快速撰寫測試,把測試組織成測試系列(包),並且使用對話方式或批次模式執行測試系列。(JUnit是Java之xUnit家族的一員--幾乎每一種語言都有其版本。)

當開發者撰寫程式時一般在編譯時執行某部分的測試。這實際上加快開發工作的速度;因為測試可以幫助找尋程式碼中任何邏輯錯誤。此外;不用透過除蟲的程序;當你你最後執行測試便可以看到改變。這些改變應該很小同時很容易找到臭蟲。

並不是每一個人都堅守XP先寫測試的風格,但從寫測試所獲得的利益也是相同的。不但讓個別的任務更快,同時建構BVT讓測試更能攔截錯誤。因為BVT每天執行許多次,這表示BVT所鎖定的問題更容易浮現;其理由是:我們可以檢視少量被改變的程式碼以便發掘臭蟲。只在被改變的程式碼中除蟲是比在整個程式碼中除蟲更有效率的。

當然你不能期望測試找到所有的錯誤。因此有人說:測試並不保證絕對沒有臭蟲。不管如何盡善盡美不是你從好的BVT獲得回報的唯一觀點。有瑕疵的測試;頻繁的執行,是比期盼完美的測試卻從不去寫測試來的好。

有一個相關的問題就是開發者在自己的程式碼撰寫測試的議題。有人認為不該自己測試自己的程式碼,因為太容易忽略自己本身工作的錯誤。雖然這是事實,自我測試(self-testing)的程序需要迅速的 轉換(turn-around)測試使之成為程式碼的基底。快速轉換的價值大於分開的測試。因此對於BVTs我們依賴開發者撰寫測試,但還是有分開的驗收測試不是由開發者撰寫的。

自我測試另一個重要的部分是:經由回饋改善測試的品質--這是XP關鍵性價值。於此回饋的形式(form)是來自未被BVT攔截的臭蟲。這裡的規則是「除非在BVT中你有一個失敗的單元測試 ;否則你沒辦法修復臭蟲」。以這種方式每次你修復一個臭蟲,你同時也加入一個測試以確保臭蟲不會再次從你手中溜走。甚至測試應該導引你思考加入其它的測試以增強BVT。

主建構

自動建構對於個別的開發者具有多重意義,但最重要的是為團隊產生一個主建構(master build)。我們發現有一個主建構的程序可以讓團隊更團結並且更容易在早期發現整合的問題。

第一步要做的是選擇一台機器執行主建構。我們使用Trebuchet(我們經常在上面玩Age of Empirse);一台有四個處理器的電腦非常適合作為主建構之用。(早期建構需耗費許多時間;其馬力不足是主要的因素。)

建構處理是在一個Java類別中不斷的執行。此建構處理器如果不進行建構一段時間則每幾分鐘循環的檢查儲存器(repository)。如果在最後的建構後沒有人簽入(check in)建構,則持續等待。如果在儲存器中有新的程式碼,則開始建構。

建構的第一階段是從儲存器執行完整的簽出(check out)。啟動指令(starteam)提供一個相當不錯Java的API,因此可以直接的掛上(hook into)儲存器。建構高手(daemon)檢視儲存器5分鐘看看是否有人簽入。如果這5分鐘內沒有人簽入可以將5分鐘前的程式碼簽出(這樣可以避免有人在簽入中途將其簽出而無須鎖住儲存器。)

高手簽出到Trebuchet中的資料夾。當所有的皆已簽出則高手啟動資料夾中的Ant腳本。Ant便接手執行完整的建構。我們從所有的原始碼執行完整的建構。Ant腳本到目前為止編譯並將產出的類別檔轉成十幾個jars以便部署到EJB伺服器中。

Ant完成編譯及部署,建構高手以新的jars啟動EJB伺服器並執行BVT測試系列。然後測試執行,如果全部通過測試我們便有了一個新的建構。建構高手便回到Starteam並 以一個建構編號標示我們簽出的原始碼。建構高手接著在他建構時尋找看看是否有人簽入,如果有便啟動另一個建構。若否便回到它的循環等待下一個簽入。

在建構終了,建構高手送出一封email給所有在這個建構當中有簽入程式碼的開發者。這封email總結這個建構的狀態。在你簽入程式碼直到收到這封email之前離開建構可能不是一個好的方式。

建構高手把記錄所有步驟寫在一個XML記錄檔。在Trebuchet中執行一個servlet允許任何人檢視這個記錄檔以便瞭解建構的狀態。

圖1: 建構servlet

在螢幕上顯示目前是否有建構在執行而且如果有則顯示何時開始。左有一個建構的所有歷史資料,顯示成功或失敗。點擊一個建構可以顯示這個建構的詳細資料:是否編譯、測試狀態、做了甚麼改變等等。

我們發現許多開發者會定期檢視這個頁面。這個頁面讓他們知道專案進行的如何及當有人簽入時有甚麼改變。從某些觀點我們可能把其他專案的新聞放到這個頁面上,但我們不想讓此頁面失去適當性。

讓所有開發者可以將主建構下載到其自己的電腦上是非常重要的。以這種方式如果整合失敗真的發生了,開發者可以在其自己的電腦上研究及除蟲而不必綁在主建構程序上。更進一步開發者在簽入之前可以在自己的電腦上執行建構以降低在主建構上的失敗。

這裡有一個合理的疑問關於主建構是否需要是一個乾淨的建構,亦即,僅允許從原始碼開始,或是從一個累進的(incremental)建構開始。累進的建構是比較快,但也有帶入潛藏問題的風險;因為有些東西並未編譯。同時也有無法重建(recreat)一個建構的風險。我們的建構相當快(200KLOC約15分鐘)因此我們比較喜歡採用乾淨的建構。不管如何有些工作室大多時候喜歡使用累進的建構,但定期的乾淨建構(至少每天)以免產生奇怪的錯誤(odd errors)。

簽入    Checking in

使用自動建構表示開發者在開發軟體時依循一種特定的節奏。這個節奏最重要的部分是有規律的整合(integrate regularly)。我們曾偶而碰過一些組織他們是每日建構,但人們並不是頻繁的簽入。如果開發者只是每幾週才簽入一次,那麼每日建構並不能提供你太多的好處。我們遵循的一個一般性原則是每一個開發者簽入程式碼約每日一次。

在一個新的任務開始之前,開發者應該先與環境管理系統同步。這意味著他自己的的原始碼必須是維持最新的版本。在過期的原始碼上寫程式只會導致麻煩及困惑。

接著開發者在這個任務中更新需要改變的檔案。當任務完成開發者便可以整合,或在任務中途便整合,但整合之前一定要執行所有的測試。

整合的第一步便是重新將儲存器與開發者手上的副本同步。任何在儲存器中被改變的檔案都被複製到工作中的目錄而若有任何衝突發生環境管理系統會向開發者提出警告。然後開發者需要建構同步的工作組並且在這些檔案成功的執行BVT。

現在開發者可以將新的檔案提交到儲存器中。當這步驟完成後開發者需要等待主建構。如果主建構成功,則簽入便成功。若否;直接的方式是開發者可以修復問題並且提交修復後的檔案。如果問題比較複雜開發者需要回復改變的部分,重新同步其工作中的目錄並且在下一次提交之前在其自己的機器中修正問題。

有些簽入程序強迫要做一系列的簽入程序。此時可以使用一個建構令牌(token),只有拿到此令牌的開發者才能做建構動作。當開發者拿到建構令牌,便將其工作副本同步,將改變的部分提交,最後釋放令牌。這種方式在有人建構時讓其他人無法更新儲存器。我們發現沒有建構令牌也不會有太多的麻煩,因此我們沒有使用建構令牌。常常有許多人在同一個主建構提交其程式,但只有少數因此導致建構失敗:而且都是很容易修復的。

我們也讓開發者自行判斷在簽入前應該有多小心。那是依賴開發者他們怎麼考慮可能的整合錯誤。如果她認為可能性很高,那麼她在簽入前先在其電腦上建構,否則便直接簽入。如果她是錯誤的一旦主建構執行便能立即發現;同時她必須回復其改變的部分並找出問題在哪裡。你可以容忍錯誤提供他們快速的尋找及方便的移除。

總結 Summing up

對於一個受控制的專案開發一種規律(discipline)及建構程序是最基本的。許多軟體訓練師如此說,但我們發現在這個領域中仍然很少見。

關鍵是要讓每一件事情自動化以及頻繁執行這些讓整合錯誤可以快速找到的程序。因此每一個人當他們需要時便隨時可以做改變,因為他們知道既使整合發成錯誤也很容易的發現及修改。當你有了這項優點,你將發現在你放棄他們之前他們是如此的讓你激動。



© Copyright Martin Fowler, all rights reserved

譯註1建構(build)廣義係指包含撰寫程式碼、測試、整合程式碼一系列的工作。其過程是每一個人針對部分單元撰寫程式碼(含編譯鍊結)及單元測試,再將此單元程式碼整合至共有的程式碼中並執行最終版本的測試。 本文中的建構主要使指將新增或修改的程式碼整合到主建構中並測試。