【问题标题】:git push is very slow for a branchgit push 对于一个分支来说非常慢
【发布时间】:2015-03-18 09:55:03
【问题描述】:

我们有一个非常大的 git 存储库(ios 应用程序资源)。我很欣赏 git 在使用它时会很慢,但是如果我创建一个新分支并编辑几个文件(不是二进制文件)并推送,则需要很长时间。

感觉整个 repo 都被推送了。我的印象是 git 只会发送差异,这是错误的吗? (我知道 git 存储整个文件的压缩版本,我的意思是我的分支和我的分支之间的差异)。

如果我运行git diff --stat --cached origin/foo,那么我会看到一个简短的文件列表,这些文件看起来像我所期望的那样,例如34 files changed, 1117 insertions(+), 72 deletions(-)。但是当我推动它到达Writing objects: 21% (2317/10804) 并停下来,好像它正在推动所有 2.4GB 的二进制数据。

我是否遗漏了什么(我已经用谷歌搜索了它)?这是预期的行为吗?我在 OS X (Mavericks) 和 ssh (git@github.com) 上使用 git 2.2.2。

我在这里发现了一个类似的问题:Git - pushing a remote branch for a large project is really slow,但没有真正的答案。

【问题讨论】:

标签: git


【解决方案1】:

您正在使用“智能”传输(这是一件好事),因此您确实获得了增量,或者更具体地说,是“增量压缩”。但这并不是说 git 会推送差异。

push 和 fetch 在这里的工作方式相同:在智能传输上,您的 git 调用远程,并且两端进行小型对话以确定谁拥有哪些存储库对象,由 SHA-1 标识并附加到特定标签(通常是分支和标签名称,但也允许使用其他标签)。

例如,在这种情况下,您的 git 调用他们的 git 并说:“我建议您将分支 master 设置为 SHA-1 1234567...。我看到您的 master 目前是 333333... ,这就是我认为你需要从那里到达7777777...的东西。”他们应该回答“好的,我需要其中一些,但我已经有了......”。一旦您的 git 确定了需要发送的内容以及已经存在的内容,您的 git 就会构建一个“瘦包”1,其中包含所有要发送的对象。 (这是“使用多达 %d 个线程进行增量压缩”阶段。)

然后将生成的瘦包装通过智能传输发送;这是您看到“写入对象”消息的地方。 (整个瘦包必须成功发送,之后接收方再次使用git index-pack --fix-thin“增肥”并将其放入存储库。)

发送的具体数据取决于精简包中的对象。 应该只是“他们拥有的东西”和“你正在发送的东西”之间的一组提交,加上这些提交所需的任何对象(树和 blob),加上你的任何注释标签发送和他们需要的任何对象,他们还没有。

您可以通过使用git fetch 获取他们的最新信息来找到有问题的提交,然后使用git rev-list 查看您将发送给他们的提交。例如,如果您只是要在master 上推送内容:

$ git fetch origin   # assuming the remote name is origin
[wait for it to finish]
$ git rev-list origin/master..master

检查这些提交可能会显示一个非常大的二进制文件,该文件包含在其中一个中间文件中,然后在以后的提交中再次删除:

$ git log --name-status origin/master..master

如果一个提交具有A giantfile.bin,然后后续(可能在git log 输出中列出的第一个)提交具有D giantfile.bin,则您可能会挂断发送giantfile.bin 的blob。

如果是这种情况,您可以使用git rebase -i 消除添加巨型二进制文件的提交,这样git push 就不必发送该提交。

(如果您的历史是线性的——没有要推送的合并——那么您也可以,或者改为使用git format-patch 创建一系列包含补丁的电子邮件。这些适用于通过电子邮件发送给其他站点的某人——并不是说 github 上有人等着接收它们,但你可以轻松地检查补丁文件,看看它们是否很大。)


1包是“瘦”的,因为它违反了正常的包文件规则,该规则要求包本身中包含任何增量压缩“下游”对象。相反,“下游”对象可以(事实上,必须)在接收瘦包的存储库中。

【讨论】:

  • 很遗憾,format-patch总共1.7MB,这并不能解释推送慢
  • 此时要调查的是精简包中的内容,以及您的 ssh 传输是否只是卡住了(由于一些可能与 git 无关的网络问题)。像 tcpdump 这样的网络跟踪器可能对后者有帮助。
  • 是瘦包 === git format-patch origin/foo..foo ?
  • 否:主要区别在于精简包包含实际的 git 对象,而 format-patch 使差异适用于电子邮件。差异提供了足够的信息来重建 文件,但不是完整的提交图,也不是标签。或者,换句话说,format-patch 以不同的格式为您提供大部分但不是全部信息。没有直接的方法来比较大小,尽管人们可能会合理地预期存在很强的正相关性。
  • @ShashankBhatt:您可能遇到了 VonC 在他的回答中提到的错误。 fetch-and-merge 可能触发了重新打包。或者,您可能遇到过深度 1 浅层克隆的情况。我们没有足够的信息来说明您的设置。
【解决方案2】:

请注意,当您拥有超过 1023 个包时,Git 2.25 修复了包对象的极端减速问题。请参阅下面的数字。

这可能会对您的案例产生积极影响,因为您有大量的包文件。

参见Jeff King (peff)commit f66e040(2019 年 11 月 11 日)。
(由 Junio C Hamano -- gitster -- 合并于 commit 8faff38,2019 年 12 月 1 日)

pack-objects:避免毫无意义的oe_map_new_pack() 调用

签字人:Jeff King
审核人:Derrick Stolee

由于43fa44fa3b (pack-objects: move in_pack out of struct object_entry, 2018-04-14),我们使用一个复杂的系统来节省一些每个对象的内存。

每个object_entry 结构都有一个 10 位字段来存储它所在包的索引。我们使用在程序开始时初始化的 packing_data->in_pack_by_idx, 将这些索引映射到指针。
如果我们有 2^10 个或更多包,那么我们将创建一个包指针数组,每个对象一个。这是packing_data->in_pack

到目前为止一切顺利。但是还有另一个棘手的情况:如果在我们初始化in_pack_by_idx, 之后有一个新包到达,它还没有索引。我们通过调用oe_map_new_pack() 来解决这个问题,它只是即时切换到不太理想的in_pack 机制,为已经看到的对象分配数组并回填它。

但即使我们已经切换到它,这种逻辑也会起作用(无论是因为我们确实看到了一个新包,还是因为我们一开始就有太多包)。结果不会产生错误的结果,但速度很慢。会发生什么:

  • 假设您有一个包含 500k 个对象和 2000 个要重新打包的包的存储库。

  • 在查看任何对象之前,我们调用prepare_in_pack_by_idx()
    它开始为每个包分配一个索引。
    在第 1024 包中,它发现太多,所以它放弃了,将 in_pack_by_idx 留下为 NULL

  • 在实际将对象添加到装箱单时,我们调用oe_set_in_pack(),它检查包是否已经有索引。
    如果它是第一个 1023 之后的包之一,那么它没有一个,我们将调用 oe_map_new_pack()

但该功能没有任何有用的工作要做。
我们已经在使用in_pack,所以它只是无用地遍历对象的完整列表,试图回填in_pack

我们最终为近 1000 个包执行此操作(每个包都可能由多个对象触发)。每次触发时,我们可能会迭代多达 500k 个对象。所以在绝对最坏的情况下,这是对象数量的二次方。

解决方案很简单:如果我们已经转换为使用in_pack,,则无需费心检查包是否有索引,因为根据定义,我们不会使用它。所以我们可以将“包是否有有效的索引”检查推到条件的那一半,我们知道我们将要使用它。

遗憾的是,p5303 中的当前测试没有注意到这个问题,因为它的最大值为 1000 包。如果我们在 2000 包时添加一个新测试,它确实显示出改进:

Test                      HEAD^               HEAD
----------------------------------------------------------------------
5303.12: repack (2000)    26.72(39.68+0.67)   15.70(28.70+0.66) -41.2%

但是,这些多包测试用例运行起来相当昂贵,因此添加越来越大的数字并没有吸引力。相反,我们可以通过使用GIT_TEST_FULL_IN_PACK_ARRAY, 更轻松地展示它,这迫使我们进入绝对最坏的情况:没有包有索引,所以我们将毫无意义地为每个对象触发oe_map_new_pack(),使其真正成为二次方。

以下是数字(git.git),其中包含对 p5303 的更改:

Test                      HEAD^               HEAD
----------------------------------------------------------------------
5303.3: rev-list (1)      2.05(1.98+0.06)     2.06(1.99+0.06) +0.5%
5303.4: repack (1)        33.45(33.46+0.19)   2.75(2.73+0.22) -91.8%
5303.6: rev-list (50)     2.07(2.01+0.06)     2.06(2.01+0.05) -0.5%
5303.7: repack (50)       34.21(35.18+0.16)   3.49(4.50+0.12) -89.8%
5303.9: rev-list (1000)   2.87(2.78+0.08)     2.88(2.80+0.07) +0.3%
5303.10: repack (1000)    41.26(51.30+0.47)   10.75(20.75+0.44) -73.9%

同样,这些改进对于 1-pack 的情况是不现实的(因为在现实世界中,全阵列解决方案不会发挥作用),但测试更复杂的代码路径会更有用。

在我们研究这个问题的同时,我们还要调整一件事:在oe_map_new_pack() 中,我们调用REALLOC_ARRAY(pack->in_pack)。但除非我们第一次回填它,否则我们永远不会期望到达这里,在这种情况下它将是NULL
因此,为了清楚起见,让我们将其切换为 ALLOC_ARRAY(),并添加一个 BUG() 来记录期望。不幸的是,这段代码在测试套件中没有得到很好的覆盖,因为它本质上是活泼的(只有在我们重新打包的过程中,如果其他人添加了新包,它才会生效)。

【讨论】:

    猜你喜欢
    • 2018-11-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-01-27
    • 1970-01-01
    • 1970-01-01
    • 2013-08-15
    • 2019-09-30
    相关资源
    最近更新 更多