暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

新功能 | 部分克隆如何提升大仓库体验

云效 2021-10-08
509

什么是部分克隆?

Git是一个分布式的版本控制系统,当在默认情况(例如不带任何参数情况下使用git clone命令)下克隆仓库时,Git会自动下载仓库所有文件的所有的历史版本。这样的设计一方面的带来了分布式的代码协同能力, 但在另一方面, 随着开发者持续的向仓库中提交代码,仓库的体积会不可避免的变得越来越大, 因为远端仓库体积的迅速膨胀, 带来的是clone后本地后磁盘空间的迅速增长以及clone耗时的不断增加。

那么,有没有一种技术,可以优化和解决这些问题呢?答案是:有的。那就是Git的部分克隆(partial-clone)特性。目前,部分克隆已经在阿里云Codeup上线, 用户可以试用该功能体验新特性带来的研发效率的提升。

简而言之, 部分克隆允许您在克隆您的代码仓库时,通过添加--filter选项来配置需要过滤的对象,从而达到按需下载的目的,这种按需下载大大减少了传输的数据量和过程的耗时,同时还可以降低本地磁盘空间的占用。在后续工作中需要用到这些缺失文件的时候,Git会自动的按需下载这些文件,在不必进行任何额外的配置的情况下,用户仍然可以正常的开展工作。

部分克隆的适用场景

在许多的场景下,都可以用部分克隆来提升您的效率,我们以几个典型场景举例:

大仓库

当我们的仓库体积较大时,就可以考虑使用部分克隆来提升开发过程中的效率及体验。例如,linux的内核,目前有100万以上的提交,整个仓库包含了830万+的对象,体积约在3.3GB左右。

要全量的克隆这样一个仓库,克隆速度以2MB/s 来算,需要约26分钟的时间。在网络条件不佳的情况下,克隆可能还会耗费更久的时间。


    $ git clone --mirror git@codeup.aliyun.com:6125fa3a03f23adfbed12b8f/linux.git linux
    克隆到纯仓库 'linux'...
    remote: Enumerating objects: 8345032, done.
    remote: Counting objects: 100% (8345032/8345032), done.
    remote: Total 8345032 (delta 6933809), reused 8345032 (delta 6933809), pack-reused 0
    接收对象中: 100% (8345032/8345032), 3.26 GiB | 2.08 MiB/s, 完成.
    处理 delta 中: 100% (6933809/6933809), 完成.


    下面我们让我们开启部分克隆的blob:none选项,来看看使用部分克隆后的效果。


      $ git clone --filter=blob:none  --no-checkout git@codeup.aliyun.com:6125fa3a03f23adfbed12b8f/linux.git
      正克隆到 'linux'...
      remote: Enumerating objects: 6027574, done.
      remote: Counting objects: 100% (6027574/6027574), done.
      remote: Total 6027574 (delta 4841929), reused 6027574 (delta 4841929), pack-reused 0
      接收对象中: 100% (6027574/6027574), 1.13 GiB | 2.71 MiB/s, 完成.
      处理 delta 中: 100% (4841929/4841929), 完成.


      可以看到,使用了blob:none选项后,需要下载的对象由834万左右减少至602万左右,需要下载的数据量更是由3.26GB下降到了1.13GB,还是以2MB/s的速度来计算的话,部分克隆的时间仅需9分钟左右,与原来的全量克隆相比,时间仅为原来的三分之一左右。

      如果使用treeless模式的部分克隆,需要下载的对象、耗费的时间还将进一步减少。但是,treeless模式的克隆在开发的场景下会更加频繁的触发缺失对象的下载,我们不推荐使用。

      使用部分克隆,我们花费了更少的时间,克隆了更少的对象,带来的提升优化是显著的。


      微服务单根代码仓

      近年来,越来越多的项目选择了使用微服务的架构,将大的单体服务拆分为若干个内聚化的微型服务,每一个服务由一个微型团队进行维护,团队间的开发可以并行、互不干扰,团队间的协同复杂度大幅降低。但是,这也将带来公用代码更难重用、不同仓库之间依赖混乱、团队之间流程规范难以协同等问题。

      因此,微服务单根代码仓的模式被提出,在这种模式下,子服务使用git来进行管理,并且由一个根仓库来统一管理所有的服务,看起来可能是这样的结构:

      使用单根代码仓,公用的代码更易于共享,项目文档、流程规范可以集中于一处,也更加易于实施持续集成。

      但是,这种模式也有缺点,对于一名开发者来说,即使他只关注项目中的某一部分,他也不得不克隆整个仓库。

      部分克隆配合稀疏检出特性,可以帮助我们解决这一问题,我们可以首先启用部分克隆,并指定--no-checkout选项来指定克隆完成后不执行自动检出,避免检出时自动下载当前分支下的所有文件。之后,再通过稀疏检出功能,只按需下载并检出指定目录下的文件。

      例如,我们创建了一个项目,具有如下的结构:

        monorepo
        ├── README
        ├── backend
        │ └── command
        │ └── command.go
        ├── docs
        │ └── api_specification
        ├── frontend
        │ ├── README.md
        │ └── src
        │ └── main.js
        └── libraries
        └── common.lib


        现在,作为一名后端的开发人员,我只关心backend下的代码,并且也不想花费时间下载其他目录下的代码,那么就可以执行。

          $ git clone --filter=blob:none  --no-checkout https://codeup.aliyun.com/61234c2d1bd96aa110f27b9c/monorepo.git
          正克隆到 'monorepo'...
          remote: Enumerating objects: 24, done.
          remote: Counting objects: 100% (24/24), done.
          remote: Total 24 (delta 0), reused 0 (delta 0), pack-reused 0
          接收对象中: 100% (24/24), 2.62 KiB | 2.62 MiB/s, 完成.


          然后,我们进入该项目,开启稀疏检出,并配置为只下载backend下的文件


            $ cd monorepo
            $ git config core.sparsecheckout true
            $ echo "backend/*" > .git/info/sparse-checkout


            最后我们执行git checkout,并执行tree命令观察目录结构。可以看到,只有backend目录下的文件被下载了。


              $ tree .
              .
              └── backend
              └── command
              └── command.go


              2 directories, 1 file


              应用构建

              在构建的场景下,构建服务器首先需要从git仓库获取代码,并执行构建,最后发布应用。在构建的过程中,我们并不需要仓库中的历史代码,而是根据代码的最新版本来构建我们的应用。此时,我们可以用部分克隆的tree:0选项,最大程度的减少需要下载的对象数量。

              对于构建的场景来说,我们还可以使用git的浅克隆特性,进一步的过滤历史的commit对象。关于git的浅克隆,请参考git-cloneGit - git-clone Documentation (git-scm.com)

              部分克隆的使用及原理简介

              Git底层对象类型简介

              在使用部分克隆前,还需要了解一些Git的底层存储原理,以便更好的理解各个选项的含义,主要涉及blob对象,tree对象,以及commit对象。

              下图以git底层对象的形式,展示了一个git仓库的结构。

              其中

               •  圆形,代表了commit对象,commit对象用于存储提交信息,并指向了父commit(如果存在)以及根tree对象。通过commit对象,我们可以回溯代码的历史版本。

               • 三角形,代表一个tree对象,tree对象用于存储文件名及目录结构信息,并且指向blob对象或其他tree对象,由此组成嵌套的目录结构。

               • 方块,代表了blob对象,存储了文件的实际内容

              部分克隆的使用限制

               • 客户端限制: 本地的git版本在2.22.0或更高。

               • 服务端filter限制: 目前, Codeup支持指定两种 --filter :

                 ○ Blobless克隆: --filter=blob:none

                 ○ Treeless克隆: --filter=tree:<depth>

               • 服务端功能开启限制: 目前,Codeup部分克隆功能正在灰度测试中,如果您对部分克隆有使用的需求,请提交工单与我们联系。

              在提交了工单并为您的仓库开启了部分克隆,且本地客户端也支持的情况下,就可以使用部分克隆来提升您的研发效率了。


              如何使用部分克隆

              要使用部分克隆,有如下几种方式:

              1、使用 git clone 命令创建部分克隆


                git clone --filter=blob:none <仓库地址>


                2、使用 git fetch 命令创建部分克隆


                  git init .
                  git remote add origin <仓库地址>
                  git fetch --filter=blob:none origin
                  git switch master


                  3、使用 git config 设置项目成为部分克隆


                    git init .
                    git remote add origin <仓库地址>
                    git config remote.origin.promisor true
                    git config remote.origin.partialclonefilter blob:none
                    git fetch origin
                    git switch master


                    这三种方式达到的效果是一样的,您可以自行选择喜欢的方式。下面,我们用git clone的方式来分别介绍blobless克隆和treeless克隆的使用以及基本的原理(Codeup近期还将支持更多的filter选项, 敬请期待)


                    Blobless的克隆

                    在克隆时使用--filter=blob:none选项,即可开启blobless模式的克隆。在这种情况下,仓库中的历史commit、tree会被下载,blob则不会。让我们用一个例子来更好的说明使用此选项克隆时仓库的结构。

                    首先,我们创建一个测试仓库。

                     • 在第一个提交中,创建文件hello.txt,内容为hello world!

                     • 在第二个提交中,创建文件src/hello.go,内容为打印"hello world"

                     • 在第三个提交中,修改文件src/hello.go,修改输出内容为“hello Codeup"

                    整个仓库的结构看起来像是这样:



                    然后,我们执行以下命令,来执行一次blobless模式的部分克隆

                      git clone --filter=blob:none \
                      https://codeup.aliyun.com/61234c2d1bd96aa110f27b9c/partial-clone-tutorial.git


                      克隆完成后,我们进入此仓库,并执行git rev-list命令来检视仓库中的对象,得到的输出为:


                        $ git rev-list --missing=print --objects HEAD
                        18990720b6e55a70ba9f9877213dad948e0973a2
                        e18cc4e7890e6ec832f683c1a0f58412b4a37964
                        2f7478bda13e73e1e1eaab6fae3d0dfd35e50b32
                        e7c719df0874ebd3b2ec02666d65879e986d537d
                        a0423896973644771497bdc03eb99d5281615b51 hello.txt
                        98a390b9c8b5ba25e9444c8b5a487634795d7c72 src
                        02a9d16faa87c68bd6fc2af27cbe3714e53af272 src/hello.go
                        b7458566de2bf5e1011142ef5fe81ccaa4c9e73e
                        3f2157b609fb05814ba0a45cf40a452640e663c3 src
                        6009101760644963fee389fc730acc4c437edc8f
                        ?f2482c1f31b320e28f0dea5c4e7c8263a0df8fec


                        注意最后一行的id,第一个字符是问号,这也就意味着这个对象在本地其实是不存在的。为什么会出现这种情况呢?其实这正是部分克隆所要达到的效果。

                        下面我们再来看看f2482c1f31b320e28f0dea5c4e7c8263a0df8fec这个对象是什么,执行:


                          $ git cat-file -p f2482c1f31b320e28f0dea5c4e7c8263a0df8fec
                          remote: Enumerating objects: 1, done.
                          remote: Counting objects: 100% (1/1), done.
                          remote: Total 1 (delta 0), reused 0 (delta 0), pack-reused 0
                          接收对象中: 100% (1/1), 109 字节 | 109.00 KiB/s, 完成.
                          package main
                          import "fmt"
                          func main() {
                          fmt.Println("hello world")
                          }


                          注意到第五行,接收对象意味着这个对象实际上是刚被下载下来的,其中的内容为fmt.Println("hello world"),也就是第二个提交的版本。

                          通过以上的分析,我们可以知道,使用部分克隆的blob:none选项克隆仓库后,只有第二个提交中的hello.go文件不存在。

                          我们将图形上色,空心代表对象不存在,实心代表对象存在,那么仓库的结构可以表示成这个样子:

                          可以看到,仓库中的历史commit、tree对象都存在,历史的blob对象则不存在。让我们把这个观察推广到更复杂一些的仓库,我们就可以总结出以blobless模式克隆仓库的一般形式:

                          需要注意的是,在当前HEAD分支下,所有的tree和blob对象都存在,这是由于在克隆之后自动执行了一次检出。在此基础之上,我们可以修改,提交代码,展开工作。对于历史提交来说,commit和tree对象都存在,仅有blob对象未被下载。通过不下载这些历史blob对象,我们达到了节省克隆时间,节省磁盘占用空间的目的。

                          如果此时我们检出历史提交,那么Git客户端会自动的批量下载这些缺失的blob对象。此外,当我们需要使用到文件的内容时,就会触发blob的下载,当我们只需要文件的OID时,就不需要了。这也就意味着我们可以运行git merge-base、git log等命令,性能与完全克隆模式相同。

                          Treeless的克隆

                          在克隆时使用--filter=tree:<depth>选项,就开启了无tree的克隆,其中depth是一个数字,代表了从commit对象开始的深度。在这种模式下,只有给定深度内的tree以及blob对象会被下载。

                          回到我们的测试仓库,这次我们利用 --filter=tree:0 来启用treeless的克隆,并用rev-list来检视本地的对象


                            $ git clone --filter=tree:0 \
                            https://codeup.aliyun.com/61234c2d1bd96aa110f27b9c/partial-clone-tutorial.git
                            $ cd partial-clone-tutorial


                            $ git rev-list --missing=print --objects HEAD
                            18990720b6e55a70ba9f9877213dad948e0973a2
                            e18cc4e7890e6ec832f683c1a0f58412b4a37964
                            2f7478bda13e73e1e1eaab6fae3d0dfd35e50b32
                            e7c719df0874ebd3b2ec02666d65879e986d537d
                            a0423896973644771497bdc03eb99d5281615b51 hello.txt
                            98a390b9c8b5ba25e9444c8b5a487634795d7c72 src
                            02a9d16faa87c68bd6fc2af27cbe3714e53af272 src/hello.go
                            ?b7458566de2bf5e1011142ef5fe81ccaa4c9e73e
                            ?6009101760644963fee389fc730acc4c437edc8f


                            现在问号出现在两个对象的id前,我们来看看这些对象是什么


                              $ git cat-file -p HEAD^^
                              tree 6009101760644963fee389fc730acc4c437edc8f
                              author yunhuai.xzy <yunhuai.xzy@alibaba-inc.com> 1631697940 +0800
                              committer yunhuai.xzy <yunhuai.xzy@alibaba-inc.com> 1631697940 +0800


                              first commit


                                $ git cat-file -p HEAD^
                                tree b7458566de2bf5e1011142ef5fe81ccaa4c9e73e
                                parent 2f7478bda13e73e1e1eaab6fae3d0dfd35e50b32
                                author yunhuai.xzy <yunhuai.xzy@alibaba-inc.com> 1631698032 +0800
                                committer yunhuai.xzy <yunhuai.xzy@alibaba-inc.com> 1631698032 +0800


                                add hello.go


                                注意其中的第二行,可以发现b74585和b74585这两个对象,正好是第一第二个提交所指向的根树。画出我们的仓库结构,也就是:



                                推广到更一般的git仓库,则如下图所示:


                                可以看到,我们拥有所有的提交信息,以及在HEAD分支下的所有对象(还是由于自动的检出),但不包含任何历史提交中的tree和blob。

                                与blobless模式相比,treeless模式需要下载的对象更少了,克隆时间会更短,磁盘占用空间也会更少。但是在后续的工作中,treeless模式的克隆会更加频繁的触发数据的下载,并且代价也更为昂贵。例如,Git客户端会向服务端请求一颗树及其所有的子树,在这个过程中,客户端不会告诉服务端本地已有一些树,服务端不得不把所有的树都发送给客户端,然后客户端可以对缺失的blob对象发起批量请求。

                                为了更好的理解treeless克隆,下图是一个使用了--filter=tree:1选项的例子。可以看到,深度为1的tree对象也被下载了,更深的tree或者blob对象则没有。



                                日常的开发过程中,我们不建议使用treeless模式的克隆,treeless克隆更加适用于自动构建的场景,快速的克隆仓库,构建,然后删除。


                                部分克隆的其他选项


                                部分克隆还可以使用其他选项,完整的选项请参考https://git-scm.com/docs/git-rev-list中的--filter=<filter-spec>节,我们正在逐步支持其他的选项。如果您对其中的某些选项有需求,请与我们联系,我们将会尽快提供支持。

                                部分克隆的性能陷阱

                                部分克隆通过只下载部分数据的方式,在首次克隆时减轻了需要传输的数据量,降低了克隆需要的时间。但是,在后续的过程中如果需要使用到这些历史数据,就会触发对象的按需下载。根据执行的命令不同,性能可能好也可能坏。

                                性能好的命令包括:

                                 • git checkout

                                 • git clone

                                 • git switch

                                 • git archive

                                 • git merge

                                 • git reset

                                这些命令支持批量下载缺失的对象,因此性能很好。


                                性能不好的命令包括:

                                 • git log --stat

                                 • git cat-file

                                 • git diff

                                 • git blame

                                这些命令会逐一的下载需要的对象,性能很差。

                                另外,在执行git rev-parse --verify "<object-id>^{object}"     命令校验某个对象是否存在的时候,如果该对象在仓库中不存在,会执行 git fetch 对该对象按需获取。如果这个缺失的对象是一个提交对象,则获取过程会将该提交关联的所有历史提交、树对象等都重新下载,即使很多历史提交已经在仓库中了。这是因为部分仓库按需获取过程中执行的 git fetch 命令使用了 -c fetch.negotiationAlgorithm=noop 参数,没有在客户端和服务器之间进行提交信息的协商。

                                如何避免性能问题

                                git clone、git checkout、git switch、git archive、git merge、git reset 等命令支持对缺失对象的批量下载,因此性能很好。

                                其他不支持批量下载的命令可以在用如下方式优化:

                                 • 找到需要访问且在仓库中缺失的对象。

                                 • 启动 git fetch进程,通过标准输入传递缺失的对象列表,批量下载缺失对象。

                                那么如何查找缺失对象呢?可以使用 git rev-list命令,通过参数 --missing=print 显示缺失对象,缺失对象在打印时会以问号(?)开头。

                                如下命令显示 v1.0.0 和 v2.0.0 之间缺失的对象。


                                  git rev-list --objects --missing=print v1.0.0..v2.0.0 | grep "^?"


                                  将获取到的缺失对象列表通过管道传递给下面的git fetch进程,实现缺失对象的批量获取:


                                     git -c fetch.negotiationAlgorithm=noop \
                                    fetch origin \
                                    --no-tags \
                                    --no-write-fetch-head \
                                    --recurse-submodules=no \
                                    --filter=blob:none \
                                    --stdin


                                    提升大仓库体验的其他方案

                                    Git LFS(大文件存储)

                                    除了部分克隆,Codeup同时提供Git LFS大文件存储能力。关于Git LFS,请参考 https://help.aliyun.com/document_detail/203101.html

                                    部分克隆与Git LFS的异同

                                    部分克隆以及大文件存储,都是为了优化,解决Git 仓库体积过大带来的克隆时间增加以及占用磁盘空间过大的问题。然而,他们在侧重点上有所不同。

                                    大文件存储,适用于解决向Git仓库中提交了大量的二进制文件的问题。比较典型的有:

                                     • 图片

                                     • 视频

                                     • 音频

                                     • 美术、设计资源

                                     • 模型

                                     • 编译产物

                                     • ...

                                    与文本文件相比,二进制文件通常体积较大,因此被称作大文件存储。从本质上来讲,Git并不擅长处理在仓库中添加二进制文件,因为二进制文件难以压缩,Git又会将文件的历史版本存储在仓库中,导致仓库体积的迅速膨胀。因此,大文件存储应运而生,通过在仓库中配置对应的文件类型,Git会将这些文件转化为指针存储在仓库中,而将实际的文件存放在第三方服务器上。在检出分支时,这些指针文件才会被替换为原来的文件。从而达到大文件的历史版本以指针的形态存储,降低克隆仓库时间,减少仓库占用体积的效果。其他文件的历史版本,则不受影响,被正常的下载并存储在仓库中。

                                    部分克隆,则更加关注于由于仓库的历史较长或文件很多,导致仓库体积增大的问题。使用部分克隆,您可以通过在克隆仓库时设置过滤器选项,从而更加精准的过滤一些不想要下载的历史对象,以减少克隆时传输的流量,缩短克隆仓库的时间,减轻仓库对本地磁盘空间的占用。部分克隆功能更适用于一些具有较长历史的仓库,或者只需要代码的最新版本等场景。

                                    部分克隆与大文件存储,也不是割裂的,Codeup对这两个特性都进行了支持。您可以根据实际情况,来选择使用某一或同时使用两个特性,以达到更好的代码协同体验。

                                    参考资料


                                    复制下方链接进行查看

                                    https://about.gitlab.com/blog/2020/03/13/partial-clone-for-massive-repositories/

                                    https://github.blog/2020-12-21-get-up-to-speed-with-partial-clone-and-shallow-clone/


                                    点击文末“阅读原文立即进入Codeup产品,体验部分克隆功能



                                    云效的小伙伴们,你都掌握了哪些git魔法,欢迎互动评论,



                                    文章转载自云效,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                                    评论