简体中文 ▾ 主题 ▾ 最新版本 ▾ partial-clone 上次更新于 2.49.0

"部分克隆"功能是Git的一种性能优化,它允许Git在没有完整的仓库副本的情况下运行。这项工作的目标是让Git能够更好地处理极其庞大的仓库。

在克隆和抓取操作期间,Git会下载仓库的完整内容和历史记录。这包括仓库整个生命周期中的所有提交、树和二进制大对象(blobs)。对于极其庞大的仓库,克隆可能需要数小时(甚至数天),并占用100+GiB的磁盘空间。

这些仓库中通常有许多用户不需要的二进制大对象和树,例如:

  1. 树中位于用户工作区之外的文件。例如,在一个每个提交有50万个目录和350万个文件的仓库中,如果用户只需要源树的一个狭窄的“锥形”,我们可以避免下载许多对象。

  2. 大型二进制资产。例如,在一个将大型构建产物检入树中的仓库中,我们可以避免下载这些不可合并的二进制资产的所有先前版本,而只下载实际引用的版本。

部分克隆允许我们在克隆和抓取操作期间提前避免下载这些不需要的对象,从而减少下载时间和磁盘使用。缺失的对象可以在需要时进行“按需抓取”。

可以稍后提供缺失对象的远程仓库被称为“承诺远程仓库”(promisor remote),因为它承诺在请求时发送对象。最初Git只支持一个承诺远程仓库,即用户从中克隆的源远程仓库,它在"extensions.partialClone"配置选项中配置。后来实现了对多个承诺远程仓库的支持。

使用部分克隆要求用户在线,并且源远程仓库或其他承诺远程仓库可用于按需抓取缺失对象。这对于用户来说可能存在问题,也可能没有问题。例如,如果用户可以停留在源树预选的子集中,他们可能不会遇到任何缺失对象。或者,如果用户知道自己将离线,他们可以尝试预先抓取各种对象。

非目标

部分克隆是一种限制给定提交范围内下载的二进制大对象和树数量的机制——因此它独立于现有的DAG级别机制,不打算与它们冲突,以限制请求的提交集(即浅克隆、单分支或抓取 <refspec>)。

设计概述

部分克隆在逻辑上由以下部分组成:

  • 客户端向服务器描述不需要或不想要的对象的一种机制。

  • 服务器从发送给客户端的 packfile 中省略这些不想要的对象的一种机制。

  • 客户端优雅地处理缺失对象(先前已被服务器省略)的一种机制。

  • 客户端根据需要回填缺失对象的一种机制。

设计细节

  • 在 fetch-pack 和 upload-pack 协商中添加了一个新的 pack-protocol 能力“filter”。

    这使用了现有的能力发现机制。请参阅 gitprotocol-pack[5] 中的“filter”部分。

  • 客户端将“filter-spec”传递给克隆和抓取操作,该“filter-spec”再传递给服务器,以请求在 packfile 构建期间进行过滤。

    有各种过滤器可用于适应不同的情况。请参阅 Documentation/rev-list-options.adoc 中的“--filter=<filter-spec>”部分。

  • 在服务器端,pack-objects 在为客户端创建“过滤”packfile 时应用请求的 filter-spec。

    这些过滤后的 packfile 在传统意义上是不完整的,因为它们可能包含引用了 packfile 中未包含且客户端尚未拥有的对象的对象。例如,过滤后的 packfile 可能包含引用缺失的二进制大对象(blobs)的树或标签,或者引用缺失的树的提交。

  • 在客户端,这些不完整的 packfile 被标记为“承诺 packfile”(promisor packfiles),并由各种命令以不同方式处理。

  • 在客户端,本地配置中添加了一个仓库扩展,以防止旧版 Git 因无法处理的缺失对象而在操作中失败。请参阅 git-config[1] 中的 extensions.partialClone

处理缺失对象

  • 对象可能因部分克隆或抓取而缺失,也可能因仓库损坏而缺失。为了区分这些情况,本地仓库特意将从承诺远程仓库获取的此类过滤 packfile 标记为“承诺 packfile”。

    这些承诺 packfile 除了其“<name>.pack”和“<name>.idx”文件外,还包含一个“<name>.promisor”文件,其中包含任意内容(类似于“<name>.keep”文件)。

  • 本地仓库将“承诺对象”视为它知道(尽其所能)承诺远程仓库已承诺拥有的对象,这可能是因为本地仓库在其中一个承诺 packfile 中拥有该对象,或者因为另一个承诺对象引用了它。

    当 Git 遇到缺失对象时,Git 可以查看它是否是承诺对象并进行适当处理。如果不是,Git 可以报告损坏。

    这意味着客户端不需要明确维护一个修改成本高昂的缺失对象列表。[a]

  • 由于几乎所有 Git 代码目前都期望任何引用的对象都在本地存在,并且我们不想强制每个命令先进行一次试运行,因此添加了一个回退机制,允许 Git 尝试从承诺远程仓库动态抓取缺失对象。

    当正常的查找对象失败时,Git 调用 promisor_remote_get_direct() 尝试从承诺远程仓库获取对象,然后重试查找对象。这允许在没有复杂预测算法的情况下“加载”对象。

    出于效率考虑,不检查缺失对象是否实际是承诺对象。

    动态对象抓取通常很慢,因为对象是逐个抓取的。

  • checkout(以及任何其他使用 unpack-trees 的命令)已被修改,以在单个批处理中批量预抓取所有必需的缺失二进制大对象(blobs)。

  • 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 抓取将从远程仓库请求一个完整的新的过滤 packfile,这可以用于更改过滤器而无需动态抓取缺失对象。

使用多个承诺远程仓库

可以配置和使用多个承诺远程仓库。

例如,这允许用户拥有多个地理位置相近的缓存服务器来抓取缺失的二进制大对象(blobs),同时继续从中心服务器执行过滤后的 git-fetch 命令。

抓取对象时,承诺远程仓库会一个接一个地尝试,直到所有对象都被抓取完毕。

被认为是“承诺”远程仓库的是由以下配置变量指定的仓库:

  • extensions.partialClone = <name>

  • remote.<name>.promisor = true

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

使用 extensions.partialClone 配置变量只能配置一个承诺远程仓库。在抓取对象时,此承诺远程仓库将是最后一个尝试的。

我们决定将其设为最后尝试的一个,因为使用多个承诺远程仓库的人很可能是因为其他承诺远程仓库在某种程度上(也许它们对于某些类型的对象更近或更快)比源远程仓库更好,而源远程仓库很可能是由 extensions.partialClone 指定的远程仓库。

这个理由不是很充分,但必须做出一个选择,而且长期的计划应该是让顺序完全可配置。

但目前,其他承诺远程仓库将按照它们在配置文件中出现的顺序进行尝试。

当前限制

  • 除了它们在配置文件中出现的顺序之外,无法以其他方式指定尝试承诺远程仓库的顺序。

    当从一个远程仓库抓取时,也无法指定要使用的顺序;当从另一个远程仓库抓取时,也无法指定不同的顺序。

  • 无法仅将特定对象推送到承诺远程仓库。

    无法以特定顺序同时推送到多个承诺远程仓库。

  • 动态对象抓取将只向承诺远程仓库请求缺失对象。我们假设承诺远程仓库对仓库有完整的视图,并且可以满足所有此类请求。

  • 重新打包(Repack)基本上将承诺 packfile 和非承诺 packfile 视为两个不同的分区,并且不混合它们。

  • 动态对象抓取会为每个项目调用一次 fetch-pack,因为大多数算法在遇到缺失对象时会中断,需要解决该对象才能继续工作。如果需要许多对象,这可能会导致显著的开销——以及多次身份验证请求。

  • 动态对象抓取目前使用现有的 pack 协议 V0,这意味着每个对象都通过 fetch-pack 请求。当连接建立时,服务器将发送一组完整的 info/refs。如果 ref 数量庞大,这可能会导致显著的开销。

未来工作

  • 改进指定尝试承诺远程仓库顺序的方式。

    例如,这可以允许明确指定类似:“从这个远程仓库抓取时,我希望按这个顺序使用这些承诺远程仓库;然而,当推送到或抓取到那个远程仓库时,我希望按那个顺序使用那些承诺远程仓库。”

  • 允许推送到承诺远程仓库。

    用户可能希望采用三角工作流,使用多个承诺远程仓库,每个远程仓库对仓库都有不完整的视图。

  • 允许非基于路径名的过滤器利用 packfile 位图(如果存在)。这只是初始实现时的遗漏。

  • 研究使用长时间运行的进程动态抓取一系列对象,例如 [5,6] 中提出的方法,以减少进程启动和开销成本。

    如果 pack 协议 V2 能够允许长时间运行的进程通过单个长时间连接进行一系列请求,那将是很好的。

  • 研究 pack 协议 V2,以避免在与服务器的每次连接上广播 info/refs 来动态抓取缺失对象。

  • 研究处理松散承诺对象的必要性。

    承诺 packfile 中的对象被允许引用可以从服务器动态抓取的缺失对象。我们曾假设松散对象只在本地创建,因此不应引用缺失对象。我们可能需要重新审视这个假设,例如,如果我们动态抓取一个缺失的树并将其存储为松散对象而非单个对象 packfile。

    这不一定意味着我们需要将松散对象标记为承诺对象;放宽对象查找或 is-promisor 函数可能就足够了。

非任务

  • 每次提到“按需加载二进制大对象”时,似乎总有人建议允许服务器“猜测”并发送可能与请求对象相关的额外对象。

    我们没有为此做任何实际工作;我们只是记录这是一个常见的建议。我们不确定它将如何运作,也没有计划在这方面开展工作。

    服务器发送比请求更多的对象是有效的(即使是动态对象抓取),但我们并未以此为基础进行构建。

脚注

[a] 修改成本高昂的缺失对象列表:在部分克隆的早期设计中,我们讨论了需要一个单一的缺失对象列表。这本质上是一个排序的 OID 线性列表,这些 OID 在克隆或后续抓取期间被服务器省略了。

该文件需要在每次对象查找时加载到内存中。它需要被读取、更新和重写(像 .git/index 文件一样),在每次显式的“git fetch”命令任何动态对象抓取时都如此。

如果存在大量缺失对象,读取、更新和写入此文件的成本可能会为每个命令增加显著的开销。例如,如果存在 1 亿个缺失二进制大对象,此文件在磁盘上将至少占用 2GiB。

有了“承诺”概念,我们根据引用它的 packfile 类型推断缺失对象。

[0] https://crbug.com/git/2 Bug#2: 部分克隆

[1] https://lore.kernel.org/git/20170113155253.1644-1-benpeart@microsoft.com/
主题:[RFC] 添加按需下载二进制大对象(blobs)的支持
日期:2017年1月13日 星期五 10:52:53 -0500

[2] https://lore.kernel.org/git/cover.1506714999.git.jonathantanmy@google.com/
主题:[PATCH 00/18] 部分克隆(从克隆到懒惰抓取,共18个补丁)
日期:2017年9月29日 星期五 13:11:36 -0700

[3] https://lore.kernel.org/git/20170426221346.25337-1-jonathantanmy@google.com/
主题:Git 仓库中缺失二进制大对象支持的提案
日期:2017年4月26日 星期三 15:13:46 -0700

[4] https://lore.kernel.org/git/1488999039-37631-1-git-send-email-git@jeffhostetler.com/
主题:[PATCH 00/10] RFC 部分克隆和抓取
日期:2017年3月8日 星期三 18:50:29 +0000

[5] https://lore.kernel.org/git/20170505152802.6724-1-benpeart@microsoft.com/
主题:[PATCH v7 00/10] 将过滤器处理代码重构为一个可重用模块
日期:2017年5月5日 星期五 11:27:52 -0400

[6] https://lore.kernel.org/git/20170714132651.170708-1-benpeart@microsoft.com/
主题:[RFC/PATCH v2 0/1] 添加按需下载二进制大对象(blobs)的支持
日期:2017年7月14日 星期五 09:26:50 -0400

scroll-to-top