Git
English ▾ 主题 ▾ 最新版本 ▾ 部分克隆上次更新于 2.43.0

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

在克隆和获取操作期间,Git 会下载存储库的完整内容和历史记录。这包括存储库整个生命周期中的所有提交、树和 Blob。对于极大的存储库,克隆可能需要数小时(或数天)并消耗 100 多 GB 的磁盘空间。

通常在这些存储库中,用户不需要许多 Blob 和树,例如:

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

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

部分克隆允许我们在克隆和获取操作期间提前避免下载这些不需要的对象,从而减少下载时间和磁盘使用量。如果/何时需要,稍后可以“按需获取”丢失的对象。

稍后可以提供丢失对象的远程称为承诺者远程,因为它承诺在请求时发送对象。最初,Git 只支持一个承诺者远程,即用户从中克隆的 origin 远程,并在“extensions.partialClone”配置选项中进行配置。后来实现了对多个承诺者远程的支持。

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

非目标

部分克隆是一种限制在给定提交范围内下载的 Blob 和树数量的机制,因此独立于并且不打算与现有的 DAG 级机制冲突以限制请求的提交集(即浅克隆、单个分支或获取 <refspec>)。

设计概述

部分克隆在逻辑上包括以下部分:

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

  • 服务器从发送到客户端的包文件中省略此类不需要的对象的机制。

  • 客户端优雅地处理丢失的对象(之前由服务器省略)的机制。

  • 客户端根据需要填充丢失对象的机制。

设计细节

  • 新的包协议功能“filter”被添加到 fetch-pack 和 upload-pack 协商中。

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

  • 客户端将“filter-spec”传递给克隆和获取,该规范被传递给服务器以在包文件构造期间请求过滤。

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

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

    这些过滤后的包文件在传统意义上是不完整的,因为它们可能包含引用包文件中未包含的对象的对象,并且客户端还没有这些对象。例如,过滤后的包文件可能包含引用丢失 Blob 的树或引用丢失树的提交的标签。

  • 在客户端上,这些不完整的包文件被标记为“承诺者包文件”,并由各种命令以不同的方式处理。

  • 在客户端上,将存储库扩展添加到本地配置中,以防止旧版本的 Git 由于无法处理的丢失对象而在操作过程中失败。请参阅 Documentation/technical/repository-version.txt 中的“extensions.partialClone”。

处理丢失的对象

  • 对象可能由于部分克隆或获取而丢失,或者由于存储库损坏而丢失。为了区分这些情况,本地存储库专门将从承诺者远程获取的此类过滤后的包文件指示为“承诺者包文件”。

    这些承诺者包文件除了其“<name>.pack”和“<name>.idx”文件外,还包含一个具有任意内容的“<name>.promisor”文件(如“<name>.keep”文件)。

  • 本地存储库认为“承诺者对象”是指它(尽其所能)知道承诺者远程已承诺拥有该对象的对象,要么是因为本地存储库在其承诺者包文件之一中拥有该对象,要么是因为另一个承诺者对象引用了它。

    当 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已更新为根本不接触承诺者包文件,并且只重新打包其他对象。

  • 全局变量“fetch_if_missing”用于控制对象查找是否尝试动态获取丢失的对象或报告错误。

    我们对这个全局变量不满意,并希望删除它,但这需要对对象代码进行大量重构以传递额外的标志。

获取丢失的对象

  • 对象的获取是通过调用“git fetch”子进程完成的。

  • 本地仓库发送一个请求,包含所有请求对象的哈希值,并且不执行任何 packfile 协商。然后它接收一个 packfile。

  • 因为我们正在重用现有的 fetch 机制,所以当前的 fetch 会获取请求对象引用的所有对象,即使它们不是必需的。

  • 使用 --refetch 进行 fetch 将从远程请求一个全新的过滤后的 packfile,这可以用于更改过滤器,而无需动态获取缺少的对象。

使用多个 promisor 远程仓库

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

例如,这允许用户拥有多个地理位置接近的缓存服务器,用于获取缺少的 blob,同时继续从中央服务器执行过滤的 git-fetch 命令。

获取对象时,会依次尝试 promisor 远程仓库,直到所有对象都已获取。

被视为“promisor”远程仓库的远程仓库是由以下配置变量指定的

  • extensions.partialClone = <name>

  • remote.<name>.promisor = true

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

只能使用 extensions.partialClone 配置变量配置一个 promisor 远程仓库。这个 promisor 远程仓库将在获取对象时最后尝试。

我们决定将其设置为最后尝试,因为使用多个 promisor 远程仓库的用户很可能是因为其他 promisor 远程仓库出于某种原因(可能是它们更接近或对于某些类型的对象更快)比 origin 更好,而 origin 很可能是由 extensions.partialClone 指定的远程仓库。

这个理由不是很有力,但必须做出一个选择,而且无论如何,长期的计划应该是使顺序以某种方式完全可配置。

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

当前限制

  • 除了按照它们在配置文件中出现的顺序之外,无法指定尝试 promisor 远程仓库的顺序。

    也无法指定从一个远程仓库获取时使用的顺序,以及从另一个远程仓库获取时使用的不同顺序。

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

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

  • 动态对象获取只会向 promisor 远程仓库请求缺少的对象。我们假设 promisor 远程仓库拥有仓库的完整视图,并且可以满足所有此类请求。

  • Repack 本质上将 promisor 和非 promisor packfile 视为两个不同的分区,并且不会将它们混合。

  • 动态对象获取会为**每个项目**调用一次 fetch-pack,因为大多数算法会遇到缺少的对象,并且需要在继续工作之前解决它。如果需要许多对象,这可能会产生很大的开销,并且会产生多次身份验证请求。

  • 动态对象获取目前使用现有的 pack 协议 V0,这意味着每个对象都是通过 fetch-pack 请求的。服务器将在连接建立时发送一组完整的 info/refs。如果存在大量 refs,这可能会产生很大的开销。

未来工作

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

    例如,这可以允许显式指定类似以下内容:“当从这个远程仓库获取时,我希望按照此顺序使用这些 promisor 远程仓库,但是,当推送到或从那个远程仓库获取时,我希望按照那个顺序使用那些 promisor 远程仓库。”

  • 允许推送到 promisor 远程仓库。

    用户可能希望在一个三角形工作流程中使用多个 promisor 远程仓库,每个远程仓库都拥有仓库的不完整视图。

  • 允许非路径名过滤器使用 packfile 位图(如果存在)。这只是在初始实现期间的一个遗漏。

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

    如果 pack 协议 V2 能够允许长期运行的进程通过单个长期运行的连接发出一系列请求,那就太好了。

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

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

    promisor packfile 中的对象可以引用缺少的对象,这些对象可以从服务器动态获取。一个假设是松散对象仅在本地创建,因此不应该引用缺少的对象。例如,如果我们动态获取一个缺少的树并将其存储为松散对象而不是单个对象 packfile,我们可能需要重新考虑这个假设。

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

非任务

  • 每次提到“按需加载 blob”主题时,似乎有人建议允许服务器“猜测”并发送可能与请求对象相关的其他对象。

    还没有投入任何工作来实际执行此操作;我们只是记录这是一个常见的建议。我们不确定它将如何工作,也没有计划对其进行工作。

    服务器发送比请求数量更多的对象是有效的(即使对于动态对象获取也是如此),但我们不会在此基础上构建。

脚注

[a] 难以修改的缺少对象列表:在部分克隆的设计初期,我们讨论了对单个缺少对象列表的需求。这基本上将是一个排序的线性 OID 列表,在克隆或后续获取过程中,服务器会省略这些 OID。

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

如果缺少许多对象,读取、更新和写入此文件的成本可能会给每个命令增加很大的开销。例如,如果存在 1 亿个缺少的 blob,则此文件在磁盘上的大小至少为 2 GiB。

使用“promisor”概念,我们根据引用它的 packfile 类型**推断**缺少的对象。

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

[1] https://lore.kernel.org/git/[email protected]/
主题:[RFC] 添加对按需下载 blob 的支持
日期:2017 年 1 月 13 日星期五 10:52:53 -0500

[2] https://lore.kernel.org/git/[email protected]/
主题:[PATCH 00/18] 部分克隆(从克隆到 18 个补丁中的延迟获取)
日期:2017 年 9 月 29 日星期五 13:11:36 -0700

[3] https://lore.kernel.org/git/[email protected]/
主题:Git 仓库中缺少 blob 支持的提案
日期:2017 年 4 月 26 日星期三 15:13:46 -0700

[4] https://lore.kernel.org/git/[email protected]/
主题:[PATCH 00/10] RFC 部分克隆和获取
日期:2017 年 3 月 8 日星期三 18:50:29 +0000

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

[6] https://lore.kernel.org/git/[email protected]/
主题:[RFC/PATCH v2 0/1] 添加对按需下载 blob 的支持
日期:2017 年 7 月 14 日星期五 09:26:50 -0400

scroll-to-top