Git
英文 ▾ 主題 ▾ 最新版本 ▾ partial-clone 最後更新於 2.43.0

「部分複製 (Partial Clone)」功能是 Git 的效能最佳化,讓 Git 能夠在沒有完整的儲存庫副本的情況下運作。此工作的目標是讓 Git 能夠更好地處理極大的儲存庫。

在複製和提取操作期間,Git 會下載儲存庫的完整內容和歷史記錄。這包括儲存庫完整生命週期的所有 commit、樹狀結構和 blob。對於極大的儲存庫,複製可能需要數小時(或數天),並消耗 100+GiB 的磁碟空間。

在這些儲存庫中,通常有很多使用者不需要的 blob 和樹狀結構,例如

  1. 樹狀結構中使用者工作區域之外的檔案。例如,在每個 commit 中有 50 萬個目錄和 350 萬個檔案的儲存庫中,如果使用者只需要來源樹的狹窄「錐形」部分,我們可以避免下載許多物件。

  2. 大型二進位資產。例如,在將大型建置產物簽入樹狀結構的儲存庫中,我們可以避免下載這些不可合併二進位資產的所有先前版本,而只下載實際被參考的版本。

部分複製允許我們在複製和提取操作期間預先避免下載此類不需要的物件,從而減少下載時間和磁碟使用量。如果需要,稍後可以「按需提取」遺失的物件。

能夠稍後提供遺失物件的遠端稱為承諾遠端 (promisor remote),因為它承諾在請求時發送物件。最初,Git 僅支援一個承諾遠端,即使用者複製的 origin 遠端,並在 "extensions.partialClone" 配置選項中設定。後來,已實作對多個承諾遠端的支援。

使用部分複製需要使用者處於連線狀態,並且 origin 遠端或其他承諾遠端可用於按需提取遺失的物件。這對於使用者來說可能有問題,也可能沒有問題。例如,如果使用者可以停留在預先選取的來源樹子集中,他們可能不會遇到任何遺失的物件。或者,使用者可以嘗試預先提取各種物件,如果他們知道他們要離線。

非目標

部分複製是一種限制在給定的 commit 範圍下載的 blob 和樹狀結構數量的機制,因此獨立於現有的 DAG 級機制(例如,淺層複製、單一分支或提取 *<refspec>*)來限制請求的 commit 集,並且不打算與之衝突。

設計概述

部分複製在邏輯上包含以下部分

  • 客戶端向伺服器描述不需要或不想要的物件的機制。

  • 伺服器從傳送到客戶端的 packfile 中省略此類不想要的物件的機制。

  • 客戶端優雅地處理遺失物件(先前由伺服器省略)的機制。

  • 客戶端在需要時回填遺失物件的機制。

設計細節

  • 新的 pack 協定功能 "filter" 已新增至 fetch-pack 和 upload-pack 協商。

    這使用現有的功能探索機制。請參閱 gitprotocol-pack[5] 中的 "filter"。

  • 客戶端將 "filter-spec" 傳遞到 clone 和 fetch,該規格會傳遞到伺服器,以請求在 packfile 建構期間進行篩選。

    有各種篩選器可用於適應不同的情況。請參閱 Documentation/rev-list-options.txt 中的 "--filter=<filter-spec>"。

  • 在伺服器上,pack-objects 會在為客戶端建立「已篩選」的 packfile 時套用請求的 filter-spec。

    這些已篩選的 packfile 在傳統意義上是不完整的,因為它們可能包含引用 packfile 中未包含且客戶端尚未擁有的物件的物件。例如,已篩選的 packfile 可能包含引用遺失 blob 或引用遺失樹狀結構的 commit 的樹狀結構或標籤。

  • 在客戶端上,這些不完整的 packfile 會標記為「承諾 packfile」,並由各種指令以不同的方式處理。

  • 在客戶端上,會將儲存庫擴充功能新增至本機配置,以防止較舊版本的 Git 因無法處理的遺失物件而在操作中途失敗。請參閱 Documentation/technical/repository-version.txt 中的 "extensions.partialClone"。

處理遺失物件

  • 物件可能因部分複製或提取而遺失,或因儲存庫損壞而遺失。為了區分這些情況,本機儲存庫會特別將從承諾遠端取得的此類已篩選的 packfile 指示為「承諾 packfile」。

    除了其 "<name>.pack" 和 "<name>.idx" 檔案之外,這些承諾 packfile 還包含任意內容的 "<name>.promisor" 檔案(例如 "<name>.keep" 檔案)。

  • 本機儲存庫認為「承諾物件」是它知道(盡其所能)承諾遠端已承諾它們擁有的物件,原因是在本機儲存庫的其中一個承諾 packfile 中有該物件,或者另一個承諾物件參考它。

    當 Git 遇到遺失物件時,Git 可以查看它是否是承諾物件,並適當地處理它。如果不是,Git 可以報告損壞。

    這表示客戶端不需要明確維護成本高昂且難以修改的遺失物件清單。[a]

  • 由於幾乎所有 Git 程式碼目前都預期任何被參考的物件都存在於本機,而且我們不希望強制每個指令先進行試執行,因此新增了回退機制,以允許 Git 嘗試從承諾遠端動態提取遺失的物件。

    當正常的物件查找無法找到物件時,Git 會呼叫 promisor_remote_get_direct() 以嘗試從承諾遠端取得物件,然後重試物件查找。這允許「故障插入」物件,而無需複雜的預測演算法。

    為了提高效率,不會檢查遺失的物件是否實際上是承諾物件。

    動態物件提取往往很慢,因為物件會一次提取一個。

  • 已教導 checkout(和任何其他使用 unpack-trees 的指令)以單一批次大量預先提取所有需要的遺失 blob。

  • 已教導 rev-list 列印遺失的物件。

    其他指令可以使用此功能大量預先提取物件。例如,「git log -p A..B」可能在內部想要先執行類似「git rev-list --objects --quiet --missing=print A..B」的操作,然後大量預先提取這些物件。

  • fsck 已更新,以便完全了解承諾物件。

  • GC 中的 repack 已更新為完全不觸碰承諾 packfile,而只重新封裝其他物件。

  • 全域變數 "fetch_if_missing" 用於控制物件查找是否會嘗試動態提取遺失的物件或報告錯誤。

    我們對這個全域變數不滿意,並且想要移除它,但這需要大幅重構物件程式碼以傳遞額外的旗標。

提取遺失的物件

  • 物件的提取是透過呼叫 "git fetch" 子程序來完成。

  • 本機儲存庫會傳送包含所有請求物件的雜湊值的請求,並且不執行任何 packfile 協商。然後,它會收到 packfile。

  • 因為我們重複使用現有的提取機制,提取操作目前會提取請求物件所參照的所有物件,即使它們並非必要。

  • 使用 --refetch 進行提取會從遠端請求一個全新的篩選過的打包檔,這可以用來變更篩選條件,而無需動態提取遺失的物件。

使用多個承諾遠端

可以配置和使用多個承諾遠端。

舉例來說,這允許使用者擁有數個地理位置相近的快取伺服器,用於提取遺失的 blob,同時繼續從中央伺服器執行篩選過的 git-fetch 命令。

當提取物件時,會依序嘗試承諾遠端,直到提取完所有物件為止。

被視為「承諾」遠端的,是透過以下組態變數指定的遠端

  • extensions.partialClone = <name>

  • remote.<name>.promisor = true

  • remote.<name>.partialCloneFilter = ...

只能使用 extensions.partialClone 組態變數配置一個承諾遠端。這個承諾遠端將是提取物件時最後嘗試的一個。

我們決定將它設為最後嘗試的遠端,因為使用多個承諾遠端的人很可能是因為其他承諾遠端在某些方面(可能它們更近或對於某些類型的物件更快)比原始遠端更好,而原始遠端很可能就是由 extensions.partialClone 指定的遠端。

這個理由並非十分充分,但必須做出一個選擇,而且無論如何,長期的計畫應該是讓順序在某種程度上完全可配置。

不過目前,其他的承諾遠端將按照它們在組態檔案中出現的順序進行嘗試。

目前的限制

  • 除了它們在組態檔案中出現的順序外,無法用其他方式指定嘗試承諾遠端的順序。

    也無法指定從一個遠端提取時使用的順序,以及從另一個遠端提取時使用的不同順序。

  • 無法僅將特定物件推送到承諾遠端。

    無法同時依特定順序推送到多個承諾遠端。

  • 動態物件提取只會向承諾遠端請求遺失的物件。我們假設承諾遠端具有儲存庫的完整檢視,並且可以滿足所有此類請求。

  • 重新打包 (Repack) 本質上將承諾和非承諾的打包檔視為 2 個不同的分割區,並且不將它們混合在一起。

  • 動態物件提取會為**每個項目**調用一次 fetch-pack,因為大多數演算法都會遇到遺失的物件,並且需要在繼續工作之前解決它。如果需要許多物件,這可能會產生相當大的額外負擔,以及多次驗證請求。

  • 動態物件提取目前使用現有的打包協定 V0,這表示每個物件都透過 fetch-pack 請求。當連線建立時,伺服器會傳送一整組 info/refs。如果有大量的 refs,這可能會產生相當大的額外負擔。

未來的工作

  • 改進指定嘗試承諾遠端順序的方式。

    例如,這可以允許明確指定類似這樣的內容:「當從這個遠端提取時,我想要依照這個順序使用這些承諾遠端;然而,當推送到該遠端或從該遠端提取時,我想要依照那個順序使用那些承諾遠端。」

  • 允許推送到承諾遠端。

    使用者可能想要在一個三角工作流程中使用多個承諾遠端,而每個承諾遠端都對儲存庫有不完整的檢視。

  • 允許非基於路徑名稱的篩選器使用打包檔位元圖 (如果存在)。這只是在初始實作期間的遺漏。

  • 研究使用長時間執行的程序來動態提取一系列物件,例如 [5,6] 中提出的,以減少程序啟動和額外負擔成本。

    如果打包協定 V2 能夠讓長時間執行的程序透過單一長時間執行的連線發出多個請求,那就太好了。

  • 研究打包協定 V2,以避免在每次連線到伺服器以動態提取遺失的物件時廣播 info/refs。

  • 研究是否需要處理鬆散的承諾物件。

    允許承諾打包檔中的物件參照可以從伺服器動態提取的遺失物件。我們假設鬆散的物件只會在本地建立,因此不應參照遺失的物件。如果我們動態提取遺失的樹狀結構,並將其儲存為鬆散的物件而不是單一物件打包檔,我們可能需要重新檢視這個假設。

    這並不一定表示我們需要將鬆散的物件標示為承諾;放寬物件查找或 is-promisor 函數可能就足夠了。

非任務

  • 每次提到「依需求載入 blob」這個主題時,似乎總有人建議允許伺服器「猜測」並傳送可能與請求物件相關的其他物件。

    沒有任何工作是實際去實作這個;我們只是記錄這是一個常見的建議。我們不確定它將如何運作,也沒有計畫去研究它。

    伺服器傳送比請求的物件還多的物件是有效的(即使是對於動態物件提取),但我們並未基於此進行建構。

註腳

[a] 難以修改的遺失物件列表:在部分複製的早期設計中,我們討論了對於單一遺失物件列表的需求。這基本上是一個排序過的 OID 線性列表,這些 OID 在複製或後續提取期間被伺服器省略。

每次查找物件時,都需要將此檔案載入到記憶體中。每次顯式執行「git fetch」命令**以及**任何動態物件提取時,都需要讀取、更新和重新寫入此檔案(如同 .git/index)。

如果遺失的物件很多,讀取、更新和寫入此檔案的成本可能會為每個命令增加相當大的額外負擔。例如,如果遺失 1 億個 blob,則此檔案在磁碟上至少會佔用 2 GiB。

透過「承諾」概念,我們根據參照它的打包檔類型**推斷**出遺失的物件。

[0] https://crbug.com/git/2 Bug#2:部分複製

[1] https://lore.kernel.org/git/20170113155253.1644-1-benpeart@microsoft.com/
主旨:[RFC] 新增支援依需求下載 blob
日期:Fri, 13 Jan 2017 10:52:53 -0500

[2] https://lore.kernel.org/git/cover.1506714999.git.jonathantanmy@google.com/
主旨:[PATCH 00/18] 部分複製(從複製到 18 個修補程式中的延遲提取)
日期:Fri, 29 Sep 2017 13:11:36 -0700

[3] https://lore.kernel.org/git/20170426221346.25337-1-jonathantanmy@google.com/
主旨:Git 儲存庫中遺失 blob 支援的提案
日期:Wed, 26 Apr 2017 15:13:46 -0700

[4] https://lore.kernel.org/git/1488999039-37631-1-git-send-email-git@jeffhostetler.com/
主旨:[PATCH 00/10] RFC 部分複製和提取
日期:Wed, 8 Mar 2017 18:50:29 +0000

[5] https://lore.kernel.org/git/20170505152802.6724-1-benpeart@microsoft.com/
主旨:[PATCH v7 00/10] 將篩選程序程式碼重構為可重複使用的模組
日期:Fri, 5 May 2017 11:27:52 -0400

[6] https://lore.kernel.org/git/20170714132651.170708-1-benpeart@microsoft.com/
主旨:[RFC/PATCH v2 0/1] 新增支援依需求下載 blob
日期:Fri, 14 Jul 2017 09:26:50 -0400

scroll-to-top