IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 开发工具 -> 当你 git push 时,极狐GitLab上发生了什么? -> 正文阅读

[开发工具]当你 git push 时,极狐GitLab上发生了什么?

本文来自:

李振楠 极狐(GitLab) 研发工程师

勇士,你可曾好奇过 Git 和极狐GitLab 是如何工作的?现在,拿起你心爱的 IDE,和我们一起踏上探索之旅吧!

基础知识

在开始旅程之前,我们需要做三分钟的知识储备,计时开始!

Git仓库内幕

使用了 Git 的项目都会在其根目录有个?.git?文件夹(隐藏),它承载了 Git 保存的所有信息,下面是我们这次关注的部分:

.git
├──?HEAD?#?当前工作空间处于的分支(ref)
├──?objects?#?git对象,git根据这些对象可以重建出仓库的全部commit及当时的全部文件
│???├──?20?#?稀疏对象,基于对象hash的第一个字节按文件夹分片,避免某个目录有太多的文件
│???│???└──?7151a78fb5e2d99f1185db7ebbd7d883ebde6c
│???├──?43?#?另一组稀疏对象
│???│???└──?49b682aeaf8dc281c7a7c8d8460f443835c0c2
│???└──?pack?#?压缩过的对象
└──?refs?#?分支,文件内容是commit的hash
????├──?heads
????│???├──?feat
????│???│???└──?hello-world?#?某个feature分支
????│???└──?main?#?主分支
????├──?remotes
????│???└──?origin
????│???????└──?HEAD?#?本地记录的远端分支
????└──?tags?#?标签,文件内容是commit的hash

图:Pro Git on git-scm.com

图注:红色部分由 refs 提供,其余部分全部由 objects 提供,commit 对象(黄色)指向保存文件结构的 tree 对象(蓝色),后者再指向各个文件对象(灰色)

Git 服务端只会存储?.git?文件夹内的信息(称为?bare repository,裸仓库),git clone?是从远端拉取这些信息到本地再重建仓库位于?HEAD?的状态的操作,而?git push?是把本地的 ref 及其相关 commit 对象、tree 对象和文件对象发送到远端的操作。

Git 在通过网络传输对象时会将其压缩,压缩后的对象称为?packfile。

Git 传输协议

让我们按时间先后顺序理理?git push?时发生了什么:

1.?用户在客户端上运行?git push

2.?客户端的 Git 的?git-send-pack?服务带上仓库标识符,调用服务端的?git-receive-pack?服务

3.?服务端返回目前服务端仓库各个 ref 所处的 commit hash,每个 hash 记为 40 位 hex 编码的文本,它们长这样:

001f#?service=git-receive-pack
000000c229859bcc73cdab4db2b70ed681077a5885f80134?refs/heads/main\x00report-status?report-status-v2?delete-refs?side-band-64k?quiet?atomic?ofs-delta?push-options?object-format=sha1?agent=git/2.37.1.gl1
0000

我们可以看到,服务端的?main?分支位于?229859bcc73cdab4db2b70ed681077a5885f80134(忽略前面的协议内容)。

4.?客户端根据返回的 ref 情况,找出那些自己有但是服务端没有的 commit,把即将变更的 ref 告知服务端:

009f0000000000000000000000000000000000000000?8fa91ae7af0341e6524d1bc2ea067c99dff65f1c?refs/heads/feat/hello-world

上面这个例子中,我们正在推送一个新分支?feat/hello-world,它现在指向?8fa91ae7af0341e6524d1bc2ea067c99dff65f1c,由于它是个新分支,以前的指向记为0000000000000000000000000000000000000000。

5.?客户端将相关 commit 及其 tree 对象、文件对象打包压缩为 packfile,发送到服务端,packfile 是二进制:

report-status?side-band-64k?agent=git/2.20.10000PACK\x00\x00\x00\x02\x00\x00\x00\x03\x98\x0cx\x9c\x8d\x8bI
\xc30\x0c\x00\xef~\x85\xee\x85"[^$(\xa5_\x91m\x85\xe6\xe0\xa4\x04\xe7\xff]^\xd0\xcb0\x87\x99y\x98A\x11\xa5\xd8\xab,\xbdSA]Z\x15\xcb(\x94|4\xdf\x88\x02&\x94\xa0\xec^z\xd86!\x08'\xa9\xad\x15j]\xeb\xe7\x0c\xb5\xa0\xf5\xcc\x1eK\xd1\xc4\x9c\x16FO\xd1\xe99\x9f\xfb\x01\x9bn\xe3\x8c\x01n\xeb\xe3\xa7\xd7aw\xf09\x07\xf4\\\x88\xe1\x82\x8c\xe8\xda>\xc6:\xa7\xfd\xdb\xbb\xf3\xd5u\x1a|\xe1\xde\xac\xe29o\xa9\x04x\x9c340031Q\x08rut\xf1u\xd5\xcbMap\xf6\xdc\xd6\xb4n}\xef\xa1\xc6\xe3\xcbO\xdcp\xe3w\xb10=p\xc8\x10\xa2(%\xb1$U\xaf\xa4\xa2\x84\xa1T\xe5\x8eO\xe9\xcf\xd3\x0c\\R\x7f\xcf\xed\xdb\xb9]n\xd1\xea3\xa2\x00\xd3\x86\x1db\xbb\x02x\x9c\x01+\x00\xd4\xff2022\xe5\xb9\xb4?09\xe6\x9c\x88?01\xe6\x97\xa5?\xe6\x98\x9f\xe6\x9c\x9f\xe5\x9b\x9b?15:52:13?CST
\xa4d\x11\xa1\xe8\x86\xdeQ\x90\xb1\xe0Z\xfd\x7f\x91\x90\xc3\xd6\x17\xe8\x02&K\xd0

6.?服务端解包 packfile,更新 ref,返回处理结果:

003a\x01000eunpack?ok
0023ok?refs/heads/feat/hello-world

Git 传输协议可以由 SSH 或者 HTTP(S) 承载。

还是挺直接的,对吧?

极狐GitLab 的组成部分

极狐GitLab 是一个常用的 Git 代码托管服务,同时支持协作开发、任务跟踪、CI/CD 等功能。

极狐GitLab 的服务并不是一个单体,我们以大版本 15 为例,和 git push 有关的组件有下面这些:

  • 极狐GitLab:使用 Ruby 开发,分为两个部分,?极狐GitLab 的 Web 服务 /API 服务(下文记为 Rails)以及任务队列/背景任务(下文记为 Sidekiq)。

  • Gitaly:使用 Go 开发,极狐GitLab 的 Git 服务后端,负责 Git 仓库的存储和读写,将各种 Git 操作暴露为 GRPC 调用。早期 Rails 直接通过 Git 命令行操作 NFS 上的 Git 仓库,规模大了之后网络 IO 延迟感人,遂分解出了 Gitaly.

  • Workhorse:使用 Go 开发,作为 Rails 的前置代理,处理 Git push/pull、文件下载/上传这类“缓慢”的 HTTP 请求。早期这些请求由 Rails 处理,它们会长时间占用可观的 CPU 和内存,为了服务稳定,极狐GitLab 不得不将 git clone 的超时时间设为 1 分钟,但是这又带来了大仓库无法完整克隆的可用性问题。而 goroutine 的成本低很多,就被用来专门处理这类请求。

  • 极狐GitLab Shell:使用 Go 开发,用来响应和鉴权 Git SSH 连接,在用户 Git 客户端和 Gitaly 之间传递数据。

  • 极狐GitLab Runner:使用 Go 开发,负责 CI/CD 工作的执行。

  • 极狐GitLab 的数据存储在 Postgres 中,使用 Redis 做缓存。Rails 和 Sidekiq 直接连接数据库和缓存,其他组件经由 Rails 暴露的 API 进行数据读写。

简明的极狐GitLab 组件关系

图/极狐GitLab architecture overview on docs.gitlab.cn

开始 git push!

三分钟过得真快!现在你已经掌握了基础,让我们开始征途吧!

你喜欢SSH?

如果你的远端地址是?git@jihulab.example.com:user/repo.git?这样的,那么你在用 SSH 与 极狐GitLab 进行通讯。在你执行 git push 时,本质上,你的 Git 客户端的 upload-pack 服务在执行下列命令:

ssh?-x?git@jihulab.example.com?"git-receive-pack?'user/repo.git'"

这里面有挺多问题值得说道的:

  • 大家的用户名都叫 git,服务端怎么分清谁是谁?(安能辨我是雄雌?)

  • ssh? 我可以在服务端上运行任意命令吗?

这两个问题由极狐GitLab Shell 的 gitlab-sshd 来解决。它是个定制化的 SSH Daemon,和一般的 sshd 讲同样的 SSH 协议,客户端没法分清它们。客户端在做 SSH 握手时会提供自己的公钥,gitlab-sshd 会调用 Rails 的内部 API?GET /api/v4/internal/authorized_keys?查询公钥是否在极狐GitLab 注册过并返回对应公钥 ID(可定位到用户),同时校验 SSH 握手的签名是否由同一份公钥对应的私钥生成。

另外,gitlab-sshd 限制了客户端可以运行的命令,其实,它在使用用户运行的命令来匹配自己应该运行哪个方法,没有对应方法的命令都会被拒绝。

可惜,看来我们是没法通过 SSH 在极狐GitLab 的服务器上运行?bash?或者?rm -rf /?了。┑( ̄Д  ̄)┍

说点有趣的,早期极狐GitLab 当真使用 sshd 来响应 Git 请求。为了解决上面这两个问题,他们这么写?authorized_keys:

#?Managed?by?gitlab-rails
command="/bin/gitlab-shell?key-1",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty?ssh-
rsa?AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt1016k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7
Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=
command="/bin/gitlab-shell?key-2",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty?ssh-
rsa?AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt1026k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7
Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=

对,你没猜错,整个极狐GitLab 的用户公钥都会被放到这个文件里,它可能会上百 MB 的大小!朴实无华!

Command?参数覆盖了每次 SSH 客户端想运行的命令,让 sshd 启动 gitlab-shell,启动参数是公钥 ID. gitlab-shell 可以在由 sshd 设定的环境变量?SSH_ORIGINAL_COMMAND?获取到客户端原本想执行的命令,进而运行相关方法。

由于 sshd 在匹配?authorized_keys?时用的是线性检索,在?authorized_keys?很大时,先注册的用户(公钥在文件的前面)的匹配优先级会被后注册的用户高很多,换句话说,老用户的 SSH 鉴权要比新用户的快,而且是可察觉的快。(真·老用户福利)

?黄金老用户的特别福利——超长git push时间。图/xkcd-excuse.com

如今 gitlab-sshd 依赖的 Rails API 背后是 Postgres 索引,这个 bug(feature?)不复存在。

通过用户身份验证后,gitlab-sshd 会检查用户对目标仓库是否有写权限(POST /api/v4/internal/allowed),同时获知这个仓库在哪一个 Gitaly 实例,以及用户 ID 和仓库信息。

最后,gitlab-sshd 会调用对应的 Gitaly 实例的?SSHReceivePack?方法,在 Git 客户端(SSH)与 Gitaly(GRPC)之间作为中继和翻译。

最后两步 gitlab-shell 的行为和 gitlab-sshd 是一样的。

从宏观视角看,经由 SSH 的 git push 是这样的:

1.?用户执行?git push;

2.?Git 客户端通过 SSH 链接到 gitlab-shell;

3.?gitlab-shell 使用客户端公钥调用?GET /api/v4/internal/authorized_keys?获得公钥 ID,进行 SSH 握手;

4.?gitlab-shell 使用公钥 ID 和仓库地址调用?POST /api/v4/internal/allowed,确认用户有到仓库的写权限;

5.?API 返回:Gitaly 地址和鉴权 token、repo 对象、钩子回调信息(逻辑用户名?GL_ID、逻辑项目名?GL_REPOSITORY);

6.?gitlab-shell 用上列信息调用 Gitaly 的?SSHReceivePack?方法,成为客户端和 Gitaly 的中继;

7.?Gitaly 在适当的工作目录运行?git-receive-pack,并且预先设定好环境变量?GITALY_HOOKS_PAYLOAD,其中包含?GL_ID,?GL_REPOSITORY?等;

8.?服务端 Git 尝试更新 refs,运行 Git hooks;

9.?完成。

Gitaly 和 refs 更新我们稍后会聊到。

你更喜欢HTTP(S)?

HTTP(S)的远端地址形如?https://jihulab.example.com/user/repo.git.?和 SSH 不一样,HTTP 请求是无状态的,而且总是一问一答。在你执行 git push 时,Git 客户端会按顺序和两个接口打交道:

  • GET https://jihulab.example.com/user/repo.git/info/refs?service=git-receive-pack:服务端会在body中返回目前服务端仓库各个分支所处的commit的hash。

  • POST https://jihulab.example.com/user/repo.git/git-receive-pack:客户端会在body中提交要更新的分支及其旧commit hash和新commit hash,同时附上所需的packfile. 服务端会在body中返回处理结果,以及我们老熟人"to create a merge request"提示:

003a\x01000eunpack?ok
0023ok?refs/heads/feat/hello-world
00000085\x02
To?create?a?merge?request?for?feat/hello-world,?visit:
??https://jihulab.example.com/user/repo/-/merge_requests/new?merge_request%5Bs0029\x02ource_branch%5D=feat%2Fhello-world
0000

上述两个请求会被 Workhorse 截获,每次它都做这两件事:

  • 把请求原样发到 Rails,后者会返回鉴权结果、用户 ID、 仓库对应 Gitaly 实例信息(有点怪,对吧?Rails 的?info/refs?和?git-receive-pack?接口居然是用来鉴权的,我猜这后面多少有些历史原因);

  • Workhorse 根据上一步 Rails 返回的信息,建立与 Gitaly 的连接,在客户端和 Gitaly 之间充当中继。

总结一下,经由 HTTP(S) 的 git push 是这样的:

1.?用户执行 git push;

2.?Git客户端调用GET https://jihulab.example.com/user/repo.git/info/refs?service=git-receive-pack,带上对应的authorization header;

3.?Workhorse 截获请求,原样发送请求到 Rails,获得鉴权结果、用户 ID、仓库对应 Gitaly 实例信息;

4.?Workhorse 根据上一步 Rails 的返回信息,调用 Gitaly 的 GRPC 服务?InfoRefsReceivePack,在客户端和 Gitaly 之间充当中继;

5.?Gitaly 在适当的工作目录运行?git-receive-pack,返回 refs 信息;

6.?Git客户端调用POST https://jihulab.example.com/user/repo.git/git-receive-pack;

7.?Workhorse ?截获请求,原样发送请求到 Rails,获得鉴权结果、用户 ID、仓库对应 Gitaly 实例信息;

8.?Workhorse 根据上一步 Rails 的返回信息,调用 Gitaly 的 GRPC 服务?PostReceivePack,在客户端和 Gitaly 之间充当中继;

9.?Gitaly 在适当的工作目录运行?git-receive-pack,并且预先设定好环境变量?GITALY_HOOKS_PAYLOAD,其中包含?GL_ID,?GL_REPOSITORY?等;

10.?服务端 Git 尝试更新 refs,运行 Git hooks;

11.?完成。

Gitaly 和 Git Hooks

呼…说完了前面的连接层和权限控制,我们终于得以接近极狐GitLab 的 Git 核心,Gitaly。

Gitaly Logo 图/Gitaly

Gitaly 这个名字其实是在玩梗,致敬了 Git 和俄罗斯小镇 Aly,后者在 2010 年俄罗斯人口普查中得出的常住人口是 0,Gitaly 的工程师希望 Gitaly 的大部分操作的磁盘 IO 也是 0。

软件工程师的梗实在是太生硬了,一般人恐怕吃不下……

Gitaly 负责极狐GitLab 仓库的存储和操作,它通过 fork/exec 运行本地的 Git 二进制程序,采用 cgroups 防止单个 Git 吃掉太多 CPU 和内存。仓库存储在本地,路径形如/var/opt/gitlab/git-data/repositories/@hashed/b1/7e/b17ef6d19c7a5b1ee83b907c595526dcb1eb06db8227d650d5dda0a9f4ce8cd9.git,早期极狐GitLab/Gitaly 也使用?#{namespace}/#{project_name}.git?的形式,但是?namespace?和?project_name?都可以被用户修改,这带来了额外的运行开销。

git push 对应 Gitaly 的?SSHReceivePack(SSH)和?PostReceivePack(HTTPS)方法,它们的底部都是 Git 的 git-receive-pack,也就是说,最核心的 refs 和 object 更新由 Git 二进制来完成。git-receive-pack 提供了钩子使得这个过程能够被 Gitaly 介入,这里面还牵扯 Rails,一个单边的请求(不含返回)流程大概像下面这样:

Gitaly 在启动 git-receive-pack 时会通过环境变量?GITALY_HOOKS_PAYLOAD?传入一个 Base64 编码的 JSON,其中有仓库信息、Gitaly Unix Socket 地址和链接 token、用户信息、要执行的哪些 Hook(对于 git push,总是下面这几个),并且设定 Git 的?core.hooksPath?参数到 Gitaly 自己在程序启动时准备好的一个临时文件夹,那里的所有 Hook 文件都符号链接到了 gitaly-hooks上。

gitaly-hooks 在被 git-receive-pack 启动后从环境变量读取?GITALY_HOOKS_PAYLOAD,通过 Unix Socket 和 GRPC 连接回 Gitaly,告知 Gitaly 目前执行的 Hook,以及 Git 提供给 Hook 的参数。

pre-receive hook

这个钩子会在 Git 收到 git push 时触发一次,在调用 gitlab-hooks 时,Git 会向其标准输入中写入变更信息,即“某个 ref 想从 commit hash A 更新到 commit hash B”,一行一个:

<旧commit?ref?hash>?SP?<新commit?ref?hash>?SP?<ref名字>?LF

其中?SP?是空格,LF?是换行符。

上述信息回到 Gitaly 之后,Gitaly 会依次调用 Rails 的两个接口:

  • POST /api/v4/internal/allowed:这个接口之前在连接层鉴权时就调过,这次额外附上变更信息,Rails 可以依据其进行更细粒度的判断,例如禁用 force push,以及判断分支是否受保护等。

  • POST /api/v4/internal/pre_receive:通知 Rails 当前仓库即将有写更新,Rails 对这个仓库的引用计数 +1,这可以避免仓库的 Git 写操作被其他地方的重大变更打断。

如果?POST /api/v4/internal/allowed?返回错误,Gitaly 会将错误返回给 gitaly-hooks,gitaly-hooks 会在标准错误中写入错误信息并且退出,退出码非 0. 错误信息会被 git-receive-pack 收集后再写入到标准错误,gitaly-hooks 非 0 的退出码会使得 git-receive-pack 停止处理当前的 git push 而退出,退出码同样非 0,控制权回到 Gitaly,后者收集 git-receive-pack 的标准错误输出,回复 GRPC 响应到 Workhorse/Gitlab-Shell.

细心的同学可能会问,Hooks 在运行的时候,相关的 object 肯定已经上传到服务端了,这时停下来这部分悬空的 object 如何处理呢?

其实没有处理完的 git push 对应的 object 会被先写入到隔离环境中,它们独立存储在?objects?下的一个子文件夹,形如?incoming-8G4u9v,这样如果 Hooks 认为这个 push 有问题,相关的资源就能容易地得到清理了。

update hook

这个钩子会在 Git 实际更新 ref 的前一刻触发,每个 ref 触发一次,入参从命令行参数传入:要更新的 ref、旧 commit hash、新 commit hash。目前这个钩子不会与 Rails 互动。

极狐GitLab 同时支持自定义 Git Hooks,pre-receive hook, update hook 和 post-receive hook 都支持,这个操作在 gitlab-hooks 通知 Gitaly 钩子运行时在 Gitaly 中完成。此刻就是触发自定义 update hook 的时候。

图/Vishal Jadhav on Unsplash

图中的这个钩子和计算机科学有着历史悠久的联系……咳咳,好吧我编不下去了,我只是担心你看到这里已经要睡着了,找张图片让你放松一下~

post-receive hook

在所有 refs 都得到更新后,Git 会执行一次 post-receive 钩子,它获得的参数与pre-receive钩子相同。

Gitaly 收到 gitaly-hooks 的提醒后,会调用 Rails 的?POST /api/v4/internal/post_receive,Rails 会在这时干很多事:

  • 返回提醒用户创建 Merge Request 的信息;

  • 将 pre-receive 中提到的仓库引用计数 -1;

  • 刷新仓库缓存;

  • 触发 CI;

  • 如果适用,发 Email。

其中有的操作是异步的,被交给 SideKiq 调度。

结语

现在,你已经从客户端到服务端走完了 git push 全程,真是一次伟大的旅程!

勇士,下图就是你的通关宝藏!

?

参考资料

如果你想继续深入了解相关内容,下面的资料会是不错的起点:

  开发工具 最新文章
Postman接口测试之Mock快速入门
ASCII码空格替换查表_最全ASCII码对照表0-2
如何使用 ssh 建立 socks 代理
Typora配合PicGo阿里云图床配置
SoapUI、Jmeter、Postman三种接口测试工具的
github用相对路径显示图片_GitHub 中 readm
Windows编译g2o及其g2o viewer
解决jupyter notebook无法连接/ jupyter连接
Git恢复到之前版本
VScode常用快捷键
上一篇文章      下一篇文章      查看所有文章
加:2022-09-30 01:11:02  更:2022-09-30 01:12:39 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 -2024/12/28 2:53:40-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码
数据统计