【问题标题】:Huge Git repository checkout at post-receive hook is extremely slow接收后挂钩处巨大的 Git 存储库签出非常慢
【发布时间】:2012-11-06 13:02:29
【问题描述】:

我们正在为我们的项目使用 Git。存储库相当大(.git 文件夹大约 8Gb)。

我们在 post-receive 挂钩中使用 git checkout -f 来更新工作树。

问题在于,即使是几个稍有更改的文件也需要很长时间才能签出,大约需要 20 秒。不知道为什么这么长。

会不会是仓库大小的问题?

我应该尝试哪些步骤或工具来进一步定位和调查问题?

感谢您的帮助。

问候, 亚历克斯

【问题讨论】:

  • 一个 8GB 的​​存储库听起来你用错了 git。签出的树是否也具有相似的大小,或者您是否例如?只是把你所有的二进制文件修订版放在那里?我想我记得一些 KDE 测试存储库大约 2GB,而整个 Linux 内核历史远低于 1GB。
  • 是的,那是因为二进制文件,不知道会不会是结帐慢的原因,只是更新了两个稍有改动的文件?
  • git checkout 在使用 git 2.8(2016 年 3 月)的大型 repo 上应该更快。见my edited answer below

标签: git git-checkout


【解决方案1】:

原始答案(2012 年 11 月)

我确认,如果您将 git 目录 (.git) 保持这么大,那么 git 的运行速度会大大降低。

你可以在this thread看到一个插图(不是因为大文件,而是因为大量文件和提交历史):

测试存储库有 400 万次提交、线性历史记录和大约 130 万个文件。
.git目录的大小约为15GB,已经用'重新打包了

git repack -a -d -f --max-pack-size=10g --depth=100 --window=250

在一台强大的机器上重新打包大约需要 2 天时间(即,大量的内存和闪存)。
索引文件大小为 191 MB。

至少,您可以考虑拆分存储库,将二进制文件隔离在自己的 git 存储库中,并使用 submodules 来跟踪源存储库和二进制存储库。

最好将大型二进制文件(尤其是生成的)存储在源引用之外。
建议使用“工件”存储库,例如 Nexus。

出现的所有 git 解决方案保留这些二进制文件是 git-annex 或 git-media,如“How to handle a large git repository?”中所示。


2016 年 2 月更新:git 2.8(2016 年 3 月)应该会显着提高 git checkout 的性能。

参见commit a672095(2016 年 1 月 22 日)和 commit d9c2bd5(2015 年 12 月 21 日)David Turner (dturner-tw)。
(由 Junio C Hamano -- gitster -- 合并于 commit 201155c,2016 年 2 月 3 日)支持>

unpack-trees:修复意外的二次行为

在解包树时(例如在 git checkout 期间),当我们遇到一个超出我们路径的缓存条目时,我们会中断迭代。

这提供了大约 45% 的 git checkout 在 master 和 master^20000 在 Twitter 的 monorepo 上。
一般来说,加速取决于存储库结构、更改数量和打包文件的决定。

do_compare_entry:使用已经计算好的路径

在 traverse_trees 中,我们为 traverse_info 生成完整的遍历路径。
后来,在 do_compare_entry 中,我们过去常常在不计算路径的情况下将 traverse_info 与 cache_entry 的名称进行比较。
但既然我们已经有了这条路,我们就不需要做所有这些工作了。
相反,我们可以直接将生成的路径放入traverse_info,进行更直接的比较。

这使得git checkout 更快——在 Twitter 上大约 25% 单仓库。
较深的目录树可能比较浅的目录树受益更多
。


使用sparse-checkout,可以大大加快对大型存储库的签出速度。

Git 2.33(2021 年第三季度)进一步改善了这一点,其中“git checkout”(man) 和 git commit(man)学会了在没有不必要地扩展稀疏索引的情况下工作。

请参阅commit e05cdb1、commit 70569fa(2021 年 7 月 20 日)和 commit 1ba5f45、commit f934f1b、commit daa1ace、commit 11042ab、commit 11042ab、commit 0d53d19(2021 年 6 月 29 日)Derrick Stolee (derrickstolee)。
(由 Junio C Hamano -- gitster -- 合并于 commit 506d2a3,2021 年 8 月 4 日)

checkout:停止扩展稀疏索引

签字人:Derrick Stolee

之前的更改对unpack-trees.c 和diff-lib.c 进行了必要的改进,以便根据与树的比较来修改稀疏索引。
剩下的唯一工作是删除一些ensure_full_index() 调用并添加测试,以验证在我们感兴趣的案例中索引没有扩展。
在这些测试中包含“switch”和“restore”,因为它们与“checkout”共享一个基本实现。

以下是来自 p2000-sparse-operations.sh 的相关性能结果:

Test                                     HEAD~1           HEAD 
--------------------------------------------------------------------------------
2000.18: git checkout -f - (full-v3)     0.49(0.43+0.03)  0.47(0.39+0.05) -4.1% 
2000.19: git checkout -f - (full-v4)     0.45(0.37+0.06)  0.42(0.37+0.05) -6.7% 
2000.20: git checkout -f - (sparse-v3)   0.76(0.71+0.07)  0.04(0.03+0.04) -94.7% 
2000.21: git checkout -f - (sparse-v4)   0.75(0.72+0.04)  0.05(0.06+0.04) -93.3%  

将完整索引情况与稀疏索引情况进行比较很重要,因为之前稀疏索引的结果被索引扩展夸大了。
对于索引 v4,这是 88% 的改进。

在 HEAD 具有超过 200 万条路径的内部存储库和包含约 60,000 条路径的稀疏签出定义中,'git checkout'(man) 从 3.5 秒缩短到 297 毫秒有了这个变化。
仅存在这约 60,000 条路径的理论最优值为 275 毫秒,因此额外的稀疏目录条目会产生 22 毫秒的开销。

【讨论】:

    【解决方案2】:

    解决此问题的另一种方法是并行结帐(从 Git 2.32 开始,2021 年第二季度)。
    如this patch (still in progress) 中所述:

    本系列将并行工作器添加到结帐机器中。

    缓存条目分布在帮助进程中,这些进程负责 读取、过滤 blob 并将其写入工作树。
    这应该有利于所有调用unpack_trees() 或check_updates() 的命令, 如:checkout、clone、sparse-checkout、checkout-index等

    Local:
    
               Clone                  Checkout I             Checkout II
    Sequential  8.180 s ± 0.021 s      6.936 s ± 0.030 s      2.585 s ± 0.005 s
    10 workers  3.406 s ± 0.187 s      2.164 s ± 0.033 s      1.050 s ± 0.021 s
    Speedup     2.40 ± 0.13            3.21 ± 0.05            2.46 ± 0.05
    

    例如,对于 Git 2.32(2021 年第 2 季度),有用于并行结帐的准备性 API 更改。

    请参阅commit ae22751、commit 30419e7、commit 584a0d1、commit 49cfd90、commit d052cc0(2021 年 3 月 23 日)Matheus Tavares (matheustavares)。
    请参阅Jeff Hostetler (Jeff-Hostetler) 的commit f59d15b、commit 3e9e82c、commit 55b4ad0、commit 38e9584(2020 年 12 月 16 日)。
    (由 Junio C Hamano -- gitster -- 合并到 commit c47679d,2021 年 4 月 2 日)

    convert:添加[async_]convert_to_working_tree_ca() 变体

    签字人:Jeff Hostetler
    签字人:Matheus Tavares

    通过添加转换函数的_ca() 变体将属性收集与实际转换分开。
    这些变体接收预先计算的“struct conv_attrs”,因此不依赖于索引状态。
    它们将在未来的补丁中使用,添加并行结账支持,原因有两个:

    • 在转换之前,我们将在checkout_entry() 中加载转换属性,以确定路径是否符合并行结帐条件。
      因此,为了实际转换,稍后再次加载它们会很浪费。
    • 并行工作人员将负责读取、转换 Blob 并将其写入工作树。
      他们无法访问主进程的索引状态,因此无法加载属性。
      相反,它们将接收预加载的并调用转换函数的 _ca() 变体。
      此外,属性机制经过优化,可以按顺序处理路径,因此最好将其留给主进程。

    还有:

    在 Git 2.32(2021 年第 2 季度)中,结帐机器被教导在可能的情况下并行执行文件的实际写出。

    参见commit 68e66f2(2021 年 4 月 19 日)和commit 1c4d6f4、commit 7531e4b、commit e9e8adf、commit 04155bd(2021 年 4 月 18 日)Matheus Tavares (matheustavares)。
    (由@987654342 合并@in commit a1cac26,2021 年 4 月 30 日)

    parallel-checkout:添加配置选项

    合着:Jeff Hostetler
    署名:Matheus Tavares

    通过引入两个新设置使并行结帐可配置:>- checkout.workers 和

    • checkout.thresholdForParallelism.
      第一个定义工作人员的数量(其中一个表示顺序结账),第二个定义尝试并行结账的最小条目数。

    为了确定 checkout.workers 的默认值,并行版本在 linux repo 中的三个操作中使用冷缓存进行了基准测试:克隆 v5.8、从 v2.6.15 签出 v5.8(签出 I)和检查从 v5.7 中取出 v5.8(结帐 II)。
    下面的四个表格显示了 5 次运行的平均运行时间和标准偏差:SSD 上的本地文件系统、HDD 上的本地文件系统、Linux NFS 服务器和 Amazon EFS(都在 Linux 上)。
    每个并行结帐测试都是使用在该环境中带来最佳整体结果的工作人员数量执行的。

    本地 SSD:

                 Sequential             10 workers            Speedup
    Clone        8.805 s ± 0.043 s      3.564 s ± 0.041 s     2.47 ± 0.03 
    Checkout I   9.678 s ± 0.057 s      4.486 s ± 0.050 s     2.16 ± 0.03 
    Checkout II  5.034 s ± 0.072 s      3.021 s ± 0.038 s     1.67 ± 0.03  
    

    本地硬盘:

                 Sequential             10 workers             Speedup
    Clone        32.288 s ± 0.580 s     30.724 s ± 0.522 s    1.05 ± 0.03 
    Checkout I   54.172 s ±  7.119 s    54.429 s ± 6.738 s    1.00 ± 0.18 
    Checkout II  40.465 s ± 2.402 s     38.682 s ± 1.365 s    1.05 ± 0.07  
    

    Linux NFS 服务器(v4.1,在 EBS 上,单一可用区):

                 Sequential             32 workers            Speedup
    Clone        240.368 s ± 6.347 s    57.349 s ± 0.870 s    4.19 ± 0.13 
    Checkout I   242.862 s ± 2.215 s    58.700 s ± 0.904 s    4.14 ± 0.07 
    Checkout II  65.751 s ± 1.577 s     23.820 s ± 0.407 s    2.76 ± 0.08  
    

    EFS(v4.1,在多个可用区复制):

                 Sequential             32 workers            Speedup
    Clone        922.321 s ± 2.274 s    210.453 s ± 3.412 s   4.38 ± 0.07 
    Checkout I   1011.300 s ± 7.346 s   297.828 s ± 0.964 s   3.40 ± 0.03 
    Checkout II  294.104 s ± 1.836 s    126.017 s ± 1.190 s   2.33 ± 0.03  
    

    上述基准测试表明,并行签出对于位于 SSD 或分布式文件系统上的存储库最为有效。
    对于旋转磁盘和/或旧机器上的本地文件系统,并行性并不总能带来良好的性能。
    出于这个原因,checkout.workers 的默认值是一,也就是
    顺序结帐。

    为了确定 checkout.thresholdForParallelism 的默认值,在“本地 SSD”设置中执行了另一个基准测试,其中并行结账显示是有益的。
    这一次,我们比较了 git checkout -f(man) 在从 Linux 工作树中随机删除越来越多的文件后,有和没有并行性的运行时间。
    下面的“顺序回退”列对应于 checkout.workers 为 10 但checkout.thresholdForParallelism 等于要更新的​​文件数加一的执行(因此我们最终按顺序写入)。
    每个测试用例抽样 15 次,每个样本随机删除一组不同的文件。
    结果如下:

                 sequential fallback   10 workers           speedup
    
    10   files    772.3 ms ± 12.6 ms   769.0 ms ± 13.6 ms   1.00 ± 0.02 
    20   files    780.5 ms ± 15.8 ms   775.2 ms ±  9.2 ms   1.01 ± 0.02 
    50   files    806.2 ms ± 13.8 ms   767.4 ms ±  8.5 ms   1.05 ± 0.02 
    100  files    833.7 ms ± 21.4 ms   750.5 ms ± 16.8 ms   1.11 ± 0.04 
    200  files    897.6 ms ± 30.9 ms   730.5 ms ± 14.7 ms   1.23 ± 0.05 
    500  files   1035.4 ms ± 48.0 ms   677.1 ms ± 22.3 ms   1.53 ± 0.09 
    1000 files   1244.6 ms ± 35.6 ms   654.0 ms ± 38.3 ms   1.90 ± 0.12 
    2000 files   1488.8 ms ± 53.4 ms   658.8 ms ± 23.8 ms   2.26 ± 0.12  
    

    从以上数字来看,100 个文件似乎是阈值设置的合理默认值。

    注意:最多 1000 个文件,我们观察到并行代码的执行时间随着文件数量的增加而下降。
    这是一个相当奇怪的行为,但在多次重复中观察到。
    超过 1000 个文件,执行时间会随着文件数量的增加而增加,正如预期的那样。

    关于测试环境:本地 SSD 测试是在运行 Manjaro Linux 的 i7-7700HQ(4 核超线程)上执行的。
    本地硬盘测试在 Intel(R) Xeon(R) E3-1230(也是具有超线程的 4 个内核)、硬盘 Seagate Barracuda 7200.14 SATA 3.1、运行 Debian 上执行。
    NFS 和 EFS 测试在具有 4 个 vCPU 的 Amazon EC2 c5n.xlarge 实例上执行。
    Linux NFS 服务器在具有 2 个 vCPUS 和 1 TB EBS GP2 卷的 m6g.large 实例上运行。
    在每次计时之前,linux 存储库都被删除(或检出到之前的状态),并执行了sync && sysctl vm.drop_caches=3。

    git config 现在包含在其man page 中:

    checkout.workers

    更新工作树时使用的并行工作器数量。 默认值为 1,即顺序执行。如果设置为小于 大于 1,Git 将使用与逻辑核心数量一样多的工作人员 可用的。此设置和checkout.thresholdForParallelism 影响 所有执行结帐的命令。例如。结帐,克隆,重置, 稀疏结帐等。

    注意:并行签出通常为存储库提供更好的性能 位于 SSD 或 NFS 上。对于旋转磁盘和/或机器上的存储库 内核数量较少时,通常会执行默认的顺序签出 更好的。存储库的大小和压缩级别也可能影响如何 并行版本执行良好。

    checkout.thresholdForParallelism

    当使用少量文件运行并行检出时,成本 子进程产生和进程间通信的重要性可能超过 并行化收益。

    此设置允许定义最小值 应尝试并行检出的文件数。

    默认为 100。


    而且,仍然使用 Git 2.32(2021 年第二季度),“并行结账”的最后一部分:

    请参阅commit 87094fc、commit d590422、commit 2fa3cba、commit 6a7bc9d、commit d0e5d35、commit 70b052b、commit 6053950、commit 9616882(2021 年 5 月 4 日)Matheus Tavares (matheustavares)。
    (由Junio C Hamano -- gitster -- 合并于commit a737e1f,2021 年 5 月 16 日)

    checkout-index: 添加并行结账支持

    签字人:Matheus Tavares

    允许 checkout-index 使用并行结账框架,遵守 checkout.workers 配置。

    checkout-index 中有两个代码路径调用checkout_entry(),因此可以使用并行结账:

    • checkout_file(),用于写入在命令行中显式给出的路径;和
    • checkout_all(),当给定--all选项时,用于写入索引中的所有路径。

    在这两种操作模式下,checkout-index 不会在 checkout_entry() 失败时立即中止。
    相反,它会在使用非零退出代码退出之前尝试检查所有剩余路径。
    为了在使用并行结账时保持这种行为,我们必须允许 run_parallel_checkout() 在退出之前尝试写入排队的条目,即使我们已经从之前的 checkout_entry() 调用中得到错误代码。

    但是,checkout_all() 不会在错误时返回,它使用代码 128 调用 exit()。我们可以让它在退出之前调用 run_parallel_checkout(),但是如果我们统一退出路径cmd_checkout_index() 的两种结帐索引模式,并让此函数负责与并行结帐 API 的交互。
    所以让我们这样做吧。

    有了这个改变,我们还必须考虑是否要继续使用 128 作为 git checkout-index --all(man) 的错误代码,而我们使用 1 表示 git checkout-index(man)<path>(即使实际错误相同)。
    由于代码 128 仅用于 --all 并没有太大价值,并且文档中也没有提及它(因此更改它不太可能会破坏任何现有脚本),让我们让两种模式在 @ 上使用代码 1 退出987654421@ 错误。


    在 Git 2.33(2021 年第三季度)之前,并行结帐代码路径未初始化用于以面向未来的方式与工作进程对话的对象 ID 字段。

    参见Matheus Tavares (matheustavares) 的commit 3d20ed2(2021 年 5 月 17 日)。
    (由 Junio C Hamano -- gitster -- 合并于 commit bb6a63a,2021 年 6 月 10 日)

    parallel-checkout:将新的 object_id 算法字段发送给工作人员

    签字人:Matheus Tavares

    存储 SHA-1 名称的 object_id 在哈希数组的末尾有一些未使用的字节。
    由于不使用这些字节,它们通常也不初始化为任何值。
    但是,在parallel_checkout.c:send_one_item() 处,缓存条目的object_id 被复制到缓冲区中,该缓冲区稍后通过管道write() 发送给结帐工作人员。
    这让 Valgrind 抱怨将未初始化的字节传递给系统调用。

    但是,由于cf09832 (hash: 添加一个算法成员到 struct object_id, 2021-04-26, Git v2.32.0-rc0 -- merge 列在batch #15) ("hash: add结构 object_id", 2021-04-26) 的算法成员,在这里使用 hashcpy() 已经不够用了,因为它不会从 object_id 复制新的算法字段。
    让我们添加并使用一个新函数,该函数满足复制所有重要object_id 数据的要求,同时仍然避免未初始化的字节,方法是在目标object_id 中填充散列数组的末尾。
    通过此更改,我们也不再需要将来自send_one_item() 的目标缓冲区初始化为零,因此让我们从xcalloc() 切换到xmalloc() 以明确这一点。

    【讨论】:

      猜你喜欢
      • 2012-05-17
      • 2011-12-28
      • 1970-01-01
      • 1970-01-01
      • 2015-07-28
      • 2018-11-12
      • 2011-03-28
      • 2012-08-06
      • 2018-10-29
      相关资源
      最近更新 更多