译 | .NET Core 基础架构进化之路(二)
原文:Matt Mitchell
翻譯:Edi Wang
(接上篇 譯 | .NET Core 基礎架構進化之路(一))
Maestro 及依賴流
.NET Core 3.0 基礎結構難題的最后一部分就是我們所說的依賴項流。這不是 .NET Core 的唯一概念。除非它們是完全獨立的,否則大多數軟件項目都包含某種對其他軟件的版本化引用。在 .NET Core 中,這些通常表示為 NuGet 包。當我們想要庫提供的新功能或修補程序時,我們會通過更新項目中引用的版本號來提取這些新更新。當然,這些包也可能具有對其他包的版本化引用,這些其他包可能具有更多的引用,依此類推。這將創建一個圖(graph)。當每個倉庫都拉取其輸入依賴項的新版本時,更改會流過此圖。
一個復雜圖
大多數軟件項目的主要開發生命周期(開發人員經常處理的)通常涉及少量相互關聯的倉庫。輸入依賴項通常穩定,更新是稀疏的。當他們確實需要更改時,它通常是手動操作。開發人員評估輸入包的可用版本,選擇適當的版本,并提交更新。.NET Core 中不是這樣。組件需要獨立,以不同的節奏提供,并且具有高效的內循環開發經驗,這導致了大量具有大量相互依賴的存儲庫。相互依賴性還形成了一個相當深的圖:
dotnet/core-sdk 倉庫充當所有子組件的聚合點。我們提供一個特定的 dotnet/core-sdk 編譯版本,它描述了所有其他引用的組件。
我們還期望新的輸出能快速通過此圖,以便盡可能頻繁地驗證最終產品。例如,我們期望ASP.NET Core 或 .NET Core 運行時的最新版本盡可能經常在 SDK 中表示自己。這實質上意味著以常規的快速節奏更新每個倉庫中的依賴項。在足夠大的圖(如 .NET Core)中,這很快成為手動執行的不可能完成的任務。這種大小的軟件項目可能會通過多種方式來解決:
自動浮動輸入版本
在此模型中,dotnet/core-sdk 可能引用 Microsoft.NETCore.App,這是 dotnet/core-setup 生成的,允許 NuGet 浮動到最新的預發行版本。雖然這行得通,但它也有重大的缺點。編譯變得非確定性。簽出較舊的 git SHA 和編譯不一定使用相同的輸入或生成相同的輸出。重現錯誤變得困難。在 dotnet/core-setup 中,一個糟糕的提交可能會破壞任何在 PR 和 CI 檢查之外拉取其輸出的倉庫。編譯的編排成為一項主要任務,因為生成中的獨立計算機可能會在不同的時間還原包,從而產生不同的輸入。所有這些問題都是"可以解決的",但需要巨大的投資和不必要的基礎設施復雜性。
"組合"編譯
在此模型中,使用每個輸入存儲庫中的最新 git SHA,以依賴項順序同時生成整個圖。生成每個階段的輸出將用于下一階段。倉庫有效地將其輸入依賴項版本號覆蓋其輸入階段。在成功編譯結束時,將發布輸出,并且所有倉庫都更新其輸入依賴項,以匹配剛剛編譯的內容。與自動浮動版本號相比,這稍有改進,因為單個存儲庫版本不會因其他存儲庫中的不良簽入而被爆,但它仍然有主要缺點。突發更改幾乎不可能在倉庫之間有效地流動,并且重現失敗仍然是有問題的,因為存儲庫中的源通常與實際構建的內容不匹配(因為輸入版本被覆蓋在源代碼管理)。
自動依賴項流
在此模型中,外部基礎結構用于在存儲庫之間以確定性、驗證方式自動更新依賴項。存儲庫在源中顯式聲明其輸入依賴項和相關版本,并"訂閱"來自其他倉庫的更新。新的編譯完成時,系統將查找匹配的訂閱,更新任何聲明的輸入依賴項,并打開具有更改的 PR。此方法提高了可重復性、對重大更改進行流式操作的能力,并允許存儲庫所有者控制更新的完成方式。缺點是,它比其他兩種方法中的任何一個都慢得多。更改只能以沿流路徑每個存儲庫中的 PR 和官方 CI 時間總和的速度從棧底部流向頂部。
.NET Core 已嘗試所有 3 種方法。我們在 1.x 的早期用了浮動版本,在 2.0 中進行了某種程度的自動依賴項流,并用在了 2.1 和 2.2 的組成版本。有了3.0,我們決定在自動化依賴項流上投入大量資金,并放棄其他方法。我們希望通過一些重要的方式改進以前的 2.0 基礎架構:
簡化產品實際內容的可追溯性
在任何給定的倉庫中,通常可以確定哪些組件的版本用作輸入,但幾乎總是很難確定這些組件的構建位置、這些組件來自哪些 git SHA、它們之間的輸入依賴關系等等。
減少所需的人工操作
大多數依賴項更新都是普通的。在更新 PR 通過驗證以加快流程時自動合并它們。
使依賴項流信息與倉庫狀態分開
倉庫應僅包含有關其節點在依賴關系圖中的當前狀態的信息。它們不應包含有關轉換的信息,例如何時應進行更新、從中提取哪些來源等。
基于"意圖"而不是分支的流依賴項
因為 .NET Core 由相當多的半自治團隊組成,具有不同的分支理念、不同的組件發貨節奏等,因此不使用分支作為意圖的代理。團隊應該根據這些輸入的用途(而不是它們來自何處)定義他們拉入存儲庫的新依賴項。此外,這些投入的目的應由這些投入的小組宣布。
"意圖"應從編譯時推遲
為了提高靈活性,請避免在生成完成之前分配生成的意圖,從而允許聲明多個意圖。在生成時,輸出只是一個在一些 git SHA 上構建的位桶。就像在 Azure DevOps 生成的輸出上運行發布管道一樣,它實質上為輸出分配了目的,在依賴項流系統中分配生成意圖開始基于意圖的流動依賴項過程。
考慮到這些目標,我們創建了一個名為 Maestro++ 的服務和一種稱為"darc"的工具來處理我們的依賴項流。Maestro++ 處理數據以及依賴項的自動移動,而 darc 為 Maestro++ 提供了人機界面以及了解整個產品依賴狀態的窗口。依賴項流基于 4 個主要概念:依賴項信息、編譯、通道和訂閱。
編譯、通道和訂閱
依賴項信息
在每個倉庫中,都有倉庫的輸入依賴項的聲明,以及eng/Version.Details中有關這些輸入依賴項的源信息。讀取此文件,然后傳遞每個輸入依賴項的倉庫+sha 組合生成產品依賴關系圖。
編譯
編譯只是 Azure DevOps 內部構建中的 Maestro+ 視圖。生成標識倉庫+sha、總版本號以及從編譯生成的完整資源集及其位置(例如 NuGet 包、zip 文件、安裝程序等)。
通道
通道表示意圖。將通道視為跨倉庫分支可能很有用。可以將生成分配給一個或多個通道,以將意圖分配給輸出。通道可以與一個或多個釋放管道關聯。將生成分配給通道將激活發布管道并導致發布發生。根據發布發布活動更新生成的資源位置。
訂閱
訂閱表示轉換。它將放置在特定通道上的編譯的輸出映射到另一個倉庫的分支上,并提供有關何時進行這些轉換的其他信息。
這些概念的設計使倉庫所有者不需要棧或其他團隊進度的全局知識,以便參與依賴項流。他們基本上只需要知道三件事:
它們所做的編譯的意圖(如果有),以便可以分配通道。
它們的輸入依賴項及其產生的倉庫。
他們希望從哪些渠道更新這些依賴項。
例如,假設我擁有 dotnet/core-setup 存儲庫。我知道我的主分支為日常 .NET Core 3.0 開發編譯二進制文件。我想將新編譯分配給預先聲明的".NET Core 3.0 開發"通道。我也知道,我有幾個 dotnet/coreclr 和 dotnet/corefx 包輸入。我不需要知道他們是如何編譯的,也不是從什么分支編譯的。我需要知道的是,我希望每天從'.NET Core 3.0開發'通道的最新dotnet/coreclr 輸入,以及來自'.NET Core 3.0開發'通道的最新dotnet/corefx 輸入,每當它們出現時。
首先,我添加一個 eng/Version.Details 文件。然后,我使用"darc"工具確保主分支上倉庫的每個新生成默認分配給".NET Core 3.0 開發"通道。接下來,我將訂閱設置為從 .NET Core 3.0 開發中提取輸入,用于網dotnet/corefx, dotnet/coreclr, dotnet/standard 等的編譯。這些訂閱具有節奏和自動合并策略(例如,每周或每個生成)。
激活每個訂閱的觸發器時,Maestro++ 會根據與新生成的輸出相交聲明的依賴項更新核心設置回購中的文件(eng/version.Details.xml、eng/version.props 和其他一些文件)。它將打開 PR,一旦滿足配置的檢查,將自動合并 PR。
這反過來在主分支上生成新的核心設置編譯。完成后,將自動將編譯分配給".NET Core 3.0 開發"通道。".NET Core 3.0 開發"通道具有關聯的發布管道,用于將構建的輸出偽影(例如包和符號文件)推送到一組目標位置。由于此通道適用于日常公共開發編譯,因此包和符號將推送到不同的公共位置。發布管道完成后,將完成通道分配,并觸發在此事件上激活的任何訂閱。隨著更多組件的添加,我們構建了一個完整流圖,表示倉庫之間的所有自動流。
.NET Core 3 開發通道的流圖,包括有助于 .NET Core 3 開發流的其他通道(例如 Arcade 的".NET 最新工具")。
一致性和不協調性
.NET Core 依賴關系圖狀態的可見性增加,這突出說明了一個現有問題:當在圖中的各個節點引用同一組件的多個版本時,會發生什么情況?.NET Core 依賴關系圖中的每個節點可能會將依賴項流到多個其他節點。例如,由dotnet/core-setup 產生的 Microsoft.NETCore.App 依賴項流向 dotnet/toolset, dotnet/core-sdk, aspnet/extensions 和許多其他位置。由于拉取請求驗證時間的變化、需要對重大更改做出反應以及所需的訂閱更新頻率,此依賴項的更新將在每個位置以不同的速率提交。當這些倉庫流向其他位置并最終在 dotnet/core-sdk 下合并時,可能有許多不同的 Microsoft.NETCore.App 版本在整個圖形中被反向引用。這稱為"不協調"。當在整個依賴關系圖中僅引用每個產品依賴項的單個版本時,該圖是符合邏輯的。如果可能的話,我們總是努力提供一個連貫的產品。
不協調會導致哪些問題?
不協調表示可能的錯誤狀態。例如,我們來看看 Microsoft.NETCore.App。此包表示特定的 API 層面。雖然可以在倉庫依賴關系圖中引用多個版本的 Microsoft.NETCore.App,但 SDK 只附帶一個版本。此運行時必須滿足可能在此運行時上執行的傳遞引用組件(例如 WinForms 和 WPF)的所有要求。如果運行時不能滿足這些要求(例如,爆破式 API 更改),則可能會發生故障。在不連貫的圖中,由于所有存儲庫均未引入同一版本的 Microsoft.NETCore.App,因此有可能錯過重大更改。
這是否意味著不協調總是錯誤狀態?
不。例如,假設圖中的 Microsoft.NETCore.App 的不協調性僅表示 coreclr 中的單個更改,即單個不會爆的 JIT Bug 修復。從技術上講,在圖表中的每個點都不需要引入新的 Microsoft.NETCore.App。簡單地將相同的組件與新的運行時進行發布就足夠了。
如果不協調只是偶爾重要,為什么我們努力提供一個連貫的產品?
因為確定何時不協調并不重要是很難的。簡單地將一致性作為所需狀態的運來比嘗試理解不相干組件之間對已完成產品的任何語義影響差異更容易。它可以完成,但在構建的基礎上,它是耗時密集型的,容易出錯。將一致性強制為默認狀態更安全。
依賴流的干貨
所有這些自動化和跟蹤都有大量的優勢,隨著倉庫圖的增大,這些優勢變得顯而易見。它為解決我們每天的實際問題開辟了許多可能性。雖然我們剛剛開始探索這一領域,但系統可以開始回答有趣的問題并處理以下情況:
dotnet/core-sdk 的 git SHA A 和 SHA B 之間發生了哪些"真正的"變化? 通過 Version.Details.xml 文件來構建完整的依賴關系圖,我可以識別圖中發生的非依賴項更改。
修復需要多長時間才能在產品中出現? 通過組合存儲庫流圖和每個存儲庫遙測數據,我們可以估計在圖中將修復程序從存儲庫 A 移動到存儲庫 B 需要多長時間。這在發布后期特別有價值,因為它有助于我們在查看是否進行特定更改時做出更準確的成本/收益估計。例如:我們是否有足夠的時間來進行此修復并完成方案測試?
core-sdk 及其所有輸入編譯生成的所有文件的位置是什么??
在服務版本中,我們希望采取特定的修復,但暫緩其他。通道可以放置在允許特定修復程序自動流經圖的模式下,但其他修復程序被阻止或需要批準。
下一步是什么?
隨著 .NET Core 3.0 逐漸落地,我們正在尋找需要改進的新領域。雖然規劃仍處于(非常)早期階段,但我們預計在一些關鍵領域進行投資:
縮短將修復程序轉換為可發布、連貫產品的時間 – 依賴關系圖中的躍點數量非常重要。這允許存儲庫在其進程中具有很大的自治性,但會增加我們的端到端"構建"時間,因為每個躍點都需要提交和正式編譯。我們希望顯著縮短端到端時間。
改進我們的基礎架構遙測 — 如果我們能夠更好地跟蹤失敗的位置、資源使用情況、依賴狀態的表現等,我們可以更好地確定我們的投資需要哪些地方才能提供更好的產品。在 .NET Core 3.0 中,我們朝這個方向邁出了一小步,但我們還有很長的路要走。
多年來,我們不斷發展我們的基礎設施。從 Jenkins 到 Azure DevOps,從手動依賴項流到 Maestro++,從許多工具實現到一個工具,我們對提供 .NET Core 3.0 所做的更改是向前邁出的一大步。我們準備開發并出一種比以往更加可靠的更令人興奮的產品。
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的译 | .NET Core 基础架构进化之路(二)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .NET开发框架(一)-框架介绍与视频演
- 下一篇: 在.Net Core中实现一个WebSo