持續集成之戲說Check-in Dance
盡管Thoughtworks的首席科學家Martion folwer 為“持續集成 ”下了定義,但由于自身背景與經歷的不同,每個人對其都有不同的理解。從狹義上講,持續集成可以認為是一種基于某種或者某些變化對軟件系統進行的經常性的構建活動(注:這里的構建活動不僅指編譯打包工作,還包含各類自動化測試、部署及發布活動)。然而,它忽視了一點,即:任何實踐中都應該包含“與人的交互”這一因素。因此,從廣意上講,持續集成應該是軟件開發團隊在上述活動的約束下所采用的整個開發流程及活動。它強調開發團隊與持續集成系統之間的互動性。我們既見過持續集成做得非常成功的團隊,也見過效果不佳的持續集成,甚至失敗的案例。
那么,到底如何從持續集成中得到最大的收益呢?這要從持續集成所涉及的諸多方面進行分析,并根據團隊具體情況(比如團隊規模、人員組成以及是否為分布式團隊 等)及所開發軟件自身的特點(是企業應用軟件,還是中間件?是嵌入式軟件,還是互聯網產品等)制定實踐策略與實現步驟。本專欄將與大家共同探討與持續集成、持續部署及持續交付相關的方法、工具與經驗。作者本人在Thoughtworks公司曾參與的一款持續集成與發布管理產品Go的交付和對外咨詢服務為專欄提供了很有素材,同時感謝肖鵬 、 李彥輝 、胡凱 、李劍等對欄目內容的支持和幫助。
在軟件開發中,持續集成實踐能夠解決的問題是盡早的集成和盡早的反饋。因此,盡管目前流行的所有版本控制工具都提供了分支/合并功能,但在少于20人的團隊中實現持續集成的話,推薦使用Single Branch開發策略。這樣會減少多分支在合并時的開銷。另外,由于理想情況下,每個分支都需要有專屬的持續集成環境(包括持續集成服務器、構建環境和測試環境等),所以Single Branch也減少了對持續集成環境的需求量(當編譯時間較長或測試用例較多時,這個因素的影響尤其重要)。
當團隊完成最初搭建持續集成服務器,編寫好一鍵式編譯和測試腳本工作后,就需要考慮如何利用持續集成環境高效地進行團隊協作開發了。一定有人會問:
“多人同時在一個分支上開發的話,每個人提交時都要合并代碼,不是更浪費時間嗎?”
這個問題也正是持續集成期望解決的問題。每當開發人員提交代碼時,就是其與其他開發人員工作成果的一次集成。如果每個人都能夠頻繁提交代碼,那么代碼集成的頻率就會提高,在持續集成的有力支持下,代碼中潛在的問題就會更早地暴露出來(比如代碼編譯鏈接問題,自動化測試失敗反映出來的代碼功能問題,或需求理解不一致等問題),以便團隊盡早解決之。
當然,持續集成所鼓勵的頻繁提交并不是指那種僅將版本控制庫當成備份工具,無約束的“隨意”提交,還需要團隊開發流程約束的。下面我們來一同探討“持續集成環境中的團隊開發流程是什么樣的”。
讓我們先設想一個軟件開發場景。
一、使用版本管理工具做備份
故事的主人公叫Joe,他打算寫一個游戲,所以用Subversion建立了一個版本控制庫用于保存代碼,然后就動手寫代碼了。Joe的開發流程是這樣的。
- 從代碼庫中簽出一份代碼;
- 為增加某個功能修改一些代碼;
- 在本地運行了一下自動化測試;
- 測試通過之后,提交代碼到版本控制庫;
- 重復前面的步驟。
如圖1所示。
二、搭建持續集成服務器做自動構建
“每次在本地手工運行自動化測試太麻煩了,”Joe想到,“這種重復的工作為什么不讓機器來做呢”。
于是,Joe上網查了一下,發現持續集成工具是做這個事情的,就找來一臺舊機器,用CruiseControl搭建了一個持續集成服務器。他的開發流程也變為:
- 從代碼庫中簽出一份代碼;
- 開發新功能或修改bug;
- 提交到版本控制庫,思考下一個功能的實現;
- 持續集成服務器運行自動化構建和測試;
- 如果測試通過,轉到步驟(1);
- 如果測試沒有通過,轉到步驟(2)。如圖2所示。
三、多人并行開發
兩周后,游戲初見原型,Joe向他的幾個朋友介紹了他的游戲創建,他們都非常喜歡,因此也加入了游戲開發。麻煩很快就出現了。持續集成服務器中構建結果經常失敗,所以每次簽出代碼后都要做問題清理工作。于是,Job與朋友們坐下來討論如何解決這個問題。
Alice說:“我們每個人都拉一個獨立分支,當每個人的功能開發完成以后,再合并到一起不就行了嗎?”
Joe不同意這樣的做法。“游戲的需求還不明晰,要經常合在一起看一下效果。所以還是在同一個分支上開發吧。下面,我們討論一下如何讓這種失敗少一些吧。”
于是,他們花了點兒時間,發現有兩個主要原因導致失敗。
- 本地代碼有問題,原本就編譯不了或會導致測試失敗,但還是提交了;
- 開始做新功能時,沒有特別注意分支上的持續集成狀態,直接將主分支上的代碼直接就與本地代碼合并了。
Joe提出,開發流程應該變成如圖3所示:
- 每個人在開發新代碼之前,只能從持續集成已成功的那個最新版本簽出代碼;
- 開發新功能或修改bug;
- 提交前將主分支上的代碼再次取到本地合并;
- 運行本地測試,確保測試可以通過;
- 提交代碼到主分支,由持續集成服務器再次運行測試。
- 如果測試通過,轉到步驟(1);
- 如果測試沒有通過,轉到步驟(2),直到修復持續集成上的構建。
可是,Alice提出反對意見。她認為:“既然本地已經運行了測試,為什么還要在持續集成服務器上再次運行呢?”
Joe解釋到:“主要是因為我們每個人的本地環境都不完全相同,很可能出現‘它在我的機器沒有問題呀’的這個現象,所以還是要在獨立的持續集成服務器上再運行一次。”
因此,大家就這么決定了。
四、兩次本地構建的目的
四周后的一天,Joe花了很長時間完成了某個新功能后,打算提交了。于是他把分支當前的代碼與其本地代碼進行了一次合并。然后運行了本地測試,但測試失敗了。他用了很長時間來定位該問題是在他自己修改的功能里,還是在被合入的代碼中。這讓他對提交流程進行了反思。
“要是在合入他人代碼之前,能夠先運行一次本地測試,驗證一下我的代碼沒問題就好了,反正本地測試所花的時間也不長。”
于是,他把這個想法告訴了其他人,最后大部分人都同意這么做。于是,其提交流程就變成了這樣:?
- 每個人在開發新代碼之前,只能從持續集成完全成功的那個最新版本簽出代碼;
- 開發新功能或修改bug;
- 運行本地測試,如果有失敗就立即修復,直至測試成本;
- 提交前將主分支上的代碼再次取到本地合并;
- 運行本地測試,確保測試可以通過;
- 提交代碼到主分支,由持續集成服務器再次運行測試。
- 如果測試通過,轉到步驟(1);
- 如果測試沒有通過,轉到步驟(2)。
這個過程就被稱為“Check-in Dance”。
Alice還說道:“我們在從主分支上簽出代碼時,一定是那個通過持續集成驗證的最新版本。這樣可以避免簽出的代碼就是有問題的,而影響自己本地的代碼。”整個過程如圖4所示。
五、持續集成令牌
過了幾天,有人把大家叫到了一起,這次是Alice。她說:
“我今天遇到一個問題。我提交代碼之后,正等著持續集成服務器返回結果呢,Bob就提交代碼了。幸好我提交的代碼通過了測試,否則的話,我就要在Bob的代碼之上修復啦。所以,我建議我們需要設立一個提交令牌,只有拿到這個提交令牌的人才能提交。也就是說,當一個人做完本地測試之后,去拿這個令牌。拿到之后,再進行代碼合并、本地測試和提交。提交以后當持續集成服務器返回成功通過的結果時,才能交還令牌。這樣就不會出現我和Bob這種情況了。”
可Bob并不同意這樣的做法,“這次沒有出什么問題,為什么還要這么做呢?”
此時,Joe把話接了過來,說道:“Alice的這個建議很好,我已經遇上過一次這樣的事情了,那次測試失敗以后,我花了很長時間才發現問題并不在我的提交中,而是在Mary的提交中。我把它修復后,又做了一次提交。”由于大多數人都同意這么做,因此團隊決定試一試。因為目前測試運行時間很短,所以提交和集成工作沒有遇到什么瓶頸。提交流程如圖5所示。
似乎事情到這里就結束了。然而,這個游戲被某投資公司看中,決定做更大的投入,招更多的開發人員,讓它成為一個開放游戲平臺。那么,接下來Joe與他的朋友們還會遇到哪些問題呢?