演進中的.NET Core基礎設施
隨著.NET Core 3 Pre 6走出家門(下稱DNC),我們覺得應該回顧下我們的構建基礎設施進化史,以及最近一年做的重大提升。
如果你對構建基礎設施有興趣,或者想看看我們是如何構建DNC這樣的龐然大物的話,這個帖子會很有意思。這篇文章不會提到新特性,也沒有什麼示例代碼,如果你還喜歡請告訴我們,我們還有幾個這種類型的帖子,但是想知道你們覺不覺得這種文章有幫助。
一個小故事
3年多以前,DNC就已經嚴重背離了微軟的傳統項目:
- 在GitHub上公開開發
- 分為單獨的git庫,而不是統統放進一個龐大單一的庫中
- 面向諸多平臺
- 它的組件可能「一心多用」(例如,Roslyn是VS的一個組件,也是它的SDK)
我們早期的基礎設施只是必要的權宜決策,用Jenkins來進行Github PR和CI驗證,因為它支持跨平臺對象存儲開發,我們的官方構建放在Azure DevOps(那時候還叫VSTS)和TeamCity(ASP.DNC用的),上面還標記(signing)並搭載著其他關鍵基礎設施。我們用手動更新包依賴和有那麼一點自動化的Github PR結合的方式,將代碼庫集中到一起,就這樣,一個需要自己打包,佈局,本地化還有處理大項目涉及到的亂七八糟工作的團隊獨立成立了。雖然不是很理想,但是在早期的日子裡,這樣也夠用了。隨著DNC從1成長到1.1又到2.0以及更高版本,我們想要弄一個更加統一的技術棧,更快的搭載速度(faster shipping cadences)和更簡單的服務。我們朝思暮想要推出一個帶有最新運行時的SDK,並且希望這一切都不會減慢這個獨立代碼庫地開發速度。
.NET Core面向獨立,分散式特性對基礎設施提出了許多挑戰,雖然這些年來變化很大,整個產品還是可以由任何地方的20-30個獨立git庫組合起來(直到最近,ASP.DNC還有很多?)。一方面,擁有許多的獨立開發筒倉(silos)往往會使這些筒倉上的開發更為高效;開發者可以在這些筒倉上快速迭代而不需要太擔心技術棧的其他部分,另一方面,它使得整個工程的創新和集成更沒有效率,蠣子如下:
- 如果我們想實施新的簽名(應為程序集簽名)和包特性,用不同的工具遍歷這麼多的獨立代碼庫很費事。
- 在技術棧之間遷移修改又慢又費事,底層技術棧(例如corefx)的修改和新特性可能幾天內無法在上層技術棧(例如SDK)中看到。如果我們在corefx裏做了個修復,這個改動必須構建,然後新版本會被送到引用了它的上層技術棧(如core setup和ASP.DNC)接著測試,提交和構建,上層棧又會將新的輸出流到更上層,直到到達最頂層。
在這些因素影響下,失敗是多層次的,進一步減慢進程,隨著DNC3計劃開始認真執行,我們發現不能在基礎設施沒有顯著變化的情況下,搞出指定範圍的正式版本。
三管齊下
我們提出了一條三管齊下的解決方案:
- 共享工具 (也就是Arcade) – 關注於我們的代碼庫之間的共享工具。
- 系統增強 (Azure DevOps) – 將我們的Github CI從Jenkins遷移到Azure DevOps,把官方構建從傳統的VSTS時期進程,轉為現代的config-as-code(代碼化配置?)。
- 自動依賴流和探測(Maestro) – 顯式跟蹤代碼庫間依賴,快速自動更新它們。
Arcade
在DNC3之前,不同的庫有三到五個依賴工具實現,(為什麼是三到五個)取決於你怎麼去數它們。
- 核心運行時庫(dotnet/coreclr, dotnet/corefx and dotnet/core-setup) 有一個dotnet/buildtools.
- ASP.DNC的庫有一個aspnet/KoreBuild
- 像dotnet/symreader等亂七八糟的庫用Repo Toolset
- 還有些孤立的庫有自己的依賴管理
當每個團隊自定義工具,只構建他們管的那一片時,就會有幾個很大的壞處:
- 開發者在庫之間流動更沒效率比如:一個程序猿從dotnet/corefx遷移到dotnet/core-sdk,庫之間的「語言」是不同的,她打算輸入什麼去構建和測試?日誌放哪?如果她想給這個庫加個新項目,怎麼做呢?(全是她,女拳警告?)
- 每個特性都需要構建N次舉例:DNC提供了堆積如山的nuget包,雖然有些不同(比如dotnet/core-setup提供的共享運行時包Microsoft.NETCore.App,和Microsoft.AspNet.WebApi.Client這種「正常」包構建是不一樣的),產出步驟卻很相似。倒黴的是,由於包的佈局,項目結構等等都不同,會在打包任務的實現上有所不同,比如代碼庫如何定義應生成哪些包,這些包中的內容,元數據等等。沒有共享工具,一個團隊更容易再造一個打包任務,而不是復用已有的輪子,浪費資源。
有了Arcade,我們努力把我們的代碼庫,代碼庫「語言」,和任務集合納入到一個通用佈局下,不過這還沒有脫離危險,任何共享工具都需要找準平衡,如果共享構建工具太過規範,任何足夠大的項目,其自定義都將變得困難,更新工具也會變難,更新很容易破壞一個代碼庫,BuildTools就深受其害。使用它的代碼庫和它耦合的太緊,不僅別的庫不能用它,對它做的任何改動都可能意外破壞用戶體驗。但如果共享工具不夠規範,代碼庫就更容易分開使用不同的工具,滾動更新需要大量的工作,這麼麻煩的話,為什麼共享構建工具要放在第一位?
Arcade同時兼顧了這兩個問題。它定義了個通用庫「語言」作為腳本集(參看這個鏈接),一個通用庫佈局,和一個通用的構建目標集合,作為MSBuild的SDK發布。完全採用了Arcade的代碼庫有了可預測的行為,在代碼庫之間做出修改更加簡單。不想這麼做的代碼庫也可以從一系列MSBuild任務包中選一個,它們也提供了基礎的功能,嘗試讓自己和其他所有庫看上去一致。我們修改構建任務時,盡了最大努力減少破壞性的變化。
來看看Arcade提供的主要特性,和它們是怎樣集成到我們的基礎設施中的:
- 通用構建任務包 – 這是個MSBuild構建任務的基礎層,既可以獨立應用,也可以作為arcade sdk的一部分……它們提供了一組通用功能,應用於大多數DNC的庫:
- 標記 Microsoft.DotNet.SignTool
- 輸出發布 (針對跨庫聚合,原文feed): Microsoft.DotNet.Build.Tasks.Feed
- 打包 Microsoft.DotNet.Build.Tasks.Packaging
- 通用的目標庫和行為 – 「Arcade SDK」作為MSBuild SDK的一部分發布. 代碼庫可選擇使用Arcade的默認構建行為,項目和實例佈局等等。
- 通用的代碼庫 『語言』 –使用依賴流,所有的Arcade庫都可以使用這組通用的腳本文件 進行同步構建(稍後詳細介紹)。這些腳本文件為應用了Arcade的代碼庫提供了一個通用的「語言」,開發者在這些代碼庫之間遷移更容易,而且,因為腳本在庫之間是同步的,應用了Arcade的代碼庫中的新改進在發布時,也可以快速將新特性和新行為引入到使用了共享工具的其他庫。
- 共享的Azure DevOps工作和步驟模板 – 雖然定義了庫「語言」的腳本主要是面向人的,不過arcade還有一組面向Azure DevopsCI的工作和步驟模板,作為一個通用構建任務包,步驟模板生成了一個可以被幾乎所有庫使用的基礎層(比如發送構建遙測),工作模板生成了更多的完成單元,可以讓代碼庫更少關注CI進程的細節。
遷移到Azure DevOps
如上所述,從2.2開始,大起來的.NET團隊用了好幾個CI系統:
- ASP.DNC的GitHub PR用的是Appveyor和Travis
- http://ASP.NET的官方構建用的TeamCity
- DNC的其他PR和發布驗證用的是Jenkins
- 傳統(非YAML)的 Azure DevOps 為所有非ASP.DNC的官方構建服務
很多差異僅僅是被迫,Azure DevOps不支持GitHub的公共PR/CI驗證,所以ASP.DNC轉向了AppVeyor和Travis來填補Jenkins的空白。傳統的Azure DevOps沒有很多構建支持,所以當DNC團隊在Azure上建了個叫PipeBuild的工具時,ASP.DNC又轉向了TeamCity,這些分歧帶來了高昂的成本,尤其在一些不是那麼顯而易見的地方:
- 雖然Jenkins很靈活,但是維護一個巨大(大約6000~8000個工作)且穩定的部署,也夠喝一壺了。
- 在傳統的Azure DevOps上構建我們自己的業務流程需要很多妥協。簽入的管道標籤(checked in pipeline job description)實際上並不是人類可讀的(它們只是導出了手動創建的構建定義的json標籤),機密管理很難看,當我們試圖處理構建要求的廣泛差異時,它們很快變得過度參數化。
- 當官方構建和日常驗證,還有PR驗證流程在不同的系統中執行時,共享邏輯就會變得困難,開發人員在進行流程更改時必須格外小心,因為崩潰是常有的事情,Jenkins的PR工作在一個特殊腳本裡面,TeamCity則有很多手動配置,AppVeyor和Travis使用它們自己的yaml文件格式, Azure DevOps有個難以理解的系統,某個PR很容易就改變構建邏輯,然後破壞官方CI構建。為了緩解這一問題,我們在官方CI和PR的構建將邏輯儘可能塞進腳本離去,但隨著時間的推移總會有差異,一些處在構建環境中的差異,基本不可能被完全移除。
- 對工作流的實際改動差別很大,經常難以理解,一個開發者針對Jenkins的netci.groovy文件的知識,並不會轉化成官方CI的PipeBuild json文件,最後結果就是構建系統知識只對幾個團隊成員有用,對.NET這個大組織沒什麼意義。
當Azure DevOps開始發布基於yaml的構建管線,支持公共GitHub工程時,DNC3也開始推進了,我們意識到這是個好機會,有了這個支持,我們可以將所有的工作流程從分立的系統中拿出來,轉移到現在的Azure DevOps,也將官方CI和PR的工作流程處理進行些改動,就從以下幾點講起:
- 處處使用yaml管線,在github上用代碼保持我們的構建邏輯
- 擁有公有/私有的項目
- 公有項目會通過GitHub代碼庫運行所有的公共CI和PR
- 私有項目會在映射到公共GitHub庫的代碼庫中運行官方CI,來容納我們自己所做出的改動
- 只有私有項目會接觸到受控的資源
- 把同一個yaml在官方CI和PR構建之間共享,使用表達式模板來區分公有和私有項目之間那些不同的行為,或者控制資源只能用於私有項目,雖然這讓全局yaml有點凌亂。這些意味著:
- 構建可能崩潰,會讓流程更慢。
- 開發者只需要改變幾處地方就可以控制CI和PR構建流程。
- 用通用任務包構建 Azure DevOps 模板,把重複的樣板yaml化簡到最小,允許用依賴流便捷的發布更新(例如遙測)。
現在,DNC3的主要代碼倉都在Azure DevOps來進行CI和PR維護了,arcade提供了官方的構建/PR流水線可作為示例。
Maestro and Dependency Flow(依賴流管理)
DNC3基礎構建設施的最後一部分就是依賴流了,這不是一個DNC專屬的內容。除非完全是自包含的應用,大多數項目都會對其他軟體暴露出一些版本參照,在DNC裡面,代表性示例就是nuget包,當我們想要實現新特性或者修改已經實現的特性時,會把項目的版本號也一併更新,當然,它的依賴包也會有版本,而依賴的依賴還有版本,如此下去就會成為一個圖表,當代碼庫拉取新版本時,依賴變動就會在圖表中「流動」。
一個複雜的依賴圖表
大多數的軟體項目的主要開發週期,只涉及到很少的互相有關的代碼庫,引入依賴一般是穩定的,更新也很稀少。當依賴需要改變的時候,通常都是手動操作。開發者自己評估引用的包版本,選擇一個適合的,然後進行更新,但是DNC明顯不是這樣的,讓組件(儘可能)彼此獨立,以不同的節奏演進,還有很好的內部循環開發體驗之類需求,導致了大量的代碼庫,和大量的互相依賴,然後形成了這樣一個複雜的依賴表: