管理 monorepos

单体代码库已经成为开发团队工作流程中的常规部分。虽然它们有许多优点,但在极狐GitLab 中使用时,单体代码库可能会带来性能挑战。因此,您应该了解:

  • 哪些存储库特性可能会影响性能。
  • 一些优化单体代码库的工具和步骤。

对性能的影响#

由于极狐GitLab 是一个基于 Git 的系统,因此在处理大小为 GB 的大型存储库时,它受到与 Git 类似的性能限制。

单体代码库可能由于多种原因而变得庞大。

在极狐GitLab 中使用大型存储库时,尤其是一个大型单体代码库每天收到许多克隆或推送时,性能风险尤为突出,这是它们的常见情况。

大型存储库的 Git 性能问题#

Git 使用 packfiles 来存储其对象,以尽可能减少占用的空间。Packfiles 也用于在 Git 客户端和 Git 服务器之间进行克隆、获取或推送时传输对象。使用 packfiles 通常是好的,因为它减少了所需的磁盘空间和网络带宽。

然而,创建 packfiles 需要大量的 CPU 和内存来压缩对象内容。因此,当存储库很大时,每个需要创建 packfiles 的 Git 操作变得昂贵且缓慢,因为需要处理和传输更多和更大的对象。

对极狐GitLab 的影响#

Gitaly 是我们构建在 Git 之上的 Git 存储服务。这意味着 Git 的任何限制都会在 Gitaly 中体验到,进而影响极狐GitLab 的最终用户。

单体代码库也会显著影响硬件,在某些情况下会达到如垂直扩展和网络或磁盘带宽限制等限制。

优化极狐GitLab 设置#

您应该尽可能多地使用以下策略来最小化 Gitaly 服务器上的获取次数。

理论依据#

Git 中资源最密集的操作是 git-pack-objects 过程,负责在计算出所有提交历史和要发送回客户端的文件后创建 packfiles。

存储库越大,提交、文件、分支和标签越多,这个操作就越昂贵。内存和 CPU 在这个操作中被严重利用。

大多数 git clonegit fetch 流量(在服务器上启动 git-pack-objects 过程)通常来自自动化持续集成系统,例如极狐GitLab CI/CD 或其他 CI/CD 系统。如果存在大量这样的流量,对大型存储库进行大量克隆可能会对服务器造成重大压力。

Gitaly pack-objects 缓存#

启用 Gitaly pack-objects 缓存,它减少了服务器在克隆和获取时的工作量。

理论依据#

pack-objects 缓存 缓存了 git-pack-objects 过程产生的数据。这个响应被发送回发起克隆或获取的 Git 客户端。如果几个获取请求相同的引用集,Gitaly 服务器上的 Git 就不必在每次克隆或获取调用时重新生成响应数据,而是从 Gitaly 维护的内存缓存中提供该数据。

这在单个存储库的高克隆率情况下可以大大帮助。

有关更多信息,请参见 Pack-objects 缓存

减少 CI/CD 中的并发克隆#

CI/CD 负载往往是并发的,因为流水线在 设定时间 调度。因此,存储库的 Git 请求在这些时间段可能显著增加,导致 CI/CD 和用户性能下降。

通过 错开时间 来减少 CI/CD 流水线的并发性,使其在不同时间运行。例如,一组在某个时间运行,另一组在几分钟后运行。

浅克隆#

在您的 CI/CD 系统中,在 git clonegit fetch 调用中设置 --depth 选项。

极狐GitLab 和极狐GitLab Runner 默认执行 浅克隆

如果可能,设置克隆深度为一个较小的数字,比如 10。浅克隆使 Git 仅请求给定分支的最新更改集,直到所需的提交数量。

这显著加快了从 Git 存储库获取更改的速度,尤其是当存储库有一个由多个大文件组成的很长的积压时,因为我们有效地减少了数据传输量。

以下极狐GitLab CI/CD 流水线配置示例设置了 GIT_DEPTH

yaml
1variables: 2 GIT_DEPTH: 10 3 4test: 5 script: 6 - ls -al

避免用于开发的浅克隆#

避免用于开发的浅克隆,因为它们会大大增加推送更改所需的时间。浅克隆在 CI/CD 作业中效果很好,因为在检出后存储库内容不会改变。

相反,对于本地开发,请使用部分克隆来:

  1. 筛选掉 blobs:

    shell
    git clone --filter=blob:none
  2. 筛选掉树:

    shell
    git clone --filter=tree:0

有关更多信息,请参见 减少克隆大小

Git 策略#

如果可能保留存储库的工作副本,请在 CI/CD 系统上使用 git fetch 而不是 git clone

默认情况下,极狐GitLab 配置为使用 fetch Git 策略,推荐用于大型存储库。

理论依据#

git clone 从头获取整个存储库,而 git fetch 只请求服务器提供存储库中不存在的引用。自然地,git fetch 使服务器的工作量减少。git-pack-objects 不必遍历所有分支和标签并将所有内容汇总到要发送的响应中。相反,它只需要处理要打包的引用子集。该策略还减少了数据传输量。

Git 克隆路径#

GIT_CLONE_PATH 允许您控制存储库的克隆位置。如果您使用大型存储库进行基于分叉的工作流,这可能会产生影响。

从极狐GitLab Runner 的角度来看,分叉存储为具有单独工作树的单独存储库。这意味着极狐GitLab Runner 不能优化工作树的使用,您可能需要指示极狐GitLab Runner 使用它。

在这种情况下,理想情况下,您希望极狐GitLab Runner 执行器仅用于给定项目,而不是跨不同项目共享,以提高效率。

GIT_CLONE_PATH 必须位于 $CI_BUILDS_DIR 设置的目录中。您不能从磁盘中选择任何路径。

Git 清理标志#

GIT_CLEAN_FLAGS 允许您控制是否需要为每个 CI/CD 作业执行 git clean 命令。默认情况下,极狐GitLab 确保:

  • 您的工作树位于给定的 SHA 上。
  • 您的存储库是干净的。

GIT_CLEAN_FLAGS 在设置为 none 时被禁用。在非常大的存储库中,这可能是理想的,因为 git clean 是磁盘 I/O 密集的。通过 GIT_CLEAN_FLAGS: -ffdx -e .build/(例如)控制可以控制和禁用在后续运行之间删除工作树中的某些目录,这可以加快增量构建。如果您重用现有机器并有一个可以重用的现有工作树进行构建,这会产生最大的效果。

对于 GIT_CLEAN_FLAGS 接受的确切参数,请参阅 git clean 的文档。可用参数依赖于 Git 版本。

Git 获取额外标志#

GIT_FETCH_EXTRA_FLAGS 允许您通过传递额外标志来修改 git fetch 行为。

例如,如果您的项目包含大量标签,而您的 CI/CD 作业不依赖于这些标签,您可以添加 --no-tags 到额外标志以使获取更快和更紧凑。

此外,在存储库没有大量标签的情况下,--no-tags 在某些情况下可以产生很大影响。如果您的 CI/CD 构建不依赖于 Git 标签,设置 --no-tags 是值得尝试的。

有关更多信息,请参阅 GIT_FETCH_EXTRA_FLAGS 文档

配置 Gitaly 协商超时#

当尝试获取或归档时,您可能会遇到 fatal: the remote end hung up unexpectedly 错误:

  • 大型存储库。
  • 并行的多个存储库。
  • 并行的同一个大型存储库。

您可以尝试通过增加默认协商超时值来缓解此问题。有关更多信息,请参阅 配置协商超时

优化您的存储库#

保持极狐GitLab 在您的单体代码库中可扩展的另一种途径是优化存储库本身。

存储库分析#

大型存储库通常在 Git 中遇到性能问题。了解您的存储库为何庞大可以帮助您开发减轻策略以避免性能问题。

您可以使用 git-sizer 获取存储库特性快照并发现单体代码库的问题方面。

要获取存储库的 完整 克隆,您需要一个完整的 Git 镜像或裸克隆以确保所有 Git 引用都存在。要分析您的存储库:

  1. 安装 git-sizer

  2. 获取存储库的完整克隆:

    shell
    git clone --mirror <git_repo_url>

    克隆后,存储库将处于与 git-sizer 兼容的裸 Git 格式。

  3. 在您的 Git 存储库目录中运行 git-sizer,获取所有统计信息:

    shell
    git-sizer -v

处理后,git-sizer 的输出应类似于以下内容,并显示存储库各方面的关注级别:

shell
1Processing blobs: 1652370 2Processing trees: 3396199 3Processing commits: 722647 4Matching commits to trees: 722647 5Processing annotated tags: 534 6Processing references: 539 7| Name | Value | Level of concern | 8| ---------------------------- | --------- | ------------------------------ | 9| Overall repository size | | | 10| * Commits | | | 11| * Count | 723 k | * | 12| * Total size | 525 MiB | ** | 13| * Trees | | | 14| * Count | 3.40 M | ** | 15| * Total size | 9.00 GiB | **** | 16| * Total tree entries | 264 M | ***** | 17| * Blobs | | | 18| * Count | 1.65 M | * | 19| * Total size | 55.8 GiB | ***** | 20| * Annotated tags | | | 21| * Count | 534 | | 22| * References | | | 23| * Count | 539 | | 24| | | | 25| Biggest objects | | | 26| * Commits | | | 27| * Maximum size [1] | 72.7 KiB | * | 28| * Maximum parents [2] | 66 | ****** | 29| * Trees | | | 30| * Maximum entries [3] | 1.68 k | * | 31| * Blobs | | | 32| * Maximum size [4] | 13.5 MiB | * | 33| | | | 34| History structure | | | 35| * Maximum history depth | 136 k | | 36| * Maximum tag depth [5] | 1 | | 37| | | | 38| Biggest checkouts | | | 39| * Number of directories [6] | 4.38 k | ** | 40| * Maximum path depth [7] | 13 | * | 41| * Maximum path length [8] | 134 B | * | 42| * Number of files [9] | 62.3 k | * | 43| * Total size of files [9] | 747 MiB | | 44| * Number of symlinks [10] | 40 | | 45| * Number of submodules | 0 | |

在此示例中,一些项目具有较高的关注级别。有关解决这些问题的信息,请参见以下部分:

  • 大量引用。
  • 大型 blobs。

大量引用#

Git 中的引用是指向特定提交的分支和标签名称。您可以使用 git for-each-ref 命令列出存储库中存在的所有引用。存储库中的大量引用会对命令的性能产生不利影响。要理解原因,我们需要了解 Git 如何存储引用并使用它们。

通常情况下,Git 将所有引用存储为存储库 .git/refs 文件夹中的松散文件。随着引用数量的增加,查找特定引用的搜索时间也增加。因此,每次 Git 需要解析引用时,文件系统的搜索时间增加导致延迟。

为了解决这个问题,Git 使用 pack-refs。简而言之,Git 创建一个包含存储库所有引用的单一 .git/packed-refs 文件,而不是为每个引用存储单独的文件。这个文件减少了存储空间,同时提高了性能,因为在单个文件中进行搜索比在目录中搜索文件更快。然而,创建和更新新引用仍然通过松散文件进行,并且不会添加到 packed-refs 文件中。要重新创建 packed-refs 文件,请运行 git pack-refs

Gitaly 在 维护 期间运行 git pack-refs,以将松散引用移动到 packed-refs 文件中。虽然这对大多数存储库非常有益,但写入频繁的存储库仍然面临以下问题:

  • 创建或更新引用会创建新的松散文件。
  • 删除引用涉及修改现有 packed-refs 文件以彻底删除现有引用。

这些问题仍然会导致相同的性能问题。

此外,从存储库中获取和克隆包括从服务器到客户端传输缺失对象。当存在大量引用时,Git 会迭代所有引用并对每个引用的内部图结构进行遍历,以查找要传输给客户端的缺失对象。迭代和遍历是 CPU 密集型操作,会增加这些命令的延迟。

在活动频繁的存储库中,这通常会导致多米诺效应,因为每个操作都更慢,而每个操作都会阻塞后续操作。

缓解策略#

为了减轻单体代码库中大量引用的影响:

  • 创建一个自动化过程来清理旧分支。

  • 如果某些引用不需要对客户端可见,请使用 transfer.hideRefs 配置设置隐藏它们。因为 Gitaly 忽略任何服务器上的 Git 配置,您必须在 /etc/gitlab/gitlab.rb 中更改 Gitaly 配置本身:

    ruby
    1gitaly['configuration'] = { 2 # ... 3 git: { 4 # ... 5 config: [ 6 # ... 7 { key: "transfer.hideRefs", value: "refs/namespace_to_hide" }, 8 ], 9 }, 10}

在 Git 2.42.0 及更高版本中,执行不同的 Git 操作时可以跳过隐藏引用。

大型 blobs#

Blobs 是用于存储和管理用户提交到 Git 存储库中的文件内容的 Git 对象

大型 blobs 的问题#

大型 blobs 对 Git 来说可能是个问题,因为 Git 不高效处理大型二进制数据。在 git-sizer 输出中超过 10 MB 的 blobs 可能意味着您的存储库中有大型二进制数据。

虽然源代码通常可以有效压缩,但二进制数据通常已经压缩。这意味着 Git 在创建 packfiles 时尝试压缩大型 blobs 时可能不会成功。这导致更大的 packfiles 和更高的 CPU、内存和带宽使用率,无论是在 Git 客户端还是服务器端。

在客户端方面,因为 Git 将 blob 内容存储在 packfiles 中(通常位于 .git/objects/pack/)和常规文件中(在 工作树 中),通常比源代码需要更多的磁盘空间。

使用 LFS 存储大型 blobs#

将二进制或 blob 文件(例如软件包、音频、视频或图形)存储为大文件存储 (LFS) 对象。使用 LFS 时,对象存储在外部,例如在对象存储中,这减少了存储库中的对象数量和大小。在外部对象存储中存储对象可以提高性能。

有关更多信息,请参阅 Git LFS 文档

参考架构#

大型存储库往往存在于拥有众多用户的大型组织中。极狐GitLab 测试平台和支持团队提供了几种 参考架构,这是在规模上部署极狐GitLab 的推荐方法。

在这些类型的设置中,使用的极狐GitLab 环境应符合参考架构以提高性能。