Git bundle 檔案儲存一個 pack-file 以及一些額外元資料,包括一組引用(refs)和一套(可能為空的)必要提交(commits)。有關更多資訊,請參閱 git-bundle[1]gitformat-bundle[5]

Bundle URI 是 Git 可以下載一個或多個 bundle 以便在從遠端抓取剩餘物件之前引導物件資料庫的位置。

一個目標是加快網路連線到源伺服器較差的使用者的克隆和抓取速度。另一個好處是允許像 CI 構建農場這樣的重度使用者利用本地資源獲取大部分 Git 資料,從而減輕源伺服器的負載。

為了啟用 bundle URI 功能,使用者可以使用命令列選項指定 bundle URI,或者源伺服器可以透過協議 v2 能力來發佈一個或多個 URI。

設計目標

bundle URI 標準旨在足夠靈活以滿足多種工作負載。bundle 提供者和 Git 客戶端在建立和使用 bundle URI 方面有多種選擇。

  • bundle 可以擁有伺服器所需的任何名稱。此名稱可以透過使用 bundle 內容的雜湊值來引用不可變資料。然而,這意味著每次內容更新後都需要一個新的 URI。如果伺服器正在釋出該 URI(並且伺服器知道正在生成新的 bundle),這可能是可以接受的,但對於使用命令列選項的使用者來說,這將不符合人體工程學。

  • bundle 可以專門為引導完整克隆而組織,但也可以為了引導增量抓取而組織。bundle 提供者必須決定採用幾種組織方案中的一種,以最大限度地減少增量抓取期間的客戶端下載量,但 Git 客戶端也可以選擇是否將 bundle 用於這些操作中的任何一種。

  • bundle 提供者可以選擇支援完整克隆、部分克隆或兩者。客戶端可以檢測哪些 bundle 適用於倉庫的部分克隆過濾器(如果有)。

  • bundle 提供者可以使用單個 bundle(僅用於克隆),或一個 bundle 列表。當使用 bundle 列表時,提供者可以指定客戶端是否需要所有 bundle URI 才能完成完整克隆,或者任何一個 bundle URI 是否足夠。這允許 bundle 提供者為不同地理區域使用不同的 URI。

  • bundle 提供者可以使用啟發式方法(例如建立令牌)來組織 bundle,以幫助客戶端避免下載不需要的 bundle。當 bundle 提供者不提供這些啟發式方法時,客戶端可以使用最佳化來最大限度地減少下載的資料量。

  • bundle 提供者不需要與 Git 伺服器關聯。客戶端可以選擇使用 bundle 提供者,而無需 Git 伺服器的釋出。

  • 客戶端可以選擇發現 Git 伺服器釋出的 bundle 提供者。這可能發生在 git clone 期間、git fetch 期間、兩者兼有或兩者皆無。使用者可以選擇最適合他們的組合。

  • 客戶端可以隨時手動配置 bundle 提供者。客戶端還可以選擇手動指定 bundle 提供者作為 git clone 的命令列選項。

每個倉庫都不同,每個 Git 伺服器也有不同的需求。希望 bundle URI 功能足夠靈活,能夠滿足所有需求。如果不能,則可以透過其版本控制機制進行功能擴充套件。

伺服器要求

為了提供 bundle 伺服器的伺服器端實現,不需要 Git 協議的其他部分。這允許伺服器維護者使用靜態內容解決方案,例如 CDN,來提供 bundle 檔案。

在 bundle URI 功能的當前範圍內,所有 URI 都應是 HTTP(S) URL,內容透過對該 URL 的 GET 請求下載到本地檔案。伺服器可以在這些請求中包含身份驗證要求,目的是觸發已配置的憑據助手以進行安全訪問。(未來的擴充套件可以使用 "file://" URI 或 SSH URI。)

假設伺服器返回 200 OK 響應,則檢查 URL 處的內容。首先,Git 嘗試將檔案解析為版本 2 或更高版本的 bundle 檔案。如果檔案不是 bundle,則使用 Git 的配置解析器將檔案解析為純文字檔案。該配置檔案中的鍵值對應描述一個 bundle URI 列表。如果這些解析嘗試均未成功,則 Git 將向用戶報告錯誤,指明提供的 bundle URI 包含錯誤資料。

伺服器提供的任何其他資料均被視為錯誤資料。

Bundle 列表

Git 伺服器可以使用一組 key=value 對來發布 bundle URI。一個 bundle URI 也可以以 Git 配置格式提供一個純文字檔案,其中包含相同的 key=value 對。在這兩種情況下,我們都將其視為一個 bundle 列表。這些對指定了關於 bundle 的資訊,客戶端可以使用這些資訊來決定下載哪些 bundle 以及忽略哪些。

一些鍵關注列表本身的屬性。

bundle.version

(必需)此值提供 bundle 列表的版本號。如果未來的 Git 更改啟用了一項功能,需要 Git 客戶端對 bundle 列表檔案中的新鍵做出反應,則此版本將遞增。當前唯一的版本號是 1,如果指定任何其他值,Git 將無法使用此檔案。

bundle.mode

(必需)此值有兩個可能的值:allany。當指定 all 時,客戶端應預期需要所有與倉庫要求匹配的列出的 bundle URI。當指定 any 時,客戶端應預期任何一個與倉庫要求匹配的 bundle URI 就足夠了。通常,any 選項用於列出位於不同地理區域的多個不同 bundle 伺服器。

bundle.heuristic

如果存在此字串值鍵,則 bundle 列表旨在很好地與增量 git fetch 命令配合使用。該啟發式方法表明每個 bundle 都有額外的可用鍵,有助於確定客戶端應下載哪些 bundle 子集。目前唯一計劃的啟發式方法是 creationToken

剩餘的鍵包含一個 <id> 段,它是伺服器為每個可用 bundle 指定的名稱。<id> 必須只包含字母數字和 - 字元。

bundle.<id>.uri

(必需)此字串值是下載 bundle <id> 的 URI。如果 URI 以協議(http://https://)開頭,則 URI 是絕對的。否則,URI 被解釋為相對於 bundle 列表所使用的 URI。如果 URI 以 / 開頭,則該相對路徑是相對於 bundle 列表所使用的域名。(這種相對路徑的使用旨在使得將一組 bundle 分發到具有不同域名的多個伺服器或 CDN 更容易。)

bundle.<id>.filter

此字串值表示一個物件過濾器,該過濾器也應出現在此 bundle 的頭部。伺服器使用此值來區分不同型別的 bundle,客戶端可以從中選擇那些與其物件過濾器匹配的 bundle。

bundle.<id>.creationToken

此值為一個非負 64 位整數,用於對 bundle 列表進行排序。當 bundle.heuristic=creationToken 時,它用於在抓取期間下載一部分 bundle。

bundle.<id>.location

此字串值釋出 bundle URI 的實際服務位置。這可以用來向用戶提供一個選項,選擇使用哪個 bundle URI,或者僅僅作為 Git 選擇了哪個 bundle URI 的資訊指示器。這僅在 bundle.modeany 時有價值。

這是一個使用 Git 配置格式的 bundle 列表示例

[bundle]
	version = 1
	mode = all
	heuristic = creationToken
[bundle "2022-02-09-1644442601-daily"]
	uri = https://bundles.example.com/git/git/2022-02-09-1644442601-daily.bundle
	creationToken = 1644442601
[bundle "2022-02-02-1643842562"]
	uri = https://bundles.example.com/git/git/2022-02-02-1643842562.bundle
	creationToken = 1643842562
[bundle "2022-02-09-1644442631-daily-blobless"]
	uri = 2022-02-09-1644442631-daily-blobless.bundle
	creationToken = 1644442631
	filter = blob:none
[bundle "2022-02-02-1643842568-blobless"]
	uri = /git/git/2022-02-02-1643842568-blobless.bundle
	creationToken = 1643842568
	filter = blob:none

此示例使用 bundle.mode=all 以及 bundle.<id>.creationToken 啟發式方法。它還使用 bundle.<id>.filter 選項來呈現兩組並行的 bundle:一組用於完整克隆,另一組用於無 blob 的部分克隆。

假設此 bundle 列表位於 URI https://bundles.example.com/git/git/,因此這兩個無 blob 的 bundle 具有以下完全展開的 URI

  • https://bundles.example.com/git/git/2022-02-09-1644442631-daily-blobless.bundle

  • https://bundles.example.com/git/git/2022-02-02-1643842568-blobless.bundle

釋出 Bundle URI

如果使用者知道他們正在克隆的倉庫的 bundle URI,則可以透過命令列選項手動指定該 URI。但是,Git 主機可能希望在克隆操作期間釋出 bundle URI,以幫助不瞭解該功能的使用者。

此功能唯一需要的是伺服器能夠釋出一個或多個 bundle URI。此釋出採用新協議 v2 能力的形式,專門用於發現 bundle URI。

客戶端可以選擇任意 bundle URI 作為選項,或者透過一些探索性檢查選擇效能最佳的 URI。由 bundle 提供者決定是擁有多個 URI 更可取,還是透過伺服器端基礎設施進行地理分散式部署的單個 URI 更可取。

使用 Bundle URI 進行克隆

bundle URI 的主要需求是加快克隆速度。Git 客戶端將按照以下流程與 bundle URI 互動:

  1. 使用者透過 --bundle-uri 命令列選項指定 bundle URI,或者客戶端發現 Git 伺服器釋出的 bundle 列表。

  2. 如果從 bundle URI 下載的資料是 bundle,則客戶端檢查 bundle 頭部,以確認所需的提交 OID 是否存在於客戶端倉庫中。如果缺少某些 OID,客戶端將延遲解包,直到其他 bundle 被解包,使這些 OID 存在。當所有必需的 OID 都存在時,客戶端使用 refspec 解包資料。使用的 refspec 是 +refs/*:refs/bundles/*。這些引用被儲存起來,以便後續的 git fetch 協商可以將每個 bundled ref 作為 have 進行通訊,從而減少透過 Git 協議抓取的資料量。為了允許從這個引用名稱空間修剪引用,Git 可能會引入一個編號名稱空間(例如 refs/bundles/<i>/*),以便可以刪除過時的 bundle 引用。

  3. 如果檔案是 bundle 列表,則客戶端檢查 bundle.mode 以檢視列表是 all 形式還是 any 形式。

    1. 如果 bundle.mode=all,則客戶端考慮所有 bundle URI。列表會根據與客戶端倉庫的部分克隆過濾器匹配的 bundle.<id>.filter 選項進行縮減。然後,請求所有 bundle URI。如果提供了 bundle.<id>.creationToken 啟發式方法,則 bundle 將按建立令牌遞減的順序下載,當一個 bundle 擁有所有必需的 OID 時停止。然後可以按建立令牌遞增的順序解包 bundle。如果 bundle 列表沒有釋出具有更大建立令牌的 bundle,客戶端會將最新的建立令牌儲存起來,作為避免未來下載的啟發式方法。

    2. 如果 bundle.mode=any,則客戶端可以選擇任何一個 bundle URI 進行檢查。客戶端可以使用多種方式在這些 URI 中進行選擇。如果初始選擇未能返回結果,客戶端還可以回退到另一個 URI。

請注意,在克隆期間,我們預期將需要所有 bundle,並且可以使用 bundle.<uri>.creationToken 等啟發式方法按時間順序或並行下載 bundle。

如果給定的 bundle URI 是一個具有 bundle.heuristic 值的 bundle 列表,則客戶端可以選擇將該 URI 儲存為它選擇的 bundle URI。然後,客戶端可以在後續的 git fetch 呼叫期間直接導航到該 URI。

下載 bundle URI 時,客戶端可以選擇在決定下載全部內容之前檢查初始內容。這可能提供足夠的資訊來確定 URI 是 bundle 列表還是 bundle。在 bundle 的情況下,客戶端可以檢查 bundle 頭部,以確定所有公佈的提示(tips)是否已在客戶端倉庫中,並取消剩餘的下載。

使用 Bundle URI 進行抓取

當客戶端抓取新資料時,它可以決定在從源遠端抓取之前先從 bundle 伺服器抓取。這可以透過命令列選項完成,但更有可能的是使用在克隆期間指定的配置值。

抓取操作遵循相同的過程從 bundle 列表下載 bundle(儘管我們希望在這裡使用並行下載)。我們期望當精簡 bundle(thin bundle)中所有必需的提交 OID 都已存在於物件資料庫中時,該過程將結束。

當使用 creationToken 啟發式方法時,如果 bundle 的建立令牌不大於已儲存的建立令牌,客戶端可以避免下載任何 bundle。抓取新的 bundle 後,Git 會更新此本地建立令牌。

如果 bundle 提供者沒有提供啟發式方法,那麼客戶端應在下載完整的 bundle 資料之前嘗試檢查 bundle 頭部,以防 bundle 提示(tips)已存在於客戶端倉庫中。

錯誤條件

如果 Git 客戶端在根據 bundle URI 或在該位置找到的 bundle 列表下載資訊時發現意外情況,則 Git 可以忽略該資料並繼續,如同沒有提供 bundle URI 一樣。遠端 Git 伺服器是最終的真相來源,而不是 bundle URI。

以下是一些錯誤條件示例

  • 客戶端無法連線到給定 URI 的伺服器,或者連線在無法恢復的情況下丟失。

  • 客戶端收到 4xx 級別的響應(例如 404 Not Found401 Not Authorized)。客戶端應使用憑據助手查詢併為 URI 提供憑據,但要符合 Git 其他 HTTP 協議在處理特定 4xx 級別錯誤方面的語義。

  • 伺服器報告任何其他失敗響應。

  • 客戶端收到的資料無法解析為 bundle 或 bundle 列表。

  • bundle 包含一個不符合預期的過濾器。

  • 客戶端無法解包 bundle,因為所需的提交 OID 不在物件資料庫中,並且沒有更多 bundle 可以下載。

還有一些情況可能被視為浪費,但並非錯誤條件

  • 下載的 bundle 包含的資訊多於克隆或抓取請求所需的資訊。一個主要示例是,如果使用者使用 --single-branch 請求克隆,但下載了儲存所有 refs/heads/* 引用中所有可到達提交的 bundle。這可能最初是浪費的,但也許這些物件會透過客戶端關心的後續引用更新變得可到達。

  • git fetch 期間下載的 bundle 包含已存在於物件資料庫中的物件。如果我們將 bundle 用於抓取,這可能是不可避免的,因為客戶端在向遠端伺服器執行“追趕”抓取後,幾乎總是會稍微領先於 bundle 伺服器。當客戶端抓取頻率遠高於伺服器計算 bundle 的頻率時,這種額外的工作最為浪費,例如客戶端使用每小時預抓取並進行後臺維護,而伺服器每週才計算一次 bundle。因此,除非伺服器透過 bundle.heuristic 值明確推薦,否則客戶端不應將 bundle URI 用於抓取。

Bundle 提供者組織示例

bundle URI 功能特意設計為靈活適應 bundle 提供者組織物件資料的不同方式。然而,在這裡描述一個完整的組織模型可能會有所幫助,以便提供者可以從該基礎開始。

此組織示例是 GVFS 快取伺服器(參見本文件末尾部分)所用模型的簡化版本,該模型在加速超大型倉庫的克隆和抓取方面非常有益,儘管使用了 Git 之外的額外軟體。

bundle 提供者在多個地理區域部署伺服器。每個伺服器管理自己的 bundle 集。伺服器可以跟蹤多個 Git 倉庫,並根據模式為每個倉庫提供 bundle 列表。例如,當映象一個位於 https://<domain>/<org>/<repo> 的倉庫時,bundle 伺服器的 bundle 列表可能在 https://<server-url>/<domain>/<org>/<repo> 可用。源 Git 伺服器可以在“any”模式下列出所有這些伺服器。

[bundle]
	version = 1
	mode = any
[bundle "eastus"]
	uri = https://eastus.example.com/<domain>/<org>/<repo>
[bundle "europe"]
	uri = https://europe.example.com/<domain>/<org>/<repo>
[bundle "apac"]
	uri = https://apac.example.com/<domain>/<org>/<repo>

這個“列表的列表”是靜態的,只有在新增或刪除 bundle 伺服器時才會發生變化。

每個 bundle 伺服器管理自己的 bundle 集。初始 bundle 列表只包含一個 bundle,其中包含從源伺服器克隆倉庫時接收到的所有物件。該列表使用 creationToken 啟發式方法,並根據伺服器的時間戳為該 bundle 生成一個 creationToken

bundle 伺服器定期執行 bundle 列表的更新,例如每天一次。在此任務期間,伺服器從源伺服器抓取最新內容,並生成一個 bundle,其中包含可從最新源引用(origin refs)訪問的物件,但這些物件未包含在先前計算的 bundle 中。此 bundle 被新增到列表中,並確保 creationToken 嚴格大於先前的最大 creationToken

當 bundle 列表變得過大,例如超過 30 個 bundle 時,最舊的“N 減去 30”個 bundle 將合併為一個 bundle。此 bundle 的 creationToken 等於合併後的 bundle 中最大的 creationToken

這裡提供一個 bundle 列表示例,儘管它只有兩個每日 bundle,而不是完整的 30 個列表

[bundle]
	version = 1
	mode = all
	heuristic = creationToken
[bundle "2022-02-13-1644770820-daily"]
	uri = https://eastus.example.com/<domain>/<org>/<repo>/2022-02-09-1644770820-daily.bundle
	creationToken = 1644770820
[bundle "2022-02-09-1644442601-daily"]
	uri = https://eastus.example.com/<domain>/<org>/<repo>/2022-02-09-1644442601-daily.bundle
	creationToken = 1644442601
[bundle "2022-02-02-1643842562"]
	uri = https://eastus.example.com/<domain>/<org>/<repo>/2022-02-02-1643842562.bundle
	creationToken = 1643842562

為了避免物件資料即使在源伺服器中變得不可訪問後仍然永久儲存和提供,這種 bundle 合併可以更謹慎。不是對舊 bundle 進行絕對合並,而是透過檢視較新的 bundle 並確保它們所需的提交都在這個合併的 bundle 中(或在另一個較新的 bundle 中)來建立 bundle。這允許“過期”在此時間視窗內未被新提交使用的物件資料。這些資料可以通過後續的推送重新引入。

這種資料組織有兩個主要目標。首先,透過從更近的來源下載預計算的物件資料,倉庫的初始克隆變得更快。其次,git fetch 命令可以更快,特別是如果客戶端幾天沒有抓取。但是,如果客戶端 30 天沒有抓取,則 bundle 列表組織將導致重新下載大量物件資料。

使這種組織對頻繁抓取的使用者更有用的一種方法是更頻繁地建立 bundle。例如,可以每小時建立 bundle,然後每天將這些“每小時”bundle 合併為一個“每日”bundle。每日 bundle 在 30 天后合併到最舊的 bundle 中。

如果此倉庫的客戶端期望使用無 blob 的部分克隆,建議使用 blob:none 過濾器重複此 bundle 策略。此無 blob bundle 列表與完整 bundle 保持在同一列表中,但使用 bundle.<id>.filter 鍵來分隔兩組。對於非常大的倉庫,bundle 提供者可能只提供無 blob 的 bundle。

實施計劃

本設計文件作為一份展望性文件獨立提交,目標是在多個補丁系列的過程中實現所有提及的客戶端功能。以下是提交這些功能的潛在大綱:

  1. 將 bundle URI 整合到 git clone 中,並帶有一個 --bundle-uri 選項。這將包括一個新的 git fetch --bundle-uri 模式,用作 git clone 底層的實現。這裡的初始版本將預期在給定 URI 處有一個單獨的 bundle。

  2. 實現從 bundle URI 解析 bundle 列表的能力,並更新 git fetch --bundle-uri 邏輯,以正確區分 bundle.mode 選項。專門設計該功能,使得配置格式解析將鍵值對列表饋送到 bundle 列表邏輯中。

  3. 建立 bundle-uri 協議 v2 命令,以便 Git 伺服器可以使用鍵值對釋出 bundle URI。連線到現有的鍵值輸入到 bundle 列表邏輯中。允許 git clone 發現這些 bundle URI 並從 bundle 資料引導客戶端倉庫。(此選擇透過配置選項和命令列選項進行選擇加入。)

  4. 允許客戶端理解 bundle.heuristic 配置鍵和 bundle.<id>.creationToken 啟發式方法。當 git clone 發現帶有 bundle.heuristic 的 bundle URI 時,它會配置客戶端倉庫在後續的 git fetch <remote> 命令期間檢查該 bundle URI。

  5. 允許客戶端在 git fetch 期間發現 bundle URI,並在設定了 bundle.heuristic 的情況下為後續抓取配置 bundle URI。

  6. 實現“檢查頭部”啟發式方法,以在 bundle.<id>.creationToken 啟發式方法不可用時減少資料下載。

隨著這些功能的審查,本計劃可能會更新。我們還期望隨著此功能的成熟並在實際場景中得到應用,將發現並實現新的設計。

Git 協議已經具備一項功能,即 Git 伺服器在響應客戶端請求時,可以列出一組 URL 以及 packfile 響應。然後,客戶端需要下載這些位置的 packfile,以便完整理解響應。

此機制由 Gerrit 伺服器(使用 JGit 實現)使用,在降低 CPU 負載和提高使用者克隆效能方面非常有效。

這種機制的一個主要缺點是源伺服器需要準確地知道這些 packfile 中包含什麼,並且 packfile 需要在伺服器響應後的一段時間內對使用者可用。源伺服器與 packfile 資料之間的這種耦合很難管理。

此外,此實現極難與抓取(fetches)配合使用。

GVFS 協議 [2] 是一組 HTTP 端點,在 Git 的部分克隆功能建立之前獨立於 Git 專案設計。此協議的一個特點是“快取伺服器”的概念,它可以與構建機器或開發人員辦公室共置,以傳輸 Git 資料而不會使中心伺服器過載。

VFS for Git 著名的端點是 GET /gvfs/objects/{oid} 端點,它允許按需下載物件。這是該產品檔案系統虛擬化的關鍵部分。

然而,一個更微妙的需求是 GET /gvfs/prefetch?lastPackTimestamp=<t> 端點。給定一個可選的時間戳,快取伺服器會響應一個預計算 packfile 列表,其中包含在這些時間間隔內引入的提交和樹。

快取伺服器使用以下策略計算這些“預抓取”(prefetch)packfile

  1. 每小時,會生成一個帶有給定時間戳的“每小時”包。

  2. 每晚,前 24 個每小時包會彙總成一個“每日”包。

  3. 每晚,所有超過 30 天的預抓取包都會彙總成一個包。

當用戶針對具有快取伺服器的倉庫執行 gvfs clonescalar clone 時,客戶端會請求所有預抓取 packfile,這最多是 24 + 30 + 1 個 packfile,僅下載提交和樹。然後客戶端會向源伺服器請求引用,並嘗試檢出該提示引用。(還有一個額外的端點,用於獲取給定提交的所有可到達的樹,以防該提交尚未存在於預抓取 packfile 中。)

git fetch 期間,一個鉤子會使用先前下載的預抓取 packfile 中最新的時間戳請求預抓取端點。只下載時間戳較晚的 packfile 列表。大多數使用者每小時抓取一次,因此他們最多隻會獲取一個每小時預抓取包。機器已關機或超過 30 天未抓取的使用者可能會重新下載所有預抓取 packfile。這種情況很少見。

需要注意的是,客戶端總是聯絡源伺服器以獲取引用公告,因此引用通常“領先”於預抓取的包資料。當 git checkoutgit log 等命令需要時,缺失的物件會使用 GET gvfs/objects/{oid} 請求按需下載。一些 Git 最佳化會停用可能導致這些按需下載過於激進的檢查。

scroll-to-top