|
(作者:Martin Fowler,譯者:滕云)
原文發(fā)布時(shí)間:2006年5月1日 翻譯時(shí)間:2012年2月25日
持續(xù)集成是一種軟件開發(fā)實(shí)踐,在實(shí)踐中項(xiàng)目成員頻繁地進(jìn)行集成,通常每個(gè)成員每天都會(huì)做集成工作,如此,每天整個(gè)項(xiàng)目將會(huì)有多次集成。每次集成后都會(huì)通過自動(dòng)化構(gòu)建(包括測(cè)試)來盡快發(fā)現(xiàn)其中的錯(cuò)誤。許多團(tuán)隊(duì)都發(fā)現(xiàn)這種方法大大地減少了集成問題并且能夠快速地開發(fā)出高內(nèi)聚性的軟件。本文簡要地總結(jié)了持續(xù)集成技術(shù)及其現(xiàn)狀。
我還清楚地記得我剛加入一個(gè)大型軟件項(xiàng)目時(shí)的情形,那時(shí)我正在英國一個(gè)電子公司做暑期實(shí)習(xí)。我的經(jīng)理(屬于QA部門)領(lǐng)我參觀了一個(gè)巨大并很壓抑的倉庫,里面堆滿了大塊頭的主機(jī)。經(jīng)理告訴我這個(gè)項(xiàng)目已經(jīng)開發(fā)了有些年頭,現(xiàn)在正在做集成,并且已經(jīng)集成了好幾個(gè)月了。經(jīng)理還告訴我說,沒有人真正知道完成集成工作需要多少時(shí)間。由此我學(xué)到了軟件項(xiàng)目的一個(gè)通病:軟件集成是一個(gè)漫長并且無法預(yù)測(cè)的過程。
然而,軟件集成不必像這樣的。在ThoughtWorks的大多數(shù)項(xiàng)目還有世界上許多其它組織的軟件項(xiàng)目中,軟件集成并不是什么難事。每個(gè)開發(fā)人員離共享的工程狀態(tài)只有咫尺之遙,并且可以在幾分鐘之內(nèi)將自己的代碼集成進(jìn)去。任何集成錯(cuò)誤都能被快速地發(fā)現(xiàn)并得到快速的修正。
這種鮮明的對(duì)比并不是源自于后者有多么昂貴或復(fù)雜的工具,而關(guān)鍵在于每人都頻繁集成這種簡單實(shí)踐,通常是每天向一個(gè)被管控的代碼庫集成。
當(dāng)我向人們闡述這種實(shí)踐時(shí),通常得到兩種反應(yīng):“(在我們這里)行不通”和“無關(guān)緊要”。當(dāng)人們嘗試了這種實(shí)踐之后,他們發(fā)現(xiàn)其實(shí)做起來比說起來簡單,而且這樣的實(shí)踐對(duì)于開發(fā)“至關(guān)重要”。因此有了第三種反應(yīng):“是的,我們就是這么做的,不然該怎么活啊?”
“持續(xù)集成”源自于極限編程(XP),并且是XP最初的12種實(shí)踐之一。當(dāng)我以咨詢師的角色加入ThoughtWorks時(shí),我鼓勵(lì)我的團(tuán)隊(duì)使用這種技術(shù)。Matthew Foemmel將我的建議變成了實(shí)實(shí)在在的行動(dòng),由此軟件集成從少有發(fā)生并且復(fù)雜的狀態(tài)變成了一樁易事。Matthew和我將我們的經(jīng)驗(yàn)寫在了本文的第一版中,而本文也是我的個(gè)人網(wǎng)站上最受歡迎的文章之一。
雖然持續(xù)集成并不需要使用特別的工具來部署,但是我們發(fā)現(xiàn)擁有一臺(tái)持續(xù)集成服務(wù)器將大有益處,其中最著名的有開源的CruiseControl,該軟件最初由ThoughtWorks的幾個(gè)員工開發(fā),現(xiàn)在由一個(gè)很大的社區(qū)維護(hù)著。后來幾款其它的持續(xù)集成服務(wù)器也相繼出現(xiàn)了,有開源的,也有商業(yè)化的,包括ThoughtWorks Studios的Cruise。
在開發(fā)中使用持續(xù)集成
對(duì)于我來說,解釋持續(xù)集成及其工作原理最簡單的方法便是以一個(gè)小的軟件功能的開發(fā)為例來進(jìn)行演示。假設(shè)我們需要向軟件添加一點(diǎn)功能,至于是什么樣的功能并不重要,我們假定它很小并且可以在幾個(gè)小時(shí)內(nèi)完成。
首先我們需要在本地機(jī)器上保留一份當(dāng)前已經(jīng)處于集成狀態(tài)的代碼的拷貝。我通過代碼管理系統(tǒng)在代碼庫的主線(mainline)上拉下(check out)一份工作代碼拷貝。
上一段文字主要針對(duì)使用代碼控制系統(tǒng)的人,對(duì)于不使用代碼控制系統(tǒng)的人來說便是胡言亂語了。因此,我將向后者解釋一下。代碼控制系統(tǒng)用于將項(xiàng)目所有的代碼保存在一個(gè)代碼庫(repository)中,項(xiàng)目當(dāng)前的狀態(tài)通常被稱為主線。任何時(shí)候開發(fā)人員都可以從主線上獲得一份拷貝到本地機(jī)器,這被稱為“checking out”。本地機(jī)器上的代碼拷貝稱為“working copy”。(多數(shù)時(shí)候,實(shí)際上你是在更新(update)本地代碼到主線狀態(tài),實(shí)踐中它們是一樣的效果。)
現(xiàn)在,為了完成軟件的功能添加,我對(duì)本地代碼進(jìn)行修改,其中既包括修改產(chǎn)品代碼,也包括添加自動(dòng)化測(cè)試。持續(xù)集成非常看重測(cè)試,并且在軟件代碼本身中達(dá)到了測(cè)試自動(dòng)化——我將其稱為自測(cè)試代碼,通常使用流行的XUnit測(cè)試框架的一個(gè)版本。
當(dāng)我完成了功能開發(fā)(或者在我開發(fā)過程的不同階段),我將在本地開發(fā)機(jī)上完成自動(dòng)化構(gòu)建。構(gòu)建過程將編譯并鏈接本地代碼,然后跑自動(dòng)化測(cè)試。只有當(dāng)構(gòu)建和測(cè)試都沒有錯(cuò)誤時(shí),該次構(gòu)建才能算是好的構(gòu)建。
有了本地的成功構(gòu)建,我便可以考慮將我修改的代碼提交到代碼庫了。但是,在我提交之前,其他開發(fā)人員可能已經(jīng)向主線提交了他們的修改,所以首先我需要將他們的修改更新到我本地并且重新構(gòu)建。如果他人的修改與我的修改有沖突,那么在本地編譯或者測(cè)試階段將會(huì)發(fā)生錯(cuò)誤,這種情況下,我需要負(fù)責(zé)修改本地代碼直到與主線代碼保持適當(dāng)同步為止。
當(dāng)本地代碼與主線代碼同步之后,我便可以向主線提交自己的修改了,代碼庫也得以更新。
然而,單是提交了修改并不表示我的工作就完成了。我需要再次構(gòu)建,但這次是在一臺(tái)擁有主線代碼的集成機(jī)器上進(jìn)行。只有這次構(gòu)建成功了才表示我的任務(wù)完成。通常會(huì)出現(xiàn)這樣的情況:我忘了提交本地機(jī)器上的一些東西,因此代碼庫并沒有得到適當(dāng)?shù)母隆V挥形姨峤坏男薷脑诩蓹C(jī)器上成功構(gòu)建之后,我的工作才算完成。這樣的集成構(gòu)建可以由我手動(dòng)完成,也可以由Cruise自動(dòng)完成。
當(dāng)兩個(gè)開發(fā)者的代碼有沖突時(shí),通常會(huì)在第二個(gè)開發(fā)者更新本地代碼時(shí)捕獲到,否則,集成構(gòu)建應(yīng)該會(huì)失敗。在這兩種途徑中,錯(cuò)誤都可以被快速地發(fā)現(xiàn)。在持續(xù)集成環(huán)境中,你決不應(yīng)該使失敗的集成構(gòu)建保留太長時(shí)間。一個(gè)好的團(tuán)隊(duì)每天都應(yīng)該有許多成功的構(gòu)建。當(dāng)然,失敗的構(gòu)建也會(huì)時(shí)常發(fā)生,但需要盡快的修復(fù)。
這樣做的結(jié)果是,我們總會(huì)得到一個(gè)穩(wěn)定并且工作正常的軟件。每個(gè)人都圍繞著一個(gè)共享并穩(wěn)定的基礎(chǔ)代碼庫工作,絕不離基礎(chǔ)代碼庫太遠(yuǎn)以至于需要很長的時(shí)間將自己的修改集成到基礎(chǔ)代碼庫中。如此這般,我們花在找bug上的時(shí)間減少了,因?yàn)閎ug在頻繁的集成中經(jīng)常出現(xiàn)。
持續(xù)集成實(shí)踐
上文只是關(guān)于持續(xù)集成的一個(gè)概要和它在日常開發(fā)中的工作原理。讓所有這些都能很好的運(yùn)作顯然不止于此。現(xiàn)在,就讓我們來看看有效持續(xù)集成所需的關(guān)鍵實(shí)踐。
維護(hù)一個(gè)單一的代碼庫
軟件項(xiàng)目需要大量的文件協(xié)同工作來構(gòu)建出最終的產(chǎn)品。跟蹤所有的文件需要大量的工作,尤其是在多個(gè)開發(fā)者參與的項(xiàng)目中。因此,我們可以并不驚奇的看到,不同的軟件開發(fā)團(tuán)隊(duì)都在開發(fā)用于管理這些文件的工具——源代碼管理工具,也叫配置管理,版本控制系統(tǒng),代碼庫等。這些工具是多數(shù)軟件項(xiàng)目不可分的組成部分。然而,令人傷心并吃驚的是,并不是所有的項(xiàng)目都使用了這樣的工具。我的確見到(雖然很少)不使用這些工具的項(xiàng)目,它們使用本地和共享磁盤這種混亂的結(jié)合來共同工作。
因此,做為最基本的持續(xù)集成實(shí)踐,請(qǐng)保證你使用一款體面的代碼管理系統(tǒng)。成本不是問題,有許多高質(zhì)量的開源代碼管理工具存在。當(dāng)前的選擇為Subversion(譯者注:現(xiàn)在有了更新的hg和git)。(更老的開源工具CVS如今仍然被大量使用,雖然比沒有強(qiáng),但是Subversion是更現(xiàn)代的選擇。)有趣的是,當(dāng)我和一些開發(fā)者聊天時(shí),我發(fā)現(xiàn)相比起多數(shù)商業(yè)化的代碼管理系統(tǒng),他們更喜歡Subversion。據(jù)我所知,唯一值得花錢買的只有Perforce。
當(dāng)你有了代碼管理系統(tǒng)之后,確保每個(gè)開發(fā)者都能方便的獲得到源代碼。不應(yīng)該有人還在問:“foo-whiffle 文件在哪兒?”所有東西都必須在代碼庫里。
雖然許多團(tuán)隊(duì)都在使用代碼庫,但是我經(jīng)常發(fā)現(xiàn),他們并不把所有東西都放在里面。如果大家需要使用一個(gè)文件,他們知道該文件放到代碼庫中,但是,構(gòu)建所需的所有都應(yīng)該包含在代碼庫里,包括測(cè)試腳本,屬性文件,數(shù)據(jù)庫模式文件,安裝腳本和第三方庫等。我所知道的有項(xiàng)目將編譯器加到代碼庫中的(對(duì)于早期脆弱的C++編譯器來說非常重要)。基本原則是:在一臺(tái)新機(jī)器上check out代碼后構(gòu)建也能構(gòu)建成功。新機(jī)器上的東西應(yīng)該盡量的少,通常包括很大的,難于安裝的,并且穩(wěn)定的軟件,比如操作系統(tǒng),Java開發(fā)環(huán)境或者數(shù)據(jù)庫管理系統(tǒng)等。
你需要將構(gòu)建所需的所有東西都加到代碼管理系統(tǒng)中,同時(shí)也需要將大家經(jīng)常操作的東西方進(jìn)去,IDE配置便是一個(gè)很好的例子,這樣便于大家共享IDE配置。
版本控制系統(tǒng)的一大功能是它允許你創(chuàng)建多個(gè)分支,以此來處理不同的“開發(fā)流”。這種功能很有用,但卻經(jīng)常被過度使用以至給開發(fā)者帶來了不少麻煩。所以,你需要將分支的使用最小化,特別建議使用主線,即項(xiàng)目中只有單一的開發(fā)分支,并且每人在多數(shù)時(shí)間里都在“離線”工作。
總之,你應(yīng)該將構(gòu)建所需的所有東西都放在代碼管理系統(tǒng)中,而不應(yīng)該將構(gòu)建的輸出放進(jìn)去。有些朋友確實(shí)將構(gòu)建輸出放在代碼管理系統(tǒng)中,但我認(rèn)為這是一個(gè)壞味道,可能導(dǎo)致更深的問題——通常是你無法完成重新構(gòu)建。
使構(gòu)建自動(dòng)化
將源代碼變成一個(gè)能運(yùn)行的軟件系統(tǒng)通常是一個(gè)復(fù)雜的過程,包括編譯,文件搬移,加載數(shù)據(jù)庫模式等等。但其中大多數(shù)任務(wù)都是可以自動(dòng)化的,并且也應(yīng)該被自動(dòng)化。讓人去輸入奇怪的命令或點(diǎn)擊對(duì)話框是非常耗時(shí)的,而且從根本上來說也是個(gè)錯(cuò)誤的做法。
構(gòu)建所需的自動(dòng)化環(huán)境對(duì)于軟件系統(tǒng)來說是一個(gè)通用功能。Unix的Make已經(jīng)誕生好多年了,Java社區(qū)有Ant, .NET社區(qū)有Nant,現(xiàn)在又有了MSBuild。當(dāng)你用這些工具構(gòu)建和啟動(dòng)系統(tǒng)時(shí),請(qǐng)確保只使用一個(gè)命令完成任務(wù)。
一個(gè)常見的錯(cuò)誤是在自動(dòng)化構(gòu)建里并沒有完全包括構(gòu)建所需的東西。構(gòu)建過程中應(yīng)該從代碼庫里取得數(shù)據(jù)庫模式文件并自動(dòng)執(zhí)行之。結(jié)合我上文所講的原則來看,任何人都應(yīng)該能夠在一臺(tái)新機(jī)器上拉下代碼庫中的代碼,并只用一個(gè)命令將系統(tǒng)運(yùn)行起來。
構(gòu)建腳本是多種多樣的,通常特定于某個(gè)平臺(tái)或社區(qū),但情況并不必須如此。我們的多數(shù)Java項(xiàng)目都使用Ant,而另外有些用Ruby(Ruby世界的Rake是一個(gè)非常不錯(cuò)的構(gòu)建工具)。我們用Ant完成了早期的一個(gè)微軟COM工程的構(gòu)建自動(dòng)化,并從中大獲裨益。
大型的構(gòu)建通常需要很長的時(shí)間,而在你只做了很小的修改的情況下,你是不想運(yùn)行所有的構(gòu)建步驟的。因此,優(yōu)秀的構(gòu)建工具能夠分析出哪些地方需要做相應(yīng)的修改,并將這個(gè)分析過程本身做為整個(gè)構(gòu)建過程的一部分。通常的做法是檢查源代碼和目標(biāo)文件的修改日期,只有當(dāng)源代碼的修改日期晚于其對(duì)應(yīng)的目標(biāo)文件時(shí)才執(zhí)行編譯。依賴關(guān)系因此變得微妙起來了:如果一個(gè)目標(biāo)文件發(fā)生了修改,那些依賴于它的文件也需要重新構(gòu)建。有些編譯器能夠處理這種依賴關(guān)系,而有些就不見得。
根據(jù)自己的需要,你可以選擇不同的東西進(jìn)行構(gòu)建。構(gòu)建中既可以包括測(cè)試,也可以不包括,甚至可以包括不同的測(cè)試板塊。有些組件可以進(jìn)行單獨(dú)構(gòu)建。構(gòu)建腳本應(yīng)該能夠允許你針對(duì)不同的情形進(jìn)行不同的構(gòu)建目標(biāo)。
我們大多數(shù)都使用IDE,而多數(shù)IDE都或多或少地集成了構(gòu)建管理功能。但是這樣構(gòu)建文件通常是特定于IDE的,而且非常脆弱。此外,它們需要依賴于IDE才能工作。雖然對(duì)于開發(fā)者個(gè)人來說,在IDE中做這樣的構(gòu)建配置并無不妥,但對(duì)于持續(xù)集成服務(wù)器來說,一份能夠被其它腳本調(diào)用的主構(gòu)建腳本卻是至關(guān)重要的。比如一個(gè)Java項(xiàng)目,各個(gè)開發(fā)者可以在自己的IDE中進(jìn)行構(gòu)建,但應(yīng)該還有一個(gè)Ant主構(gòu)建腳本來保證構(gòu)建能在集成服務(wù)器上順利完成。
使構(gòu)建自測(cè)試
傳統(tǒng)意義上的構(gòu)建包括只編譯,鏈接等過程。此時(shí)程序也許能運(yùn)行起來,但這并不意味著系統(tǒng)就能正確地運(yùn)行。雖然現(xiàn)在的靜態(tài)語言已經(jīng)能夠捕捉到許多bug,但是漏網(wǎng)之魚卻更多。
一種快速并高效發(fā)現(xiàn)bug的方法是將自動(dòng)化測(cè)試包含到構(gòu)建過程中。當(dāng)然,測(cè)試也不見得完美,但的確能發(fā)現(xiàn)很多bug——足夠多了。特別是隨著極限編程(XP)的升溫,測(cè)試驅(qū)動(dòng)開發(fā)(TDD)也使自測(cè)試代碼流行起來,越來越多的人開始注意到這種技術(shù)的價(jià)值所在。
經(jīng)常讀我著作的讀者可能知道我是一個(gè)TDD和XP的大粉絲,然而我想強(qiáng)調(diào)的是這兩種方法和自測(cè)試并沒有必然聯(lián)系。TDD和XP都要求先寫測(cè)試代碼,再寫功能代碼使測(cè)試通過。在這種模式下,測(cè)試既用于發(fā)現(xiàn)bug,又用于完成系統(tǒng)設(shè)計(jì)。這是非常好的,但對(duì)于持續(xù)集成來說不必如此,因?yàn)榇藭r(shí)我們自測(cè)試代碼的要求并不那么高。(然而TDD是我寫自測(cè)試代碼的首選。)
對(duì)于自測(cè)試代碼而言,你需要一組自動(dòng)化測(cè)試來檢測(cè)一大部分代碼庫中的bug。測(cè)試能通過一個(gè)簡單的命令來運(yùn)行并且具備自檢功能。測(cè)試的結(jié)果應(yīng)該能指出哪些測(cè)試是失敗的。對(duì)于自測(cè)試的構(gòu)建來說,測(cè)試失敗應(yīng)導(dǎo)致構(gòu)建失敗。
過去這些年里,TDD使開源的XUnit家族流行起來,成為了理想的測(cè)試工具。在ThoughtWorks,XUnit已經(jīng)是非常有用的測(cè)試工具,我也經(jīng)常建議人們使用。這組工具起初由Kent Beck開發(fā),它們使自測(cè)試環(huán)境的搭建變得非常簡單。
XUnit當(dāng)之無愧地是你進(jìn)行代碼自測(cè)試的起點(diǎn)。當(dāng)然,你也應(yīng)當(dāng)多看看那些更側(cè)向于端到端測(cè)試的工具,包括FIT,Selenium,Sahi,Watir,FITnesse等等,我就不逐一列舉了。
當(dāng)然,別指望測(cè)試就是萬能的。常言道,測(cè)試并不代表就沒有bug。
每人每天都向主線提交代碼
集成首先在于交流,它使其他成員能夠看到你所做的修改。在這種頻繁的交流下,大家都能很快地知道開發(fā)過程中所做的修改。
在向主線提交代碼之前,開發(fā)人員必須保證本地構(gòu)建成功。這當(dāng)然也包括使測(cè)試全部通過。另外,在提交之前需要更新本地代碼以匹配主線代碼,然后在本地解決主線代碼與本地代碼之間的沖突,再在本地進(jìn)行構(gòu)建。如果構(gòu)建成功,便可以向主線提交代碼了。
在這種頻繁提交下,開發(fā)者可以快速地發(fā)現(xiàn)自己代碼與他人代碼之間的沖突。快速解決問題的關(guān)鍵在于快速地發(fā)現(xiàn)問題。幾個(gè)小時(shí)的提交間隔使得代碼沖突也可以在幾個(gè)小時(shí)內(nèi)發(fā)現(xiàn),此時(shí)大家的修改都不多,沖突也不大,因此解決沖突也很簡單。對(duì)于好幾周都發(fā)現(xiàn)不了的沖突,通常是很難解決的。
在更新本地代碼庫時(shí)就進(jìn)行構(gòu)建,這意味著我們既可以發(fā)現(xiàn)文本上的沖突,又可以發(fā)現(xiàn)編譯沖突。既然構(gòu)建是自測(cè)試的,那么運(yùn)行時(shí)的沖突也可以被檢測(cè)出來,而這樣的沖突往往是一些特別煩人的bug。由于提交間隔只有短短的幾個(gè)小時(shí),bug便沒多少藏身之處了。再者,因?yàn)槊看翁峤坏男薷亩疾欢啵憧梢允褂?a target="_blank">diff-debugging來幫你找出這些bug。
我的基本原則是:每個(gè)開發(fā)者每天都應(yīng)當(dāng)向代碼庫進(jìn)行提交。在實(shí)踐中,越是頻繁提交,可能導(dǎo)致沖突的地方就越少,因而也越容易發(fā)現(xiàn)。
頻繁提交鼓勵(lì)開發(fā)人員以幾個(gè)小時(shí)為單位來分割他們的代碼,這樣便于跟蹤進(jìn)度。通常,人們一開始認(rèn)為在短短的幾個(gè)小時(shí)內(nèi)做不了什么事情,但我們發(fā)現(xiàn)找個(gè)導(dǎo)師和多實(shí)踐可以幫助他們學(xué)習(xí)。
每次提交都應(yīng)在集成機(jī)上進(jìn)行構(gòu)建
有了每日提交,也就又了每日測(cè)試,這應(yīng)該表明主線處于健康狀態(tài)。但是在實(shí)踐中,的確有出錯(cuò)的時(shí)候,原因之一在于紀(jì)律——有人并沒有在提交之前進(jìn)行本地更新和構(gòu)建。另外,不同開發(fā)機(jī)之間的環(huán)境不同也是一個(gè)原因。
因此,你應(yīng)該保證在集成機(jī)上進(jìn)行構(gòu)建,只有當(dāng)集成機(jī)上構(gòu)建成功后,才表明你的任務(wù)完成了。由于提交者需要對(duì)自己的提交負(fù)責(zé),他就得盯著主線上的構(gòu)建,如果失敗,馬上修改。如果下班之前你提交的修改失敗了,那么,對(duì)不起,請(qǐng)修改好了才回家。
我見到過兩種方式來保證主線構(gòu)建的成功:一是手動(dòng)構(gòu)建,二是使用持續(xù)集成服務(wù)器。
手動(dòng)構(gòu)建是最簡單的,基本上與開發(fā)者在本地做的構(gòu)建差不多——先到集成機(jī)上拉下主線的最新代碼,然后運(yùn)行構(gòu)建命令,在構(gòu)建過程中你得盯著構(gòu)建過程,如果構(gòu)建成功,表明你的任務(wù)完成。(另見Jim Shore的描述。)
持續(xù)集成服務(wù)器則一直監(jiān)視著代碼庫,一旦檢測(cè)到有提交,便自動(dòng)拉下代碼到本機(jī),然后開始構(gòu)建,并將結(jié)果通知提交者。只有當(dāng)提交者收到通知后——通常是以電子郵件的方式,才表明自己的任務(wù)完成。
在ThoughtWorks,我們是持續(xù)集成服務(wù)器的忠實(shí)粉絲,我們領(lǐng)導(dǎo)了CruiseControl和CruiseControl.NET的初期開發(fā),此兩者均是廣為使用的CI服務(wù)器。從那時(shí)起,我們也開發(fā)了商業(yè)化的Cruise。在幾乎每個(gè)項(xiàng)目中,我們都使用了CI服務(wù)器,并且結(jié)果是令人愉悅的。
不是所有人都傾向于使用CI服務(wù)器的,Jim Shore便給出了一個(gè)很好的論述,在此論述中,他解釋了為什么他更傾向于手動(dòng)構(gòu)建。我同意他的看法——CI不過是安裝一些軟件而已,所有的實(shí)踐都應(yīng)當(dāng)旨在有效地完成持續(xù)集成。但同樣,許多使用CI服務(wù)器的團(tuán)隊(duì)的確發(fā)現(xiàn)CI服務(wù)器是很好的工具。
有很多團(tuán)隊(duì)定期的進(jìn)行構(gòu)建,比如每晚構(gòu)建。這和持續(xù)構(gòu)建并不是一回事,而且對(duì)于持續(xù)集成來說,也是不夠的。持續(xù)集成的關(guān)鍵在于盡快地發(fā)現(xiàn)問題。而每晚構(gòu)建意味著整個(gè)白天都發(fā)現(xiàn)不了bug,如此,需要很長的時(shí)間發(fā)現(xiàn)并清楚這些bug。
持續(xù)構(gòu)建的重點(diǎn)在于,如果主線構(gòu)建失敗,你應(yīng)該馬上進(jìn)行修改。在持續(xù)集成中,你一直是在一個(gè)穩(wěn)定的代碼庫基礎(chǔ)上進(jìn)行開發(fā)。主線構(gòu)建失敗并不是一件壞事,但是,如果這樣的情況經(jīng)常發(fā)生,那么就意味著開發(fā)人員對(duì)于本地更新并沒在意或者在提交之前并沒在本地構(gòu)建。主線構(gòu)建一旦失敗,必須馬上修正。為了避免主線構(gòu)建失敗,也許你可以試試 pending head。
快速構(gòu)建
持續(xù)集成的關(guān)鍵在于快速反饋,需要長時(shí)間構(gòu)建的CI是極其糟糕的。我的多數(shù)同事都認(rèn)為一個(gè)小時(shí)的構(gòu)建時(shí)間對(duì)于CI來說決無道理可言。我也記得曾經(jīng)有團(tuán)隊(duì)夢(mèng)想著他們的構(gòu)建能有多么多么的快,但有時(shí)我們不得不面對(duì)很難快速構(gòu)建的情況。
對(duì)于多數(shù)項(xiàng)目來說,將構(gòu)建時(shí)間維持在10鐘之內(nèi)是合理的,這也是XP的方針之一,我們多數(shù)項(xiàng)目也達(dá)到了這個(gè)目標(biāo)。這種做法是值得的,因?yàn)檫@樣省下的時(shí)間是為開發(fā)者節(jié)約的。
如果你的構(gòu)建長到了一小時(shí),那么想使其加速便不是那么容易了。對(duì)于企業(yè)級(jí)應(yīng)用來說,我們發(fā)現(xiàn)構(gòu)建時(shí)間的瓶頸通常發(fā)生在測(cè)試上,特別是那些需要于外部交互的測(cè)試——比如數(shù)據(jù)庫。
可能最好的解決辦法是引入階段性構(gòu)建(也叫構(gòu)建管道或者部署管道),因?yàn)闃?gòu)建事實(shí)上是分階段性的。代碼提交后首先觸發(fā)的是構(gòu)建稱為提交構(gòu)建,提交構(gòu)建應(yīng)該快速完成,而棘手的是怎么保持速度與查找bug之間的平衡。
提交構(gòu)建成功后,其他人便可自信的工作了。但是,你可能還有其它跑得比較慢的測(cè)試需要寫,這時(shí)可以用額外的機(jī)器來專門跑這些耗時(shí)的測(cè)試。
一個(gè)簡單的例子是將構(gòu)建分為兩個(gè)階段,第一個(gè)階段完成編譯,并且跑那些不需要外部交互的單元測(cè)試,數(shù)據(jù)庫交互也通過stub的方式完全消除掉。這些測(cè)試可以很快跑完,原則是將其保持在10分鐘之內(nèi)。但是,對(duì)于那些需要大量外部交互——特別是涉及到真實(shí)數(shù)據(jù)庫交互時(shí)才能發(fā)現(xiàn)的bug,這個(gè)階段便無能為力了。第二個(gè)階段跑的測(cè)試則需要操作真實(shí)的數(shù)據(jù)庫了,同時(shí)還應(yīng)包括端到端測(cè)試。這個(gè)階段可能需要幾個(gè)小時(shí)。
在這種情況下,通常將第一階段視為提交構(gòu)建,并將此做為主要的CI周期。第二階段則可在有必要時(shí)才進(jìn)行,如果這個(gè)階段構(gòu)建失敗,它也不需要像第一階段那樣“停下全部手頭的工作”,但也應(yīng)該得到盡快的修改。第二階段的構(gòu)建不見得需要保持一直通過,對(duì)于已經(jīng)發(fā)現(xiàn)的bug來說,可以在之后幾天修改。對(duì)于這個(gè)案例來說,第二階段全是測(cè)試,因?yàn)橥ǔG闆r下最慢的即是測(cè)試。
如果第二階段構(gòu)建發(fā)現(xiàn)了bug,通常意味著應(yīng)該在第一階段中引入新的測(cè)試來予以保證。
當(dāng)然,以上的兩階段構(gòu)建只是一個(gè)例子,你完全可以加入多個(gè)構(gòu)建階段。提交構(gòu)建之后的其它構(gòu)建是可以并行完成的,如果這些階段的構(gòu)建需要好幾個(gè)小時(shí),那么可以用幾臺(tái)機(jī)器來并行完成。通過這種并行化,你可以將提交構(gòu)建之外的所有測(cè)試都引入到構(gòu)建過程中來,比如性能測(cè)試。
在與生產(chǎn)環(huán)境的拷貝環(huán)境中運(yùn)行測(cè)試
測(cè)試旨在發(fā)現(xiàn)可能在生產(chǎn)環(huán)境中出現(xiàn)的問題,因此如果你的測(cè)試環(huán)境與生產(chǎn)環(huán)境不同,那么測(cè)試很有可能發(fā)現(xiàn)不了生產(chǎn)環(huán)境中的bug。
因此,你的測(cè)試環(huán)境應(yīng)該盡量與生成環(huán)境相同。使用相同的數(shù)據(jù)庫,相同的操作系統(tǒng),并且版本都應(yīng)該一樣。另外,將生產(chǎn)環(huán)境中的庫文件也放到測(cè)試環(huán)境中,即使構(gòu)建時(shí)用不到這些庫。IP地址和端口號(hào)也應(yīng)當(dāng)相同,當(dāng)然還包括硬件。
但事實(shí)上這是有限制的。如果你開發(fā)的是桌面軟件,很難預(yù)測(cè)你的客戶在使用哪些第三方庫。再者,生產(chǎn)環(huán)境可能非常昂貴。即便存在這么多限制,你依然應(yīng)當(dāng)盡量去復(fù)制生產(chǎn)環(huán)境,并熟知因測(cè)試環(huán)境和生產(chǎn)環(huán)境的不同而可能導(dǎo)致的風(fēng)險(xiǎn)。
如果你搭建的環(huán)境足夠簡單并沒有多少煩人的外部交互,那么你的提交構(gòu)建便可在仿真環(huán)境中進(jìn)行。但是,由于系統(tǒng)反應(yīng)慢等原因,你可能需要test doubles。因此,通常情況是在人工環(huán)境下跑提交構(gòu)建以獲取速度,而用一個(gè)生產(chǎn)環(huán)境的拷貝環(huán)境來跑其它測(cè)試。
我注意到,虛擬化技術(shù)越來越引起人們的興趣。由于虛擬機(jī)可以保存構(gòu)建所需的所有東西,故在虛擬機(jī)中運(yùn)行構(gòu)建和測(cè)試相對(duì)比較容易。另外,虛擬機(jī)技術(shù)也允許你在一臺(tái)機(jī)器上運(yùn)行多個(gè)測(cè)試,或者可以模擬多臺(tái)機(jī)器同時(shí)訪問網(wǎng)絡(luò)的情況。隨著虛擬機(jī)性能逐漸提升,它將引起更多的注意。
使任何人都能輕易獲得可執(zhí)行文件
軟件開發(fā)最困能的事情之一便是你不能保證所開發(fā)的是正確的軟件。我們發(fā)現(xiàn)人們往往很難預(yù)知自己究竟想要什么,而相反,對(duì)已有的東西進(jìn)行評(píng)判和修改卻容易的多。敏捷開發(fā)過程則恰恰是符合人類這種行為習(xí)慣的。
為此,項(xiàng)目中的所有成員都應(yīng)能夠獲得最新的可執(zhí)行文件并能成功的運(yùn)行,目的可以包括做演示,瀏覽測(cè)試或者僅僅看看項(xiàng)目本周有何修改。
這是很容易達(dá)到的:確保一個(gè)通用的地方來存放最新可執(zhí)行文件。在同一個(gè)地方存放多個(gè)可執(zhí)行文件也是很有用的。對(duì)于最新的可執(zhí)行文件,應(yīng)當(dāng)保證能夠通過提交測(cè)試。
如果你的開發(fā)過程有一個(gè)很好的迭代計(jì)劃,將每次迭代最后一次構(gòu)建生成的可執(zhí)行文件存放起來也是明智的做法。
人人都能看到正在發(fā)生什么
持續(xù)集成主要在于交流,因此應(yīng)當(dāng)保證每人都能輕易看到當(dāng)前系統(tǒng)的狀態(tài)和已做的修改。
主線的構(gòu)建狀態(tài)是非常重要的,Cruise服務(wù)器包含一個(gè)網(wǎng)站,你可以在該網(wǎng)站上看到當(dāng)前的構(gòu)建狀態(tài)和最后一次主線構(gòu)建的結(jié)果,許多團(tuán)隊(duì)喜歡用比較顯眼的標(biāo)識(shí)來反應(yīng)構(gòu)建狀態(tài),比如在屏幕上放一盞燈,燈綠表示構(gòu)建成功,燈紅表示失敗。尤其常見的是lava lamps——不僅表明構(gòu)建狀態(tài),還可顯示構(gòu)建時(shí)間。如果紅燈中有了氣泡,則表明構(gòu)建已經(jīng)失敗了很長一段時(shí)間了。每個(gè)團(tuán)隊(duì)都有自己的選擇,當(dāng)然,適合自己的才是最好的。
對(duì)于手工完成的持續(xù)集成過程,這種可見性也是很重要的,構(gòu)建機(jī)器的顯示器應(yīng)該能顯示主線構(gòu)建的狀態(tài)。通常,正在做集成的人會(huì)放一個(gè)token在桌上來表明他正在做集成。人們喜歡在構(gòu)建成功后播放一些簡單的聲音,比如鬧鈴之類的。
當(dāng)然,CI服務(wù)器的網(wǎng)站可以展示更多的信息。Cruise不但能可以顯示是誰在構(gòu)建,并且能顯示最新提交的修改。另外,Cruise還可以查看提交歷史,這樣,團(tuán)隊(duì)成員便可以很清楚項(xiàng)目的進(jìn)展情況。據(jù)我所知,有些團(tuán)隊(duì)的頭便是通過這種方式來了解項(xiàng)目成員的工作情況和整個(gè)系統(tǒng)的修改情況。
使用CI網(wǎng)站的另一個(gè)好處是,哪怕不在一起工作的人都可以看到當(dāng)前項(xiàng)目的狀態(tài)。再者,你也可以將不同項(xiàng)目的構(gòu)建信息放到一起。
并不是只有CI網(wǎng)站才能展示顯示構(gòu)建信息。由于構(gòu)建的不穩(wěn)定性是一直存在的,這時(shí)我們可以將全年的日歷畫在一張墻上,每天對(duì)應(yīng)一個(gè)方塊,如果構(gòu)建成功,QA則在該天的方塊貼上綠色標(biāo)簽,否則貼上紅色標(biāo)簽。時(shí)間一久,這份日歷便可顯示出項(xiàng)目的穩(wěn)定性進(jìn)展情況。
自動(dòng)化部署
做持續(xù)集成需要多種環(huán)境,不同的構(gòu)建階段需要不同的環(huán)境。每天,項(xiàng)目的可執(zhí)行文件都會(huì)在這些環(huán)境之間搬來移去,于是你希望將這些過程自動(dòng)化。因此,自動(dòng)化部署腳本便很重要了,不僅包括測(cè)試環(huán)境的腳本,也包括針對(duì)生產(chǎn)環(huán)境的部署腳本。雖然我們不是每天都向生產(chǎn)環(huán)境部署,但自動(dòng)化部署不僅可以加速部署過程,并且能夠減少部署錯(cuò)誤。
如果你已經(jīng)有了生產(chǎn)環(huán)境的自動(dòng)化部署,那么也應(yīng)該考慮一下相應(yīng)的自動(dòng)化回滾。由于失敗是時(shí)而會(huì)發(fā)生的事情,在這種情況下,我們希望能快速回滾到失敗之前的狀態(tài)。這樣一來,我們?cè)诓渴鹗且膊挥媚敲次肥孜肺擦耍谑俏覀兛梢灶l繁的發(fā)布軟件,用戶亦能盡快的享受到新的功能。(Ruby on Rails社區(qū)有一款名為Capistrano的工具即是一個(gè)典型的例子。)
在集群環(huán)境中,我看到有每次只向一個(gè)節(jié)點(diǎn)部署的情況,由此在幾個(gè)小時(shí)之內(nèi)逐漸完成所有節(jié)點(diǎn)的部署。
對(duì)于一些面向大眾的web應(yīng)用,我所了解的另外一種很有趣的部署方式是,先試驗(yàn)性針對(duì)一部分用戶進(jìn)行部署,再通過這些用戶的試用情況來決定是否向所有用戶部署。自動(dòng)化部署,做為CI的一項(xiàng)原則,能夠很好的勝任這些工作。
持續(xù)集成的好處
總的來說,我認(rèn)為持續(xù)集成的最大好處在于降低風(fēng)險(xiǎn)。我又想起了我在本文一開始提到的那個(gè)項(xiàng)目——已經(jīng)處于項(xiàng)目的末期,但是仍然不知到何時(shí)才能結(jié)束。
延期集成的缺點(diǎn)在于,很難預(yù)測(cè)集成到底要花多少時(shí)間,更糟的是,你很難了解集成的進(jìn)展情況。
持續(xù)集成正好解決了這些問題。每次集成的時(shí)間都不長,任何時(shí)候你都知道自己所處的情況,軟件的哪些地方在工作,哪些沒有。
Bug——惡心的玩意兒,傷害我們的自信,攪亂我們的日程,還破壞我們的名聲。如果在生產(chǎn)環(huán)境中遇到了bug,那么用戶將會(huì)把氣往你身上撒。而在開發(fā)環(huán)境中,bug攔著你的路,迫使你無法完成余下的工作。
持續(xù)集成并不能消除bug,卻能幫你快速的發(fā)現(xiàn)bug并予以清除。這種情況下,持續(xù)集成更像是自測(cè)試的代碼。當(dāng)遇到bug時(shí),由于你只做了很小的修改,這樣便大大縮小的bug的查找范圍。另外,由于是你剛寫的代碼,你還記得很清楚,因此也使查找bug更加容易。你還可以使用diff debugging,將當(dāng)前的代碼版本和先前沒有bug的版本進(jìn)行比較。
Bug也存在積累性,bug越多,越難清除。部分原因在于bug之間存在牽連。另外也存在心理因素,bug一多,人便沒那么多精力去修了——這就是所謂的“Broken Windows 綜合征”。
因此,對(duì)于采用持續(xù)集成的團(tuán)隊(duì),bug將大大減少,不管是在生產(chǎn)環(huán)境,還是在開發(fā)環(huán)境。但是,我想強(qiáng)調(diào)的是,你的獲益程度取決于測(cè)試的好壞程度。你或許已發(fā)現(xiàn),寫出好多測(cè)試并不難。然而,要達(dá)到低bug率的程度依然是需要時(shí)間的,你還得不斷地引入并改進(jìn)自己的測(cè)試。
有了持續(xù)集成,頻繁部署也不是什么難事了。頻繁部署的價(jià)值在于,你的客戶可以快速的享用軟件的新功能,并能快速的提出反饋。這將有利于清除客戶和開發(fā)之間的障礙——我認(rèn)為這是軟件開發(fā)最大的障礙。
引入持續(xù)集成
然后你開始試著玩持續(xù)集成了,但該從何入手呢?上文中我所羅列持續(xù)集成實(shí)踐可以給你帶來太多的好處,但是你并不必在一開始就完全采用這些實(shí)踐的。
做持續(xù)集成沒有套路,主要取決于你團(tuán)隊(duì)自身的情況,但是我們發(fā)現(xiàn)以下幾點(diǎn)對(duì)于持續(xù)集成來說是比較通用的。
(1)第一步需要將構(gòu)建自動(dòng)化,并將你所需的所有東西都放在代碼管理系統(tǒng)中,以至于可以通過一個(gè)命令來構(gòu)建整個(gè)系統(tǒng)。對(duì)很多項(xiàng)目來說,這并非易事。一開始,你可以按照需要進(jìn)行構(gòu)建,或者可以只做自動(dòng)化的夜晚構(gòu)建。雖然,這些做法都不能稱為持續(xù)集成,但夜晚構(gòu)建確是一個(gè)好的起點(diǎn)。
(2)在構(gòu)建中引入一些自動(dòng)化測(cè)試,試著確定出現(xiàn)問題的主要范圍,并用自動(dòng)化測(cè)試去發(fā)現(xiàn)這些問題。對(duì)于已有的項(xiàng)目來說,很難建立起一組好的快速測(cè)試,這時(shí)你就得另尋它路了。
(3)使提交構(gòu)建快速完成。雖然好幾個(gè)小時(shí)的持續(xù)集成比沒有要好,但是如果你能將構(gòu)建時(shí)間縮短到幾十分鐘,或者就短短的10分鐘,這就再好不過了。
(4)對(duì)于新項(xiàng)目,從項(xiàng)目開始就采用持續(xù)集成。注意構(gòu)建時(shí)間,如果構(gòu)建時(shí)間違背了“10分鐘原則”,那么請(qǐng)盡快采取行動(dòng)。
(5)尋找?guī)椭矣薪?jīng)驗(yàn)的人幫助你。和其它的新技術(shù)一樣,當(dāng)不知到結(jié)果會(huì)是什么樣時(shí),很難開頭。找一個(gè)導(dǎo)師可能要花錢,但是不找的話,你所付出的代價(jià)是時(shí)間的浪費(fèi)和低下的生產(chǎn)力。(ThoughtWorks提供這樣的咨詢服務(wù),畢竟你可能遇到的問題我們之前都遇到過。)
總結(jié)
自Matt和我發(fā)布了本文的第一版之后,持續(xù)集成逐漸變成了軟件開發(fā)的主流技術(shù),在ThoughtWorks,幾乎所有的項(xiàng)目都使用到持續(xù)集成,同時(shí)我們也看到世界上其他組織也在使用持續(xù)集成技術(shù)。相比起充滿爭議的極限編程來說,持續(xù)集成很少得到負(fù)面的評(píng)論。
如果你還沒有采用持續(xù)集成,我強(qiáng)烈建議你試一試。如果你已經(jīng)采用了持續(xù)集成,本文可能會(huì)幫助你進(jìn)一步提高效率。這些年來,我們已經(jīng)學(xué)到了許多關(guān)于持續(xù)集成的知識(shí),我們也希望有更多可以學(xué)習(xí)和改進(jìn)的地方。
延伸閱讀
像本文這樣的文章通常只能涵蓋一些基本,但它卻是一種重要的話題,所以我在自己網(wǎng)站上放了一個(gè)guide page,那里你可以獲得更多的信息。
如果想了解持續(xù)集成更多的細(xì)節(jié),我建議Paul Duvall(Jolt獎(jiǎng)得主)的Continuous Integration: Improving Software Quality and Reducing Risk。對(duì)于更寬泛的持續(xù)交付,可以看看Humble 和 Dave Farley的Continuous Delivery: Reliable Software Releases through Build, Test, and Deployment Automation。
it知識(shí)庫:重溫大師經(jīng)典:Martin Fowler 的持續(xù)集成,轉(zhuǎn)載需保留來源!
鄭重聲明:本文版權(quán)歸原作者所有,轉(zhuǎn)載文章僅為傳播更多信息之目的,如作者信息標(biāo)記有誤,請(qǐng)第一時(shí)間聯(lián)系我們修改或刪除,多謝。