Git

Git 包是存储打包文件以及一些额外元数据的文件,包括一组引用和一组(可能为空)必要的提交。有关更多信息,请参阅 git-bundle[1]gitformat-bundle[5]

包 URI 是 Git 可以下载一个或多个包的位置,以便在从远程获取剩余对象之前预先引导对象数据库。

一个目标是加快网络连接到源服务器较差的用户克隆和获取速度。另一个好处是允许大量用户(例如 CI 构建农场)使用本地资源存储大部分 Git 数据,从而减少源服务器的负载。

要启用包 URI 功能,用户可以使用命令行选项指定包 URI,或者源服务器可以通过协议 v2 功能宣传一个或多个 URI。

设计目标

包 URI 标准旨在足够灵活以满足多种工作负载。包提供者和 Git 客户端在如何创建和使用包 URI 方面有几个选择。

  • 包可以具有服务器所需的任何名称。此名称可以通过使用包内容的哈希来引用不可变数据。但是,这意味着在每次更新内容后都需要一个新的 URI。如果服务器正在宣传 URI(并且服务器知道正在生成新包),这可能是可以接受的,但对于使用命令行选项的用户来说并不符合人体工程学。

  • 这些包可以专门用于引导完整的克隆,但也可以用于引导增量获取。包提供者必须从几种组织方案中选择一种以最大程度地减少增量获取期间客户端的下载量,但 Git 客户端也可以选择是否将包用于这两个操作中的任何一个。

  • 包提供者可以选择支持完整克隆、部分克隆或两者兼而有之。客户端可以检测哪些包适合存储库的部分克隆过滤器(如果有)。

  • 包提供者可以使用单个包(仅用于克隆)或包列表。在使用包列表时,提供者可以指定客户端是否需要所有匹配其存储库要求的包 URI,或者任何一个包 URI 就足够了。这允许包提供者为不同区域使用不同的 URI。

  • 包提供者可以使用启发式方法(例如创建令牌)来组织包,以帮助客户端避免下载不需要的包。当包提供者不提供这些启发式方法时,客户端可以使用优化来最大程度地减少下载的数据量。

  • 包提供者不需要与 Git 服务器相关联。客户端可以选择使用包提供者,而无需 Git 服务器宣传它。

  • 客户端可以选择发现 Git 服务器宣传的包提供者。这可能发生在git clone期间、git fetch期间、两者兼而有之或两者都不发生。用户可以选择最适合他们的组合。

  • 客户端可以选择随时手动配置包提供者。客户端还可以选择在git clone的命令行选项中手动指定包提供者。

每个存储库都不同,每个 Git 服务器都有不同的需求。希望包 URI 功能足够灵活以满足所有需求。如果不是,则可以通过其版本控制机制扩展该功能。

服务器要求

要提供包服务器的服务器端实现,不需要 Git 协议的其他部分。这允许服务器维护者使用静态内容解决方案(例如 CDN)来提供包文件。

在包 URI 功能的当前范围内,所有 URI 都应为 HTTP(S) URL,其中内容使用对该 URL 的GET请求下载到本地文件。服务器可以将身份验证要求包含在这些请求中,目的是触发已配置的凭据助手以进行安全访问。(未来的扩展可以使用“file://”URI 或 SSH URI。)

假设服务器返回200 OK响应,则会检查 URL 中的内容。首先,Git 尝试将文件解析为版本 2 或更高版本的包文件。如果文件不是包,则使用 Git 的配置解析器将文件解析为纯文本文件。该配置文件中的键值对预计描述了包 URI 列表。如果这两个解析尝试均未成功,则 Git 将向用户报告错误,指出提供的包 URI 数据错误。

服务器提供的任何其他数据都被视为错误。

包列表

Git 服务器可以使用一组key=value对宣传包 URI。包 URI 还可以提供 Git 配置格式的纯文本文件,其中包含这些相同的key=value对。在这两种情况下,我们都认为这是包列表。这些对指定有关客户端可以用来决定下载哪些包以及忽略哪些包的信息。

一些键侧重于列表本身的属性。

bundle.version

(必需)此值提供包列表的版本号。如果未来的 Git 更改启用了需要 Git 客户端对包列表文件中的新键做出反应的功能,则此版本将递增。当前唯一的版本号是 1,如果指定了任何其他值,则 Git 将无法使用此文件。

bundle.mode

(必需)此值具有两个值之一:allany。当指定all时,客户端应预期需要所有与其实现的存储库要求匹配的列出的包 URI。当指定any时,客户端应预期任何一个与其实现的存储库要求匹配的包 URI 就足够了。通常,any选项用于列出位于不同区域的许多不同的包服务器。

bundle.heuristic

如果存在此字符串值键,则包列表旨在与增量git fetch命令配合使用。启发式信号表示每个包都有其他键可用,这些键有助于确定客户端应下载哪些包的子集。目前唯一计划的启发式方法是creationToken

其余键包括一个<id>段,它是服务器为每个可用包指定的名称。<id>必须仅包含字母数字和-字符。

bundle.<id>.uri

(必需)此字符串值是下载包<id>的 URI。如果 URI 以协议(http://https://)开头,则 URI 为绝对 URI。否则,将 URI 解释为相对于用于包列表的 URI。如果 URI 以/开头,则该相对路径相对于用于包列表的域名。(使用相对路径是为了更容易地跨大量具有不同域名的服务器或 CDN 分发一组包。)

bundle.<id>.filter

此字符串值表示也应出现在此包标头中的对象过滤器。服务器使用此值来区分不同类型的包,客户端可以从中选择与其实现的对象过滤器匹配的包。

bundle.<id>.creationToken

此值是一个非负的 64 位整数,用于对包列表进行排序。当bundle.heuristic=creationToken时,这用于在获取期间下载包的子集。

bundle.<id>.location

此字符串值宣传提供包 URI 的真实世界位置。这可用于向用户提供使用哪个包 URI 的选项,或者只是作为所选包 URI 的信息指示器。仅当bundle.modeany时,此功能才有价值。

以下是用 Git 配置格式编写的包列表示例

[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/heads/*:refs/bundles/*,但可以配置。这些ref被存储起来,以便以后的git fetch协商可以将每个bundle的ref作为have进行通信,从而减小通过Git协议获取的大小。为了允许从该ref命名空间中修剪ref,Git可能会引入一个编号命名空间(例如refs/bundles/<i>/*),以便可以删除过时的bundle ref。

  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头以确定所有已发布的提示是否已存在于客户端仓库中,并取消其余下载。

使用Bundle URI获取

当客户端获取新数据时,它可以决定在从原始远程获取之前从bundle服务器获取。这可以通过命令行选项完成,但更有可能使用在克隆期间指定的配置值,例如。

获取操作遵循相同的过程从bundle列表下载bundle(尽管我们希望在此处使用并行下载)。我们预计该过程将在瘦bundle中的所有先决条件提交OID都已存在于对象数据库中时结束。

当使用creationToken启发式方法时,如果bundle的创建令牌不大于存储的创建令牌,则客户端可以避免下载任何bundle。获取新的bundle后,Git会更新此本地创建令牌。

如果bundle提供者未提供启发式方法,则客户端应在下载完整bundle数据之前尝试检查bundle头,以防bundle提示已存在于客户端仓库中。

错误条件

如果Git客户端在根据bundle URI或该位置找到的bundle列表下载信息时发现意外情况,则Git可以忽略该数据并继续,就像没有给出bundle URI一样。远程Git服务器是最终的真相来源,而不是bundle URI。

以下是一些示例错误条件

  • 客户端无法连接到给定URI上的服务器,或者连接丢失且无法恢复。

  • 客户端收到400级响应(例如404 Not Found401 Not Authorized)。客户端应使用凭据助手查找并提供URI的凭据,但在处理特定400级错误方面与Git的其他HTTP协议的语义相匹配。

  • 服务器报告任何其他故障响应。

  • 客户端接收的数据无法解析为bundle或bundle列表。

  • bundle包含与预期不符的过滤器。

  • 客户端无法解包bundle,因为先决条件提交OID不在对象数据库中,并且没有更多bundle可供下载。

还有一些情况可能被视为浪费,但不是错误条件

  • 下载的bundle包含比克隆或获取请求请求的更多信息。一个主要示例是,如果用户使用--single-branch请求克隆,但下载存储来自所有refs/heads/*引用的所有可到达提交的bundle。这最初可能是一种浪费,但也许这些对象会因客户端关心的以后的ref更新而变得可到达。

  • 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服务器可以在https://<server-url>/<domain>/<org>/<repo>提供其bundle列表。原始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列表的更新,例如每天一次。在此任务期间,服务器从原始服务器获取最新内容,并生成一个包含从最新原始ref可到达的对象的bundle,但不包含在先前计算的bundle中。此bundle将添加到列表中,注意creationToken严格大于先前的最大creationToken

当bundle列表变得太大时,例如超过30个bundle,则将最旧的“N减30”个bundle合并到一个bundle中。此bundle的creationToken等于合并的bundle中的最大creationToken

此处提供了一个bundle列表示例,尽管它只有两个每日bundle,而不是完整的30个bundle列表

[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中。30天后,每日bundle将合并到最旧的bundle中。

建议在使用blob:none过滤器重复此捆绑策略,如果此存储库的客户端期望使用无 Blob 的部分克隆。此无 Blob 捆绑包列表与完整捆绑包列表位于同一列表中,但使用bundle.<id>.filter键将这两组分开。对于非常大的存储库,捆绑包提供者可能只想提供无 Blob 的捆绑包。

实施计划

本设计文档作为一份愿望文档单独提交,目标是在多个补丁系列的过程中实现所有提到的客户端功能。以下是提交这些功能的潜在概述

  1. 将捆绑包 URI 集成到git clone中,并使用--bundle-uri选项。这将包括一个新的git fetch --bundle-uri模式,用作git clone底层的实现。此处的初始版本将期望在给定的 URI 中有一个捆绑包。

  2. 实现从捆绑包 URI 解析捆绑包列表的功能,并更新git fetch --bundle-uri逻辑以正确区分bundle.mode选项。具体来说,设计该功能以便配置格式解析将键值对列表馈送到捆绑包列表逻辑中。

  3. 创建bundle-uri协议 v2 命令,以便 Git 服务器可以使用键值对来宣传捆绑包 URI。插入到捆绑包列表逻辑中现有的键值输入中。允许git clone发现这些捆绑包 URI 并从捆绑包数据引导客户端存储库。(此选择是通过配置选项和命令行选项进行选择加入的。)

  4. 允许客户端理解bundle.heuristic配置键和bundle.<id>.creationToken启发式。当git clone发现具有bundle.heuristic的捆绑包 URI 时,它会配置客户端存储库以在以后的git fetch <remote>命令期间检查该捆绑包 URI。

  5. 允许客户端在git fetch期间发现捆绑包 URI,并在设置bundle.heuristic时为以后的获取配置捆绑包 URI。

  6. 实现“检查报头”启发式,以在bundle.<id>.creationToken启发式不可用时减少数据下载量。

随着这些功能的审查,此计划可能会更新。我们还预计,随着此功能的成熟并在实际场景中使用,将发现并实现新的设计。

Git 协议已经具有服务器在服务客户端请求时,可以将一组 URL 与 packfile 响应一起列出的功能。然后,客户端需要在这些位置下载 packfile,以便全面了解响应。

此机制由 Gerrit 服务器(使用 JGit 实现)使用,并且在减少 CPU 负载和提高克隆的用户性能方面非常有效。

此机制的一个主要缺点是,源服务器需要准确地知道这些 packfile 中的内容,并且在服务器响应一段时间后,用户需要能够访问这些 packfile。这种源服务器和 packfile 数据之间的耦合难以管理。

此外,这种实现很难与获取一起使用。

GVFS 协议 [2] 是一组 HTTP 端点,在创建 Git 的部分克隆之前独立于 Git 项目设计。此协议的一个功能是“缓存服务器”的概念,该服务器可以与构建机器或开发人员办公室位于同一位置,以传输 Git 数据而不会过载中央服务器。

VFS for Git 著名的端点是GET /gvfs/objects/{oid}端点,它允许按需下载对象。这是该产品文件系统虚拟化的关键部分。

但是,更细微的需求是GET /gvfs/prefetch?lastPackTimestamp=<t>端点。给定一个可选的时间戳,缓存服务器将响应一个预计算的 packfile 列表,其中包含在这些时间间隔内引入的提交和树。

缓存服务器使用以下策略计算这些“预取”packfile

  1. 每小时生成一个具有给定时间戳的“每小时”pack。

  2. 每晚,将前 24 个每小时的 pack 合并到一个“每天”的 pack 中。

  3. 每晚,将所有超过 30 天的预取 pack 合并到一个 pack 中。

当用户对具有缓存服务器的存储库运行gvfs clonescalar clone时,客户端将请求所有预取 packfile,最多为24 + 30 + 1个 packfile,仅下载提交和树。然后,客户端会向源服务器请求引用,并尝试检出该顶端引用。(还有一个额外的端点可以帮助从给定的提交中获取所有可到达的树,以防该提交尚未位于预取 packfile 中。)

git fetch期间,一个钩子使用先前下载的预取 packfile 中的最新时间戳请求预取端点。仅下载具有较晚时间戳的 packfile 列表。大多数用户每小时获取一次,因此他们最多获取一个每小时的预取 pack。机器已关闭或以其他方式未获取超过 30 天的用户可能会重新下载所有预取 packfile。这种情况很少见。

需要注意的是,客户端始终联系源服务器以获取 ref 公告,因此 ref 经常“领先于”预取的 pack 数据。在需要时,例如通过git checkoutgit log等命令,使用GET gvfs/objects/{oid}请求按需下载缺少的对象。一些 Git 优化禁用了会导致这些按需下载过于激进的检查。

另请参阅

scroll-to-top