持續集成之“依賴管理”
在前文《分支策略(續)》中,我們討論了多組件應用程序的持續集成策略,即:為相對獨立的組件創建自己專屬的代碼庫,然后通過現代持續集成工具進行組件間的持續集成。Joe的團隊在首次發布之后,開始使用這種方式。然而,沒有多久,他們就遇到了一個問題:一次提交構建所花費的時間太長。
一天,Joe就早早地來到了辦公室。因為他前一天下班前,他開發的用戶故事還有一小點就完事兒了。他想利用早上這點兒時間把它搞完,交給測試人員進行測試。他修改了某個模塊的一段代碼,在本地構建測試通過以后,就提交了, 然后起身去樓下買些早點。十五分鐘后,他回到了電腦前,令他沮喪的是,這次構建還在進行最后的階段,即所有模塊集成測試和系統級測試。他只好又起身去沖了杯咖啡。然后,一邊看著屏幕上的構建進度條,一邊喝著咖啡。七分鐘后,構建終于成功結束了。雖然這是一次成功的構建,但總是覺得不爽,花了二十多分鐘才做完提交構建。于是,他開始仔細地查看起構建腳本和構建日志。
一、一次生成,多次復用
中午吃過午飯,他把Bob和Alice叫到一起,開始討論早上他遇到的問題。
“的確是非常煩人,現在構建時間太長了。”Alice說道。
“我今天早上查看了一下我們的構建日志,發現構建時間長的原因之一是:每種測試開始之前都要更新代碼,再重新編譯一次。”Joe說道。
Bob提出了一個解決方案,并畫在了白板上。“我們是否可以建立統一的產物庫,每次構建的產物都以一定的規則放在其中?這樣,后續的測試需要使用這些二進制產物的話,直接從產物庫中獲取即可。”(如圖1所示)
“聽上去不錯。然而,我們是否需要把每次構建中產生的內容都放入產物庫,這會非常快地吃掉我們的磁盤空間。”Alice不無擔心的說。
“目前構建完成以后,所有的產物都放在那臺構建機器上。我們也遇到過因構建機器硬件問題或誤操作將所有重要歷史信息都丟失的事情。所以,我們至少需要備份。”Bob回答道,“另外,將每次構建的產物放在統一產物庫中,我們就可以解決Joe剛才提出的重復編譯問題。當然,我們需要有選擇地將重要的構建產物放到統一產物庫中,而不是所有內容。通過在每次構建后增加一個上傳任務,讓各小組將其認為有用的信息上傳到產物庫,比如構建日志、測試報告、構建后的二進制文件等。但一些臨時文件就沒有必要了。當然,這只能緩解產物庫膨脹的速度。盡管持續構建的次數非常多,但我們并不是需要一直保持所有構建的產物,所以,可以定期刪除那些沒有保留價值的構建產物,比如對那些重要構建的產物進行標記,其它的就可以刪除了。”
Alice和Joe都點了點頭,表示同意。但Joe的眉頭馬上又皺了起來。“嗯,好象這里還有點兒問題。”
“什么問題?”Alice和Bob同時問道。
Joe說道:“對于我們平臺中的一些小游戲組件來說,這沒有什么問題。因為它們的構建產物都不太大,網絡傳輸帶寬和速度都不是問題。但是,對于那些很大的二進制文件或測試數據來說,這么做的話,可能就有問題了。”大家都點了點頭,并開始思考這個問題。
忽然,Joe叫道:“不好意思,其實這不是個真正的問題。首先,我們的測試數據變化就不頻繁,原來也沒有放在產物庫中,而是放在了一個共享目錄中進行版本管理。所以,這部分在構建中的做法與之前沒有什么不同。其次,對于較大的二進制文件,只要在需要它的構建機器上把它緩存起來。那么在下一次構建時,構建腳本可以對這個本地版本進行驗證,如果版本正確且沒有被破壞(比如通過MD5驗證)就可以繼續使用。否則,就再從統一產品庫取出正確的文件將其覆蓋就行了。”
“這么做還有一個好處,而且是非常重要的好處。”Alice補充道,“我們的手工測試版本也可以從統一的產物庫中拿到,這就保證了自動化測試所有的二進制文件與部署到手工測試環境中的二進制文件是同一個文件了,也就不會出現因重新編譯時的環境不同而導致的不一致問題了。而當我們做上線部署時,也從這個統一產品庫中獲取,從而做到自編譯開始直到上線部署的二進制包的一致性啦。”
于是,Joe與團隊一起對其持續集成平臺和所有構建進行了改造,將其打造成了一個具有組織級產物庫的持續集成和發布管理平臺。他們不但有效地縮短了每次構建的時間,還可以輕松地通過產物庫追蹤到每個上線版本在代碼版本控制庫中的對應代碼,讓問題追查變得更容易了。
二、依賴管理
一個月后,根據市場的需求反饋,他們開發的一個游戲升級了,反應速度非常快,效果非常好。但引申出來的一個問題是:游戲和平臺的升級頻率不一致,持續集成應該怎么做。對于Joe的團隊來說,是一個非常大的問題,因為他們的開發流程嚴重地依賴于持續集成平臺。于是,Joe和團隊的核心成員打算討論一下,如何應對目前這種情況。
在會議室的白板前,Joe畫出了當前所用的持續集成策略(如前圖所示)。
Bob說道:“到目前為止,我們已經發布了幾次,而且最近一次只發布了一個游戲應用。我們如何管理我們的發布流程呢?在我之前工作過的公司中,產品會有幾個版本,包括穩定版本、已對外發布或即將發布的版本、最新版本:用于公司內部測試。每當將要發布新版本時,就拉出一個分支,進行內部測試,并修復嚴重的缺陷。當沒有嚴重缺陷時,才能作為穩定版本公開發布。”
Alice答道:“對于單個的軟件交付產品來說,通常可以通過“按發布拉分支” 的方式進行開發,正如我們最開始所使用的持續集成策略。但是,現在我們的游戲平臺與單個交付產品不同。我們有自己的服務器集群,只要測試覆蓋率及測試質量足夠好,測試速度足夠快,我們就可以通過小流量試驗部署后再大規模上線的方式進行發布。現在,我們的問題是由于各個游戲組件的發布頻率各不相同,組件存在依賴關系,導致很難決定在持續集成過程中,到底應該使用哪個依賴版本。尤其是我們現在還有一個公共庫,被多個組件使用。”
Joe說道:“我們先梳理一下整個平臺上的依賴關系吧。通常來說,軟件中的依賴關系通常包括編譯時依賴、測試時依賴和運行時依賴。而從依賴形式上可以分為庫依賴和組件依賴。所謂庫依賴,是指依賴于那些不受控的庫文件,比如我們使用了一些開源或者付費的的類庫文件或工具,這些庫文件的特點是更新較慢,甚至基本不需要更新。而組件依賴是指依賴于那些由自己團隊或公司內的其它團隊開發的組件,這類依賴的特點是更新頻率相對高,有些甚至非常頻繁。對于庫文件依賴,我們可以在代碼庫中建立一個目錄,叫做lib,并在其下建立build、test、run三個子目錄,把我們所依賴的庫文件放到相應的子目錄中。同時,每個庫文件的文件名中最好包含它的版本號,如nunit-2.6.0.11089.bin。這樣,就很容易看出依賴了哪些庫文件。”
Bob接道:“可惜我們不是用Java平臺,否則我們可以用象Maven或Ivy這樣的工具來管理這些外部庫依賴了。而且,同時可以在公司內部利用Artifactory或Nexus這樣的開源工具建立一個內部統一服務器,專門管理公司內部所用的這些庫依賴。”
Alice說道:“我們也可以自己做一個簡單的依賴管理系統。比如使用Key-value的格式用文本文件來描述所用到的庫文件名及版本號及存放位置,然后再寫個通用腳本讀取信息下載到本地使用。”
Bob接著問道:“對于這種庫文件的依賴管理相對容易一些。而我們面臨的重要問題好象是組件依賴管理。有什么好辦法嗎?”
Joe想了想,說道:“方法倒是有幾個,各有優缺點。一種方法是將組件依賴轉成庫依賴。其適用的場景是該組件經過一段時間的開發的維護后已趨于穩定,變化不太多。此時就可以將這個組件打包后與其它外部依賴庫放在一起,并加入正確的描述,以便依賴于它的所有組件都可以正確地拿到正確的版本。還有一種方法是我們目前所用的方法。即每個組件各自進行持續構建,然后再做集成構建。其中存在的問題是我們如何管理各組件不同版本之間的組合關系。我們一直使用的策略是無論哪次提交,都會觸發整個構建。目前要做的有兩件事:一是將公共庫獨立出來,進行單獨構建,并且一旦構建成功,自動觸發那些依賴于它的其它組件構建,最后進行集成構建。只要我們記錄每次構建后的版本及源代碼的revision就行,以便可以追蹤。二是將游戲平臺的持續構建觸發其它游戲組件的持續集成。所以,觸發關系應該是這樣的。”Joe拿起筆,在白板上重新畫了一下觸發關系圖(圖2)。
Bob搖了搖頭,說道:“這樣還是解決不了我們之前說過的問題,即我們的發布頻率不一致,如何來管理這些發布之間的關系。”
“噢,這個問題是這樣的。”Joe回答道:“我認為,我們之前單獨發布一個游戲組件是不對的。我們因市場壓力而將該游戲組件直接部署到生產環境中,盡管在發布前的評估認為,該游戲所依賴的平臺接口沒有發生變化。正確的做法有兩種:(方案A)將平臺作為一個整體一同發布,因為我們對平臺也做了修改,當時,所有的持續集成測試都是基于主干的最新版本所做的。(方案B)讓所有游戲組件依賴于游戲平臺的最新發布的穩定版本進行開發。由于平臺的新功能開發較慢,所以只要平臺接口不發生變更,各游戲應用都可以基于平臺的穩定發布版本進行快速更新。但只要某個游戲需要修改平臺的接口,就必須與平臺的最新代碼進行持續集成,并一同發布。”
Alice皺了皺眉,說道:“這么看來,對于整個軟件來說,能夠保持主干隨時可以發布才更容易管理組件依賴。因為每當需要發布時,直接做主干發布就行了。實在不行的話,只要將所有組件在同一時間點拉出一個發布分支,然后統一上線就行了。”
Bob說道:“這樣也有問題。我們的部署會很麻煩,時間可能會很長。”
Joe笑著說:“部署麻煩,我們可以通過一系統列的自動化操作來解決。部署時間長的話,我們使用的是集群部署,因此可以采用分批替換的方式來部署。但這種發布方式給我們帶來的益處是可以很快的響應市場需求。”
Joe拿起杯子喝了口咖啡,接著說道:“當然,這對我們的開發工作也提出了挑戰。我們必須使用多種手段才能做到主干持續可發布狀態。比如(1)將新功能隱蔽起來,直到它完成為止;(2)把所有的變更都變成一次次非常小的增量式修改,每個修改都做到可發布;(3)通過抽象達到分支的目的(Branch by Abstraction)。另外,我們的自動化測試也需要保持在較高的覆蓋率,并豐富其它類型的自動化測試,比如性能測試,壓力測試等。如果遇到特殊情況,我們再坐下來商量對策。”
Bob仍舊有點遲疑,“這樣可能會增加我們的開發成本。不過,可以試一下,看看效果如何。”
于是,整個團隊開始行動起來了。他們在這條道路上還會遇到什么情況呢?讓時間來回答這個問題吧。
留言列表