如何改進遺留系統的流水線問題?
瀏覽量: 次 發布日期:2023-09-07 23:26:12
如何改進遺留系統的流水線問題?
作者| 楊政權
編輯|小智
和傳統的集成方式相比,持續集成可以有效地縮短反饋周期、提高軟件質量、降低開發成本。但它并不能做到完美,總是會有新的問題出現。如何改進遺留系統的流水線問題,你是怎么想的?
寫在前面
持續集成(Continuous Integration)是一種軟件開發實踐,它倡導開發團隊頻繁地進行系統集成,每一次的集成都可以通過流水線(Pipeline)快速驗證。和傳統的集成方式相比,持續集成可以有效地縮短反饋周期、提高軟件質量、降低開發成本。這種開發實踐也越來越為更多的開發者所接受。對于一個有七年歷史的項目,非常幸運的是我們在項目剛開始就使用了持續集成,這也是我們可以長期、穩定地給客戶交付高質量軟件的保障之一,但是有時我們在項目中也經常會聽到一些這樣的聲音:
每次想提交代碼的時候都沒有機會, 我們是不是要考慮引入提交“令牌”機制,擁有“令牌”的人才能提交
這個Pipeline已經掛了這么久了,今天估計是無包可測了
這個是測試“隨機掛”,重新觸發一下Pipeline就好了
我的提交Break了Pipeline了嗎?我確認一下
….
通過分析我們發現,這些聲音背后的真相更是殘酷:
約20對Pair依賴的核心Pipeline構建時間超過1個小時,開發的反饋周期長,大量的半成品積累在本地開發環境和Pipeline上
代碼提交之后大約需要2個小時才能出包,每周平均每天可供QA測試的包數量不足1個,平均每個Story的周期時間長
上線前凍結代碼、回歸測試的時間大約需要2周左右,凍結期間產生的代碼無法集成、驗證
Pipeline不穩定測試導致某些Pipeline構建至少需要重新觸發2到3次左右
依賴關系復雜,牽一發而動全身,優化不知從何做起
有時“正確地做事情”比“做正確的事情”還要困難,在項目一開始便在項目中嘗試實施TDD等敏捷開發實踐,但是隨著項目的規模的增加,功能越來越豐富,單元測試在增加、基于UI的功能性測試也在增加,流水線的構建速度卻變得越來越慢。微服務架構具有易擴展,技術選擇靈活和部署獨立等特性,于是我們把應用拆分為不同的微服務,但是同時也帶來了流水線的數量和微服務之間集成測試的增加,Pipeline的依賴關系也變得越來越復雜。
在去年,項目中的DevOps小組,在前人的基礎上和團隊便大刀闊斧地開始了Pipeline的改進工作,希望可以通過一些必要的措施,優化流水線,保證QA每天有包可測、縮短開發期間的反饋周期。
改進什么?
這是我們在改進剛開始就面臨的一個問題,本地Pipeline數量眾多,每一個Pipeline平均有3-4個構建階段,每一個階段又有2-3個并行執行的任務,如此眾多的Pipeline和任務,應該從什么地方著手?面對Pipeline構建時間長、測試不穩定、代碼凍結時間長,依賴關系復雜等問題,應該如何決定改進的優先級?
根據高德拉特的約束理論(ToC),所有在非約束點的改進都是假象,我們可以把整個構建流水線看作是一個完整的系統,如果我們改進的是約束點的上游,就會增加約束點的負擔;如果我們改進的是約束點的下游,由于通過下游的工作量主要由約束點決定,所以任何在這個位置的改進都是徒勞無獲,無益于整個系統產出的提高還造成了浪費。
現狀問題樹是尋找約束點的方法之一,借助于這個思維過程(Thinking Process)可以幫助我們梳理“不良效果”(Undesirable Effects )之前的因果關系,最后找到需要解決的核心問題,解決了根本問題之后,由其衍生的各種“不良效果”也大多會消失。通過內部的討論和演練,我們最終把問題定位在以下幾個方面:
核心Pipeline構建時間過長
集成環境測試成功率低
缺少必要的監控預警機制
如何改進?
降低資源的占用時間
在零件制造車間中,每一個零件都需要按照既定工序通過車、鏜、銑、磨、刨等車床,每一個階段都需要占用車床資源進行特定的加工工作。和零件加工類似,來自客戶的每一個功能性需求也同樣要經過類似處理流程,從需求分析到編碼開發,從構建打包到部署測試,每一個環節都需要占用一定的資源。如果在單位時間內,資源占用的比率越高,就會產生比較嚴重的排隊現象。如下圖所示,在單位時間內如果資源占用大于百分之七十,隊列的長度也會呈指數型增長。
Pipeline作為整個軟件交付流程重要的一環,如果每次構建資源占用比例過高,會導致大量的代碼積壓在開發環境等待構建、驗證和打包,“在制品”數量也越來越多,而過多的“在制品”恰恰就是軟件交付延期的隱形殺手之一。解決對Pipeline資源占用比例過高的途徑只有一個:加速處理“在制品”的流程。在改進的過程中我們總結出了以下幾種主要的加速手段:
1.并行化
假設Pipeline單元測試需要30分鐘才能運行結束,可以通過切分單元測試多進程并發執行,如下圖所示,可以節省近20分鐘的運行時間:
同樣也可以把并行化用于優化Pipeline結構,如下圖所示,通過減少不必要的Pipeline依賴關系,讓不同的Pipeline并行執行,可以減少大約五分鐘的端到端的構建時間。
優化前: 端到端構建時間20分鐘
優化后:端到端構建時間15分鐘
2.使用Mock或者Stub,隔離真實服務
對于有數據庫依賴的單元測試,如果在運行期間連接真實的數據庫,讀寫速度會比較慢,除了IO操作之外,為了保證不同測試之間的隔離性,往往還需要考慮測試運行之后的數據清理工作,而這也會帶來一部分的性能損耗。針對這種情況,可以考慮使用內存數據庫替代真實的數據庫,在提升IO操作的同時,數據清理工作也變得很簡單。
為了保證代碼的修改沒有破壞現有的功能,一般我們會增加基于UI的回歸測試,在測試運行之前部署當前應用以及其所依賴的各個服務。對于應用依賴的服務的部署和API調用,也會消耗部分時間。這是可以考慮使用Sinatra 、Moco等工具隔離部分第三方服務,從而縮短部署時間和API的調用時間。
但是隔離真實服務的同時也掩蓋了測試替身和真實組件之間的差異性,比如我們在API測試中使用Sqlite替代sql server,但是SQLite并沒有datatime字段類型,需要在測試代碼中需要做額外的映射配置,這種差異性同樣也會導致潛在的產品缺陷。所以我們在選擇Mock或者Stub時需要權衡利弊,如果使用則需要額外的手段來驗證這種差異性。
3.優化基礎設施和運行環境
增加硬件配置如CPU,內存、替換固態硬盤等也可以一定程度地降低Pipeline的構建時間。對于由于語言或者框架本身帶來的性能約束,也可以通過升級到新版本來解決,比如把Ruby從1.8.7版本升級到2.0版本。
提高構建成功率
團隊在改進的過程中發現可以通過下面的公式大致估算出平均每天Pipeline產出的可用包的數量:
根據這個公式,如果要增加平均每天出包的數量,除了降低每次構建的時間之外還需要提高Pipeline的構建成功率,而影響構建成功率最常見的問題就是“非確定性”測試。在項目的Pipeline上曾經出現過下面這些情況:
一些UI測試每次至少需要被重新執行一次才能通過
部分單元測試在特定的時間段會穩定失敗
構建結構和測試被執行的順序有關
Martin Fowler在Eradicating Non-Determinism in Tests中指出了這種非確定性測試存在的兩個問題:首先它們屬于無用測試,由于測試本身的不確定性,它們已經無法用來描述、驗證對應的功能。測試運行的結果也無法給開發人員提供正確的反饋,如果測試失敗,開發人員無法直接判斷這個測試是由于產品缺陷導致還是由于非確定行為導致。其次這些測試就像“致命的傳染病菌”一樣,降低正常測試的存在價值。假設一個測試套件中有100個測試,其中10個測試為非確定性測試,這些非確定測試會給開發團隊帶來很多的“噪音”,團隊對于Pipeline失敗會覺得司空見慣、習以為常,剩余90個測試的作用也會大打折扣。
1.保證隔離性
在Pipeline的結構方面,由于非確定性測試的“傳染性“,在著手解決非確定性測試之前可以考慮從測試套件中隔離這種類型的測試,這種隔離一方面可以保證正常的測試可以繼續提供正確的反饋,另一方面也方便開發人員解決非確定性的測試問題,如果被隔離的測試失敗,只需要重新執行部分測試而不是整個測試套件,很大程度地縮短了修復過程中的反饋周期。
從測試代碼級別也需要保證不同測試之間的隔離性,構建的結果不應該依賴于測試被執行的順序。在優化過程中我們遇到過這樣的情況:基于UI的功能性測試依賴于一部分用戶基礎數據,其中測試T1在運行過程中需要修改特定用戶的角色,在測試T2需要使用該用戶完成其他的業務操作。如果T1在T2之前執行可以構建成功,反之則會構建失敗。
解決這類問題通常有兩種做法,測試運行之前創建不同的用戶或者測試運行結束之后恢復用戶數據。對于第二種方法,如果當前測試沒有正確地清理數據會導致下一個執行測試失敗,增加了定位問題的難度,所以更推薦使用前者來保證不同測試之前數據隔離。
2.增加必要的等待
在UI測試中很多操作都依賴于頁面元素出現的時間、位置等,在不同的網絡環境、機器性能不同,頁面的加載速度也不一樣,測試運行的結果也會有所不同。通常web driver會提供一系列的方法來幫助開發者判斷元素是否已經加載完成、是否可見、頁面是否已經加載完成等(比如Watir的when_present, wait_until_present等),在測試代碼中合適的地方使用這些方法可以讓測試代碼更加健壯,從而提升Pipeline構建的成功率。
3.正確測試異步行為
系統中的異步操作可以為用戶提供更好的使用體驗,系統不需要等待當前操作完成就可以繼續處理其他操作,但是異步操作也增加了測試的復雜度。在項目的集成測試代碼中我們發現類似這樣的等待操作:sleep 10, 這種原始的等待策略不夠穩定,對于網絡狀況、機器性能、數據量等外部因素依賴較大。回調(callback)和輪詢(loop)是兩種推薦的測試異步的方法,回調不會有任何嘗試任何多余的等待時間,但是使用場景比較有限;輪詢通用性更高但是會產生一定的多余等待時間,對于輪詢操作,建議使用更小的等待時間間隔(interval)和重試(retry)上限。
調整測試結構
不合理的測試結構也是影響Pipeline性能的重要因素,根據測試金字塔理論,就測試數量來說,從低層級到高層級的測試需要保證金字塔狀的結構。測試的運行時間呈現的卻是一個倒金字塔狀,測試的層級越高測試運行的時間越長,對應Pipeline的構建時間也越長。所以改進Pipeline也可以從調整測試層級結構開始。
1.梳理業務流程,簡化測試結構
新的功能在不斷增加,已有的需求也不斷在變動,產品本身也不斷接受來自最終用戶和市場的反饋,現有的測試有可能并沒有覆蓋那些最有價值的場景而已經覆蓋的場景也許在真正的產品環境下使用率很低;有些場景已經在低層級的單元測試覆蓋,在高層級測試中出現了很多重復的用例。
調整測試結構可以和領域專家一起,重新梳理業務流程,把測試的重點放在那些最有價值的業務場景上,在高層級增加適當的UI測試保證核心功能沒有被破壞。對于出現重復或者價值不大的測試,可以考慮刪除高層級的測試,用更多的單元測試來替代,從而降低測試的運行時間。關于自動化測試更多的優化手段可以參考一個遺留系統自動化測試的七年之癢。
除此之外還需要構建有效的反饋回路,通過Google Analytics等網站分析平臺收集來自于最終用戶和市場的數據、用戶使用習慣、時區語言等地域性信息對應地調整現有的測試結構,讓測試環境下的業務場景更加接近真實的產品環境。
2.用契約測試替代集成測試
Integrated tests are a scam. A self replicating virus that threatens the very health of your codebase, your sanity, and I’m not exaggerating when I say, your life.” - JB Rainsberger
JB Rainsberger的這個說法一點也不夸張,在項目中總是有關于集成測試的各種“吐槽”,構建時間慢、問題難以復現和定位、修復難以驗證、不穩定等。而契約測試就是那個可以拯救你,讓你脫離“苦海”的利器。
契約測試是“單元級別”的集成測試,基于消費者驅動的契約測試把契約測試分為了兩個階段:消費者(Consumer)生成契約和提供方(Provider)驗證契約,在生成契約時通過Mock隔離真實的服務提供方,運行單元測試生成用JSON描述的契約文件;服務端驗證只需要部署自身就可以驗證契約文件的正確性。
契約測試有很多優點,首先它不依賴于完整的集成環境,部署成功率高,其實在測試運行期間無真實的API調用和模擬的UI操作,測試運行的速度快,成功率高;而且在本地開發環境就可以驗證契約測試,問題容易定位,修復的反饋周期短。引入契約測試不但給Pipeline性能帶來大幅度的改善,還可以提升整個團隊的開發效率。
如何保護改進成果
Pipeline是軟件交付的命脈,為了保證每一個功能需求可以長期穩定、源源不斷地通過,在優化過程中我們引入了關于Pipeline性能的監控機制,我們基于ThoughtWorks的開源產品GoCD 提供的API開發了一個監控工具,每一次構建之后可以自動統計該次構建的時間和成功率,如果超過這兩個指標超過了閾值,則讓該次構建失敗,提醒代碼提交者檢查是否引入了影響Pipeline性能的變更,避免性能的進一步惡化。考慮到網絡和機器性能的問題,在設置實際的閾值的時候可以稍大于期望值。
我們還構建了基于郵件通知的預警機制,每個工作日的下午發送出包數量通知,提醒團隊解決影響出包的問題,同時我們把Pipeline的性能可視化并納入每周的周報中。
寫在最后
優化Pipeline除了Pipeline結構、測試策略和監控可視化手段之外,還需要關注軟件架構和團隊組織結構,下面是我們項目架構的局部依賴圖:
核心的微服務OrderAPI依賴復雜,和RatingSrv之間甚至出現了雙向依賴,領域上下文(Bounded Context)的不合理切分使得業務邏輯散落在不同的服務之間,不管我們增加集成測試還是契約測試,這種依賴關系也同樣會體現在Pipeline之間的依賴關系上,增加Pipeline的復雜度。
在ThoughtWorks技術雷達上A single CI instance for all teams目前處于Hold狀態,在一個組織中多個團隊共享一個臃腫的CI會導致很多的問題,比如上文中提到的構建隊列過長,構建時間長等,一旦這個共享的Pipeline出現問題會造成多個團隊工作 的中斷。技術雷達建議在具有多團隊的組織中由各個團隊分布式地管理自己獨立的CI。這種分布式的CI同樣也依賴于整潔的軟件架構和與之相契合的團隊組織形式。
在項目的DevOps小組解散之后,我們成立的項目內部的DevOps Community,以保證產品交付為目標,同時肩負著項目提高內部DevOps技能的職責。項目內部的成員有跨多個開發團隊的不同角色組成,DevOps community產生的相關Task,最后都會分配到不同的開發團隊中。DevOps是一種文化而不應該是一個單獨的小組,DevOps主旨在于構建整個團隊中的責任共享的文化,改進現有的流水線是每一個開發團隊都需要具有的技能之一。
今日薦文
點擊下方圖片即可閱讀