Rush Stack商店部落格活動
跳至主要內容

協同建置 (實驗性)

Rush 的「協同建置」功能 (cooperative builds) 提供一個輕量級的解決方案,用於將工作分散到多部機器上。其概念是您已經在做的事情的簡單延伸:只需在不同的機器上產生相同 CI 管線的多個執行個體,讓它們透過 Rush 的建置快取來共用工作。

例如,假設您的作業執行 rush install && rush build,而我們在兩部機器上啟動此指令。如果機器 #1 已經建置一個專案,則機器 #2 將會跳過該專案,而是從建置快取中擷取結果。如此一來,建置工作會在兩個管線之間分配,而且在完美平行化的情況下,建置可能會在一半的時間內完成。

但這個概念有一個缺點:如果機器 #2 存取到機器 #1 已經開始建置但尚未完成的專案,該怎麼辦?這個快取遺失會導致機器 #2 開始建置相同的專案,但等待機器 #1 完成該專案時,可能最好去處理其他事情。我們可以透過使用簡單的鍵/值儲存來傳達機器之間的進度,以此解決這個問題。(在本教學中,我們將使用 Rush 的Redis提供者,但如果您的公司已經託管其他服務,例如Memcached,則實作您自己的提供者相當容易。)

何時使用協同建置?

在沒有協同建置的情況下,Rush 已經可以在單一機器上平行化您的作業。(這可能不是立即顯而易見的,因為 Rush 的輸出會為了易於閱讀而「整理」,使其看起來好像專案是一個接一個地建置。) 您可以使用 --parallelism 命令列參數微調最大平行化,但請記住,專案只能在彼此不依賴的情況下同時建置。因此,只有在您已經達到單一機器的限制 (考慮 CPU 核心、磁碟 I/O 速率和可用記憶體) 時,協同建置才會有幫助。而且只有在您的單一儲存庫的專案依賴圖實際上可以進一步平行化的情況下。

協同建置功能會在假設機器隨時可用的情況下啟動 CI 管線的多個執行個體。例如,如果您的協同建置配置 4 部機器,而您的機器集區有 40 部機器,那麼在佇列中有 10 個提取要求之前,集區爭用不會成為問題。相反地,一個極大型的單一儲存庫可能需要數千部機器,此時使用「建置加速器」(例如BuildXL) 而不是協同建置會更有意義。(也有計劃將 Rush 與bazel-buildfarm整合;Bazel 是 Google 等同於 BuildXL 的工具。) 建置加速器通常會要求您使用它們的集中式作業排程器來取代您的 CI 系統,該排程器管理自己的專屬機器集區。這類系統需要大量的維護,而且學習曲線可能較陡峭,因此我們通常建議從協同建置開始。

在採用協同建置之前,我們建議先考慮更簡單的解決方案

  1. 啟用建置快取建置快取是協同建置的先決條件。

  2. 識別瓶頸:如果您的單一儲存庫的依賴圖實際上不允許平行建置許多專案,則必須先修正此問題,然後再考慮分散式建置。您可以使用 Rush 的 --timeline 參數來識別導致太多專案在開始建置之前必須等待的瓶頸。這些瓶頸可以透過以下方式解決

    • 消除專案之間不必要的依賴
    • 引入Rush 階段,將建置步驟分解為多個作業
    • 重構程式碼,將大型專案分解為較小的專案
  3. 升級您的硬體:如果您的建置速度緩慢,增加更多機器可能會有所幫助。我們通常建議根據 rush installrush build 的典型行為,為您的方案選擇具有最大 RAM 和 CPU 核心的高階硬體。但每個單一儲存庫都不同,因此請收集不同硬體組態的基準,以為您的決策提供資訊。加速建置可以讓每個人都更有生產力;但是,由於硬體升級通常來自與工程師薪資不同的預算,因此管理階層有時可能需要一些幫助才能看到這種關聯。

  4. 在執行之間快取狀態:CI 機器通常會使用完全乾淨的機器映像啟動 rush install && rush build。快取可以改善這一點,例如,可以使用 RUSH_PNPM_STORE_PATH 環境變數將 PNPM 儲存重新定位到 CI 系統可以在執行之間儲存和還原的位置,藉此改善 rush install 時間。某些環境允許重複使用相同的機器來執行多個作業,以便保留其他 Rush 快取。

  5. 考慮使用合併佇列:如果有兩個提取要求等待合併,CI 系統通常會建置 pr1+mainpr2+main 的熱合併,以確保每個 PR 分支都使用最新的 main 進行測試。但是,在合併 pr1+main 之後,我們通常不會強制使用新的 main 來重做 pr2+main;這種缺乏安全性偶爾會導致建置中斷。(例如,假設 pr1 刪除了一個 API,但 pr2 新增了對該 API 的另一個呼叫。)「合併佇列」(也稱為「提交佇列」) 會改為建置 pr1+mainpr1+pr2+main,以此提高安全性;如果第一個 PR 失敗,則會使用 pr2+main 重試。進階合併佇列支援「批次」,它們會直接測試提取要求的「列車」pr1+pr2+main,而且只有在發生失敗時才會測試 pr1+main。這可以加速建置和/或減少機器爭用,同時仍然保證安全性。在撰寫本文時,GitHub 的合併佇列不支援批次,但是Mergify第三方服務實作了批次,而且已使用 Rush 進行測試。

必要條件

若要使用協同建置功能,您將需要

  • 已啟用 Rush 建置快取和雲端儲存提供者。

  • Redis 伺服器。如果您的公司使用其他鍵/值服務,您可以依照rush-redis-cobuild-plugin的範例實作外掛程式。(並考慮將其貢獻回 Rush Stack!)

  • 一個 CI 系統,能夠在觸發 CI 管線時配置多部機器。例如,使用 GitHub Actions 時,「工作流程」可以啟動多個「作業」,其「執行器」是不同的機器。使用 Azure DevOps 時,「管線」可以在多個「代理程式」上執行作業,而這些代理程式可以位於不同的機器上。

  • 建議使用Rush 階段來增加平行化,但並非協同建置的必要條件。

啟用協同建置功能

  1. rush.json 中的 rushVersion 升級至 5.104.1 或更新版本。

  2. 為 Rush 外掛程式建立自動安裝程式

    rush init-autoinstaller --name cobuild-plugin

    使用現有的自動安裝程式也沒問題。如需關於 Rush 外掛程式和自動安裝程式的詳細資訊,請參閱使用 Rush 外掛程式自動安裝程式

  3. @rushstack/rush-redis-cobuild-plugin 外掛程式新增至自動安裝程式。(在本教學中,我們將使用 Redis。)

    common/autoinstallers/cobuild-plugin/package.json

    {
    "name": "cobuild-plugin",
    "version": "1.0.0",
    "private": true,
    "dependencies": {
    "@rushstack/rush-redis-cobuild-plugin": "5.104.0"
    }
    }

    👉 重要:

    隨著時間推移,請務必保持 @rushstack/rush-redis-cobuild-plugin 的版本與 rush.json 中的 rushVersion 同步。

  4. 更新自動安裝程式的鎖定檔

    rush update-autoinstaller --name cobuild-plugin

    # Remember to commit the updated pnpm-lock.yaml file to git
  5. 接下來,我們需要更新 rush-plugins.json,從我們的 rush-plugins 自動安裝器載入外掛程式。

    common/config/rush/rush-plugins.json

    {
    "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush-plugins.schema.json",
    "plugins": [
    /**
    * Each item defines a plugin to be loaded by Rush.
    */
    {
    /**
    * The name of the NPM package that provides the plugin.
    */
    "packageName": "@rushstack/rush-redis-cobuild-plugin",

    /**
    * The name of the plugin. This can be found in the "pluginName"
    * field of the "rush-plugin-manifest.json" file in the NPM package folder.
    */
    "pluginName": "rush-redis-cobuild-plugin",

    /**
    * The name of a Rush autoinstaller that will be used for installation, which
    * can be created using "rush init-autoinstaller". Add the plugin's NPM package
    * to the package.json "dependencies" of your autoinstaller, then run
    * "rush update-autoinstaller".
    */
    "autoinstallerName": "cobuild-plugin"
    }
    ]
    }
  6. 透過建立其設定檔來設定 rush-redis-cobuild-plugin

    common/config/rush-plugins/rush-redis-cobuild-plugin.json

    {
    /**
    * The URL of your Redis server
    */
    "url": "redis://server.example.com:6379",

    /**
    * An environment variable that your CI pipeline will assign,
    * which the plugin uses to authenticate with Redis.
    */
    "passwordEnvironmentVariable": "REDIS_PASSWORD"
    }
  7. 您可以使用 rush init 來建立用於啟用協同建置功能的 cobuild.json 設定檔。請務必設定 "cobuildFeatureEnabled": true,如下所示

    common/config/rush/cobuild.json

    /**
    * This configuration file manages Rush's cobuild feature.
    * More documentation is available on the Rush website: https://rush.dev.org.tw
    */
    {
    "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/cobuild.schema.json",

    /**
    * (Required) EXPERIMENTAL - Set this to true to enable the cobuild feature.
    * RUSH_COBUILD_CONTEXT_ID should always be specified as an environment variable with an non-empty string,
    * otherwise the cobuild feature will be disabled.
    */
    "cobuildFeatureEnabled": true,

    /**
    * (Required) Choose where cobuild lock will be acquired.
    *
    * The lock provider is registered by the rush plugins.
    * For example, @rushstack/rush-redis-cobuild-plugin registers the "redis" lock provider.
    */
    "cobuildLockProvider": "redis"
    }
  8. 執行 rush update,現在應該會安裝 cobuild-plugin 自動安裝器。這會下載其資訊清單檔案

    common/autoinstallers/cobuild-plugin/rush-plugins/@rushstack/rush-redis-cobuild-plugin/rush-plugin-manifest.json

    也將此檔案提交到 Git。(作為外掛程式系統的一部分,此檔案會快取重要資訊,以便 Rush 可以存取它,而無需安裝外掛程式的 NPM 套件。)

設定建置管線

每個 CI 系統都有不同的定義工作的方式。在本教學中,我們將使用 GitHub Actions 工作流程,因為它包含在公用專案的免費方案中。

假設我們的非協同建置 CI 管線如下所示(啟用建置快取寫入)

.github/workflows/ci-single.yml

name: ci-single.yml
on:
#push:
# branches: ['main']
#pull_request:
# branches: ['main']

# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
build:
name: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- uses: actions/setup-node@v3
with:
node-version: 16

- name: Rush Install
run: node common/scripts/install-run-rush.js install

- name: Rush build (install-run-rush)
run: node common/scripts/install-run-rush.js build --verbose --timeline
env:
RUSH_BUILD_CACHE_WRITE_ALLOWED: 1
RUSH_BUILD_CACHE_CREDENTIAL: ${{ secrets.RUSH_BUILD_CACHE_CREDENTIAL }}

以下是我們如何將其轉換為具有 3 個執行器的協同建置

.github/workflows/ci-cobuild.yml

name: ci-cobuild.yml
on:
#push:
# branches: ['main']
#pull_request:
# branches: ['main']

# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
build:
name: cobuild
runs-on: ubuntu-latest
strategy:
matrix:
runner_id: [runner1, runner2, runner3]
steps:
- uses: actions/checkout@v3

- uses: actions/setup-node@v3
with:
node-version: 16

- name: Rush Install
run: node common/scripts/install-run-rush.js install

- name: Rush build (install-run-rush)
run: node common/scripts/install-run-rush.js build --verbose --timeline
env:
RUSH_BUILD_CACHE_WRITE_ALLOWED: 1
RUSH_BUILD_CACHE_CREDENTIAL: ${{ secrets.RUSH_BUILD_CACHE_CREDENTIAL }}
RUSH_COBUILD_CONTEXT_ID: ${{ github.run_id }}_${{ github.run_number }}_${{ github.run_attempt }}
RUSH_COBUILD_RUNNER_ID: ${{ matrix.runner_id }}
REDIS_PASSWORD: ${{ secrets.REDIS_PASSWORD }}

runner_id 矩陣會使工作在 3 個不同的機器上執行。REDIS_PASSWORD 變數名稱是我們稍早在 rush-redis-cobuild-plugin.json 中定義的名稱。RUSH_COBUILD_CONTEXT_IDRUSH_COBUILD_RUNNER_ID 變數將在下方說明。

協同建置環境變數詳解

RUSH_COBUILD_CONTEXT_ID

協同建置執行器必須定義此環境變數;如果沒有它,Rush 將執行常規建置,而沒有任何協同建置邏輯。

RUSH_COBUILD_CONTEXT_ID 變數會控制快取:想像一下,提取請求驗證失敗,因為某個專案發生錯誤。如果沒有協同建置,發生錯誤的專案不會儲存到建置快取。如果有人前往 GitHub 網站並按一下按鈕來「重新執行此工作」,則成功的專案將從快取中提取,但該失敗的專案將會被迫再次建置,這很好,因為可能只是暫時性的失敗。

然而,對於協同建置,如果某個專案發生錯誤,我們不希望其他兩台機器嘗試建置該專案。錯誤記錄會儲存到建置快取中,並由其他執行器還原和列印(以便在每台機器上提供完整的記錄)。但是,如果有人按一下「重新執行此工作」,我們如何在這種情況下強制失敗的專案重新建置?RUSH_COBUILD_CONTEXT_ID 識別碼會解決這個問題。Rush 會將其新增至失敗專案的建置快取金鑰,以確保在重新嘗試工作時重新建置這些專案。

RUSH_COBUILD_CONTEXT_ID 在每個系統中的指定方式不同。它可以是具有以下屬性的任何字串

  • 對於給定的管線,每個機器上的 RUSH_COBUILD_CONTEXT_ID 必須相同
  • 每次執行管線時,包括「重新嘗試」和「重試」,RUSH_COBUILD_CONTEXT_ID 都必須不同
  • 它必須是短字串,因為它會成為快取金鑰的一部分

一些範例

CI 系統RUSH_COBUILD_CONTEXT_ID 的建議值
Azure DevOps$(Build.BuildNumber)_$(System.JobAttempt)
CircleCI${CIRCLE_WORKFLOW_ID}_${CIRCLE_WORKFLOW_JOB_ID}
GitHub Actions${{ github.run_id }}_${{ github.run_number }}_${{ github.run_attempt }}

RUSH_COBUILD_RUNNER_ID

此環境變數會唯一識別每台機器。如果未定義此變數,Rush 會在每次執行時產生隨機識別碼。

在範例中,為了方便閱讀,我們將其指定為 RUSH_COBUILD_RUNNER_ID: ${{ matrix.runner_id }}

技術細節

建置快取的正確性

您會發現協同建置功能提高了每個專案的輸出準確儲存和由快取還原的要求。為了了解原因,假設專案 A 直接依賴專案 B。不精確的快取仍可能會產生成功建置的方法有幾種

  1. 專案 AB 都是快取未命中,因此不會進行快取。- 或 -
  2. 專案 AB 都是快取命中。B 無法正確還原。A 會無法編譯,除非我們不需要建置 AA 的最終結果仍然可用。- 或 -
  3. 只有專案 A 是快取未命中。B 無法正確還原,但是遺失的檔案仍存在於同一機器先前建置的磁碟上。因此,A 會編譯而不會發生錯誤。

這些幸運的情況在非協同建置的情境中相對常見。如果您運氣不好,重新嘗試工作可能會導致問題「清除」(由於新的快取命中)。在這種情況之前,底層問題不會持續被注意到

  1. 只有專案 A 是快取未命中。B 無法正確還原,並且我們的建置會從乾淨的磁碟開始。

協同建置大幅增加了遇到 #4 的可能性,因為它們的目標是盡可能建置依賴快取命中的快取未命中。簡而言之,在首次啟用協同建置功能後,您可能需要花一些時間來修正不正確的建置快取設定。

👉 疑難排解建置快取不準確性

如果您懷疑檔案未被 Rush 建置快取準確地儲存/還原,請嘗試 rush-audit-cache-plugin。它會在您的建置操作期間監視檔案寫入,藉此偵測這類問題。然後,會將寫入的檔案路徑與專案的快取設定進行比較,產生未正確快取檔案路徑的報告。然後,您可以透過更正快取設定或修正工具將其輸出寫入可快取的位置,來解決問題。

Redis 中儲存了什麼?

協同建置功能將 Redis 用於兩個主要目的

  1. 可重入的鎖定機制。 對應於鎖定的金鑰格式為 cobuild:lock:<context_id>:<cluster_id>,而對應的值為 <runner_id>。在設定鎖定金鑰時,也會設定 30 秒的到期時間。這可確保相同的執行器在嘗試再次取得鎖定時可以重新取得鎖定,同時在執行器在一段時間內沒有回應時自動釋放鎖定。

  2. 追蹤已完成的操作。 對應於已完成狀態的金鑰格式為 cobuild:completed:<context_id>:<cache_id>,而對應的值是以序列化形式表示的操作執行結果字串和對應的 cache_id。在嘗試取得鎖定之前,機器會先查詢此完成結果資訊。如果有可用的完成結果,則會根據剖析的資訊重複使用結果。

另請參閱