欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

解决 Git 拖得太慢问题的方法

最编程 2024-03-14 14:15:11
...

背景

随着开发迭代,整个代码仓库越来越大,git操作越来越慢,大大影响的整个开发节奏,想要解决这一个问题。在解决这一个问题前,需要对下面这几个问题有答案。

  • git 操作是怎么运行的?
  • git是怎么存储的?
  • 为什么随着开发迭代,会越来越慢?慢在哪里?

git是怎么运行的?

git从根本上来讲,是一个内容寻址系统。意味着Git的核心部分是一个简单的键值对数据库(key-value data store)。 你可以向 Git仓库中插入任意类型的内容,它会返回一个唯一的键,通过该键可以在任意时刻再次取回该内容。 它分为底层命令和上层命令,上层命令就是我们熟悉的git命令,底层命令是指能在各个系统执行的底层命令,通过上层命令的调用,连接到底层命令,才能够真正执行, 比如再Unix系统上,主要是通过一系列script来实现的。比如说, git add命令就对应了git-add-script git commit命令就对应了git-commit-script 这个其实只要看一下其对应的文件目录就可以了:

这些命令的初始化时机在我们执行了git init操作之后。

$ ls -F1
config
description
HEAD
hooks/
info/
objects/
refs/

description:作为gitWeb使用的,无需关心 config:git配置 info:全局排除的文件,比如写入.gitigonore里面的 hooks:放置服务端和客户端的钩子文件 //比较重要的几个 HEAD:指向被检测出来的分支 refs:目录存储数据的提交对象的指针(分支、标签和仓库) objects:存储数据所有数据内容 index:保存暂存区信息

objects

Blob对象

解决文件存储问题, 1.Blob对象: 图片、源文件、二进制大对象

find .git/objects -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

Tree对象

解决文件名保存问题,类似Unix系统的目录结构。存储指向blob对象的指针和指向tree对象的指针

git cat-file -p master^{tree}
100644 blob a906cb2a4a904a152e80877d4088654daad0c859      README
100644 blob 8f94139338f9404f26296befa88755fc2598c289      Rakefile
040000 tree 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0      lib

commit对象

一个commit的对象的生成需要有Tree对象的引用和父级的Commit对象。

$ echo 'second commit' | git commit-tree 0155eb -p fdf4fc3
cac0cab538b970a37ea1e769cbbde608743bc96d
$ echo 'third commit'  | git commit-tree 3c4e9c -p cac0cab
1a410efbd13591db07496601ebc7a059dd55cfe9

如何存储git对象呢:

  • 读取文件内容,添加一段特殊标记到头部,得到新的内容,记为 content
  • 计算这个content的SHA-1值
  • 通过 zlib 压缩内容
  • 通过SHA-1值的前两个字符作为目录,后38个字符作为文件名

所有的 Git 对象均以这种方式存储,区别仅在于类型标识——另两种对象类型的头部信息以字符串“commit”或“tree”开头,而不是“blob”。 另外,虽然数据对象的内容几乎可以是任何东西,但提交对象和树对象的内容却有各自固定的格式。

结合3种对象,我们可以知道commit引用tree对象,tree对象引用tree和blob对象,这样就能记录所有的变更。

refs

refs里面记录着git引用,这基本就是 Git分支的本质:一个指向某一系列提交之首的指针或引用。

head

当你执行 git branch 时,Git 如何知道最新提交的 SHA-1 值呢? 答案是 HEAD 文件。 head文件通常存放着一个符号引用,指向目前所在的分支,所谓符号引用,表示它是一个指向其他引用的指针。 在某些罕见的情况下,HEAD 文件可能会包含一个 git 对象的 SHA-1 值。 当你在检出一个标签、提交或远程分支,让你的仓库变成 “分离 HEAD”状态时,就会出现这种情况。

cat .git/HEAD
ref: refs/heads/master

tags

标签对象(tag object) 非常类似于一个提交对象——它包含一个标签创建者信息、一个日期、一段注释信息,以及一个指针。 主要的区别在于,标签对象通常指向一个提交对象,而不是一个树对象。 它像是一个永不移动的分支引用——永远指向同一个提交对象,只不过给这个提交对象加上一个更友好的名字罢了。

传输协议

智能传输协议: 它可以读取本地数据,理解客户端有什么和需要什么,并为它生成合适的包文件。 总共有两组进程用于传输数据,它们分别负责上传和下载数据。

上传数据

为了上传数据至远端,Git 使用 send-pack 和 receive-pack 进程。 运行在客户端上的 send-pack 进程连接到远端运行的 receive-pack 进程。 协商完传输数据,再发起请求上传。

下载数据

当你在下载数据时, fetch-pack 和 upload-pack 进程就起作用了。 客户端启动 fetch-pack 进程,连接至远端的 upload-pack 进程,以协商后续传输的数据。协商完传输数据,再发起请求下载。

git包

在不执行优化的情况下,如果我们提交了一个10M大文件,那么将会在object内部增加一个blob对象,这个对象将会是经过zlib压缩的对象。一旦我们再次对该文件修改,并且add之后,将会再一次的生成hash值不同的一个blob对象,也就是说,当前的object大小将近有20M。

git gc

1.收集所有松散对象并将它们放置到包文件中, 2.将多个包文件合并为一个大的包文件 3.移除与任何提交都不相关的陈旧对象 4.打包引用到一个单独的文件

如果你在这个时候更新引用,Git 并不会修改这个文件,而是向 refs/heads 创建一个新的文件。 为了获得指定引用的正确 SHA-1 值,Git 会首先在 refs 目录中查找指定的引用,然后再到 packed-refs 文件中查找。 所以,如果你在 refs 目录中找不到一个引用,那么它或许在 packed-refs 文件中。

Git 最初向磁盘中存储对象时所使用的格式被称为“松散(loose)”对象格式。 但是,Git 会时不时地将多个这些对象打包成一个称为“包文件(packfile)”的二进制文件,以节省空间和提高效率。 当版本库中有太多的松散对象,或者你手动执行 git gc命令,或者你向远程服务器执行推送时,Git 都会这样做。 要看到打包过程,你可以手动执行 git gc 命令让 Git 对对象进行打包 这个时候再查看 objects 目录,你会发现大部分的对象都不见了,与此同时出现了一对新文件

$ find .git/objects -type f
.git/objects/bd/9dbf5aae1a3862dd1526723246b20206e5fc37
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
.git/objects/info/packs
.git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.idx
.git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.pack

Git 打包对象时,会查找命名及大小相近的文件,并只保存文件不同版本之间的差异内容. 通常来说,最新的数据会以全量的数据保存,而老版本以差异方式保存。因为大部分情况下,我们都只会使用最新的数据。

解决

问题原因

分支数过多

在开发迭代中,我们每一个feature都可能是一个单独的分支,导致随着迭代周期,分支数越来越多。主要影响点在于:

  • 上传和下载的过程中,需要去计算分支的更新,涉及到heads目录的遍历
  • 如果有有些分支没有合入主分支,并且这个分支已经废弃了,不再维护,那么在这个分支上新增的文件,commit信息,tree信息,都会一直存储在远端仓库,导致每次推拉都要再次计算这些数据。
objects数据量变大

在多次开发迭代之后,没有进行gc操作进行优化,导致重复数据比较多,整个objects目录越来越大。计算越来越耗时。

解决流程

  1. 删除无用分支 规则:删除各个分支上,最新的commit信息更新时间大于距离现在超过60天的分支。 步骤: 1.获取出当前远端所有分支 2.遍历所有远端分支,计算当前最新commit的更新时间 3.跟当前时间间隔大雨60天的,执行打tag,并且删除操作

  2. 执行git仓库的gc操作 执行: 登录到远端git仓库,执行git gc,删除无用commit和没有引用的object

详细的脚本代码

#!/bin/bash

git fetch origin
git fetch
git fetch -p
rm -rf parseBranchdir
mkdir parseBranchdir
rm -rf deleteBranchFile
touch deleteBranchFile
rm -rf whiteListBranch
touch whiteListBranch
echo ${WHITE_LIST} > whiteListBranch
lastBranch=""
whiteList="false"

function isInWhiteList() {
  while read line
  do
    if [ "$line" = "$1" ]; then
       whiteList="true"
       echo "$1 in white list ......, return"
        return 1
    fi
  done < whiteListBranch
   whiteList="false"
}

function tryArchiveBranch {
  if [ "$1" = "$lastBranch" ]; then
       echo "same branch, just return"
       return
  fi
  lastBranch=$1
  isInWhiteList $1
  if [ "$whiteList" = "true" ]; then
      echo "in white list, just return"
      return
  fi
  if [ "${ENABLE_DELETE}" = "true" ]; then
      echo "the branch $1 should be delete...., try delete"
      git reset --hard remotes/origin/$1
      git checkout .
      git tag archive/$1
      git push --delete origin $1
      git push origin archive/$1
  else
     echo "the branch $1 should be delete...."
     echo "the branch $1 should be delete...." >> deleteBranchFile
  fi
}

function mapMonthToInt() {
    case $1 in
    "Jan")
      return 1
      ;;
    "Feb")
      return 2
      ;;
    "Mar")
      return 3
      ;;
    "Apr")
      return 4
      ;;
    "May")
      return 5
      ;;
    "Jun")
      return 6
      ;;
    "Jul")
      return 7
      ;;
    "Aug")
      return 8
      ;;
    "Sep")
      return 9
      ;;
    "Oct")
    "Nov")
    "Dec")
      return 12
      ;;
    esac
}

function calculateTime() {
#!/bin/bash

git fetch origin
git fetch
git fetch -p
rm -rf parseBranchdir
mkdir parseBranchdir
rm -rf deleteBranchFile
touch deleteBranchFile
rm -rf whiteListBranch
touch whiteListBranch
echo ${WHITE_LIST} > whiteListBranch
lastBranch=""
whiteList="false"

function isInWhiteList() {
  while read line
  do
    if [ "$line" = "$1" ]; then
       whiteList="true"
       echo "$1 in white list ......, return"
        return 1
    fi
  done < whiteListBranch
   whiteList="false"
}

function tryArchiveBranch {
  if [ "$1" = "$lastBranch" ]; then
       echo "same branch, just return"
       return
  fi
  lastBranch=$1
  isInWhiteList $1
  if [ "$whiteList" = "true" ]; then
      echo "in white list, just return"
      return
  fi
  if [ "$1" = "develop" ]; then
     echo "return for develop"
     return
  fi
  if [ "$1" = "master" ]; then
     echo "return for master"
     return
  fi
  if [ "$1" = "release_temp" ]; then
     echo "return for release_temp"
     return
  fi
  if [ "${ENABLE_DELETE}" = "true" ]; then
      echo "the branch $1 should be delete...., try delete"
      git reset --hard remotes/origin/$1
      git checkout .
      git tag archive/$1
      git push --delete origin $1
      git push origin archive/$1
  else
     echo "the branch $1 should be delete...."
     echo "the branch $1 should be delete...." >> deleteBranchFile
  fi
}

function mapMonthToInt() {
    case $1 in
    "Jan")
      return 1
      ;;
    "Feb")
      return 2
      ;;
    "Mar")
      return 3
      ;;
    "Apr")
      return 4
      ;;
    "May")
      return 5
      ;;
    "Jun")
      return 6
      ;;
    "Jul")
      return 7
      ;;
    "Aug")
      return 8
      ;;
    "Sep")
      return 9
      ;;
    "Oct")
    "Nov")
    "Dec")
      return 12
      ;;
    esac
}

function calculateTime() {
  #echo "calculateTime $1, $2"
  month=$(echo $1 |awk -F' *'  '{print $3}')
  mapMonthToInt $month
  month=$?
  day=$(echo $1 |awk -F' *'  '{print $4}')
  year=$(echo $1 |awk -F' *'  '{print $6}')
  currentTime=$(date '+%Y-%m-%d')
  currentYear=$(echo $currentTime |awk -F'-'  '{print $1}')
  currentMonth=$(echo $currentTime |awk -F'-'  '{print $2}')
  currentDay=$(echo $currentTime |awk -F'-'  '{print $3}')
  echo " current time is-> $currentYear:$currentMonth:$currentDay"
  echo "$2 merge time is ->  -> $year:$month:$day"
  mergeTimeToDays=$(((year-2016) * 365 + (${month#0} * 31) + (${day#0} - 0)))
  dividerDays=$((currentTimeToDays - mergeTimeToDays))
  echo "dividerDays is $dividerDays"
  if [[ $dividerDays -gt 60 ]]; then
      tryArchiveBranch $2
  fi
}

function parseTime() {
  #echo "parseTime $1, $2"
  dataFilter="Date:"
  result=$(echo $1 | grep "$dataFilter")
  if [ "$result" != "" ];then
         calculateTime "$1" "$2"
  fi
}

function parseBranch() {
  #echo "parse branch $1"
  currentBranch=$(echo $1 |awk -F/ '{print $3}')
  #echo "parseBranch short -> $currentBranch"
  #git log develop | grep -C 5 "$currentBranch" | grep -C 5 "Merge branch" | grep -C 5 "into develop" > parseBranchdir/parseBranch$currentMsg.txt
  #git reset --hard $1
  git log -1 $1 > parseBranchdir/parseBranch$currentMsg.txt
  while read line
  do
    parseTime "$line" "$currentBranch"
  done < parseBranchdir/parseBranch$currentMsg.txt
}

#git branch -a | grep remotes/origin/feature_ > featureBranchName.txt
git branch -a | grep remotes/origin > featureBranchName.txt