解决 Git 拖得太慢问题的方法
背景
随着开发迭代,整个代码仓库越来越大,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目录越来越大。计算越来越耗时。
解决流程
-
删除无用分支 规则:删除各个分支上,最新的commit信息更新时间大于距离现在超过60天的分支。 步骤: 1.获取出当前远端所有分支 2.遍历所有远端分支,计算当前最新commit的更新时间 3.跟当前时间间隔大雨60天的,执行打tag,并且删除操作
-
执行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
上一篇: git 拉取远程分支并与本地分支合并
推荐阅读
-
解决 iPhone 8 白色苹果界面卡死问题,让你学会 2 种还原系统的方法!
-
解决苹果 13 白色屏幕无法正常打开问题的 2 种方法!
-
有关金融和贸易的常见问题及解决方法
-
问题解决思路的贪婪算法、回溯算法和动态编程分析 - 决策方法
-
STM32 八大在线升级(IAP)超详细图解及需要注意的问题解决方法 --转载
-
Win11 分辨率太高字体很小的解决方法?Win11 分辨率太高字体很小的问题分析
-
Oracle导出导入dmp等文件类型多表数据的常用方法、遇到的常见问题及解决办法(exp无效sql????)
-
岩石谷 P4999 令人讨厌的数学作业问题解决方法
-
组合优化案例研究:解决复杂问题的方法
-
解决 "Windows 11 上 DNS 服务器无响应 "问题的 12 种方法