惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
人人都是产品经理
人人都是产品经理
Cisco Talos Blog
Cisco Talos Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
V
V2EX
博客园 - 三生石上(FineUI控件)
Martin Fowler
Martin Fowler
WordPress大学
WordPress大学
D
Docker
S
SegmentFault 最新的问题
博客园 - 聂微东
美团技术团队
Apple Machine Learning Research
Apple Machine Learning Research
月光博客
月光博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Last Week in AI
Last Week in AI
M
MIT News - Artificial intelligence
F
Fortinet All Blogs
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
The GitHub Blog
The GitHub Blog
GbyAI
GbyAI
L
LangChain Blog
Vercel News
Vercel News
博客园 - 叶小钗
MongoDB | Blog
MongoDB | Blog
Stack Overflow Blog
Stack Overflow Blog
H
Help Net Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Cloudflare Blog
Engineering at Meta
Engineering at Meta
T
Threat Research - Cisco Blogs
T
Threatpost
Scott Helme
Scott Helme
T
Tailwind CSS Blog
Latest news
Latest news
Stack Overflow Blog
Stack Overflow Blog
Blog — PlanetScale
Blog — PlanetScale
The Register - Security
The Register - Security
罗磊的独立博客
P
Proofpoint News Feed
腾讯CDC
S
Schneier on Security
雷峰网
雷峰网
A
About on SuperTechFans
T
Tenable Blog
F
Full Disclosure
Cyberwarzone
Cyberwarzone
博客园_首页
有赞技术团队
有赞技术团队
K
Kaspersky official blog

任霏博客

我将关闭服务器:AI彻底掐死了奄奄一息的个人博客 - 博客文章 - 任霏的个人博客网站 Vibe Coding 实现本地模型 Token 自由 IntelliJ IDEA + LM Studio + LM Link + Continue 1Password涨价后,别急着退订1Password,这个操作能帮你省25% - 博客文章 - 任霏的个人博客网站 我,吃饱了撑的注册了个域名,Cloudflare账号没了,不建议将域名放在Cloudflare - 博客文章 - 任霏的个人博客网站 临时邮箱:保护隐私与免骚扰的新方式 - 博客文章 - 任霏的个人博客网站 价值4100万美元SOL被盗SwissBorg在Solana上遭遇安全事件超200万枚ETH排队退出质押 - 博客文章 - 任霏的个人博客网站 注意 Web3 钱包遭遇 NPM 超大规模供应链攻击投毒事件 - 博客文章 - 任霏的个人博客网站 我受到以太坊ERC-20假代币地址投毒攻击记录一下大家谨防上当受骗 - 博客文章 - 任霏的个人博客网站 在2025年使用显卡 NVIDIA RTX 2080 Ti 挖矿收益记录和分析还能不能挖矿 - 博客文章 - 任霏的个人博客网站 分享我是如何成功戒烟的经验(包含失败的经验) - 博客文章 - 任霏的个人博客网站 在 OpenWRT 中配置 PassWall2 插件的教程记录 - 博客文章 - 任霏的个人博客网站 Office Professional Plus 2019 VL 版下载与 KMS 激活 - 博客文章 - 任霏的个人博客网站 最近几天我的 CDN 流量受到来自电信[山东烟台]、[江苏扬州]两地家庭宽带的攻击 - 博客文章 - 任霏的个人博客网站 自建AI服务器使用PVE配置显卡直通虚拟机安装驱动、CUDA和cuDNN运行LLM大模型进行AI炼丹 - 博客文章 - 任霏的个人博客网站 各代英特尔Intel芯片组主板适配兼容的CPU和DDR内存数据统计 - 博客文章 - 任霏的个人博客网站 GitLab Global 国际站将在60天内删除中国大陆、香港、澳门地区的账号 - 博客文章 - 任霏的个人博客网站 Github Copilot Free 开放免费版所有人均可使用 OpenAI GPT-4o、Anthropic Claude 3.5 AI 代码生成服务 - 博客文章 - 任霏的个人博客网站 Cloudflare 更新了订阅协议明确禁止优选IP和搭建梯子的行为 - 博客文章 - 任霏的个人博客网站 Linux(systemd)手动离线安装二进制(binary)MairaDB数据库指定版本 - 博客文章 - 任霏的个人博客网站 流程引擎 Flowable/Activiti 无法启动报错:liquibase - Waiting for changelog lock.... - 博客文章 - 任霏的个人博客网站 Spring Boot 全局异常捕获 ControllerAdvice 无法捕获 过滤器(Filter)和拦截器(Interceptor)中的异常 - 博客文章 - 任霏的个人博客网站 Freenom 收回了全部免费域名(.tk/.cf/.gq/.ga/.ml) - 博客文章 - 任霏的个人博客网站 Alibaba Druid 数据库连接池 takeLast() AQS 死锁导致程序无响应 - 博客文章 - 任霏的个人博客网站 你的网站加入 HSTS preload 预加载列表了吗 - 博客文章 - 任霏的个人博客网站 我的博客网站接入使用 Cloudflare 的架构分享 - 博客文章 - 任霏的个人博客网站 在 Ubuntu 上的 Nginx 高并发配置实践 - 博客文章 - 任霏的个人博客网站 技术分析黑客敲诈勒索站长的新手法百度对此也无能为力 - 博客文章 - 任霏的个人博客网站 百度站长平台快速收录权限和sitemap提交权限被全部收回 - 博客文章 - 任霏的个人博客网站 极狐 GitLab 免费时代结束不升级付费账号将禁止登陆 - 博客文章 - 任霏的个人博客网站 免费.ml域名10年委托合同到期被马里共和国收回域名经营权 - 博客文章 - 任霏的个人博客网站 从极狐Gitlab看各种中间件技术选型 - 博客文章 - 任霏的个人博客网站 时隔十年首次收到 Google AdSense 的付款 - 博客文章 - 任霏的个人博客网站 ga域名被加蓬共和国从Freenom公司手中收回域名经营权 - 博客文章 - 任霏的个人博客网站 Freenom 被 Meta(Facebook) 起诉导致暂停 .tk/.ga/.ml/.cf/.gq 等新域名注册 - 博客文章 - 任霏的个人博客网站 生花妙笔信手来 – 基于 Amazon SageMaker 使用 Grounded-SAM 加速电商广告素材生成 [1] - 博客文章 - 任霏的个人博客网站 github.renfei.net 不再完整代理 Github 页面改为代理指定文件 - 博客文章 - 任霏的个人博客网站 优雅的源代码管理(三):本地优雅的使用 Git Rebase 变基 - 博客文章 - 任霏的个人博客网站 优雅的源代码管理(二):Git 的工作原理 - 博客文章 - 任霏的个人博客网站 优雅的源代码管理(一):版本控制系统 VCS(Version Control System)与软件配置管理 SCM(Software Configuration Management) - 博客文章 - 任霏的个人博客网站 ChatGPT 开发商 OpenAI 买下极品域名 AI.com - 博客文章 - 任霏的个人博客网站 火爆的 AI 人工智能 ChatGPT 国内注册教程、使用方式和收费标准 - 博客文章 - 任霏的个人博客网站 解决 SpringCloud 中 bootstrap.yml 不识别 @activatedProperties@ 参数 - 博客文章 - 任霏的个人博客网站 Cron表达式书写教程搞定Linux、Spring、Quartz的定时任务 - 博客文章 - 任霏的个人博客网站 阿里云香港可用区C发生史诗级故障 - 博客文章 - 任霏的个人博客网站 国产统信UOS服务器操作系统V20提供免费使用授权 - 博客文章 - 任霏的个人博客网站 开源站长推送工具效果评测推荐(百度/必应/谷歌) - 博客文章 - 任霏的个人博客网站 获取公网IP服务「ip.renfei.net」升级增加地理定位数据字段公示 - 博客文章 - 任霏的个人博客网站 腾讯微信成为 GitHub 秘钥扫描合作伙伴 - 博客文章 - 任霏的个人博客网站 免费设置亚马逊远程桌面 - 博客文章 - 任霏的个人博客网站 我关站了-个人备案核查要求关闭论坛系统 - 博客文章 - 任霏的个人博客网站 Linux 中 chmod 644、755、777权限的含义和使用方法 - 博客文章 - 任霏的个人博客网站 Spring Boot 3.0 发布啦但是我还是暂时放弃升级了 - 博客文章 - 任霏的个人博客网站 过时老旧电脑安装 Windows11 跳过 Win11 TPM、RAM、Secure Boot 最低系统要求限制检查 - 博客文章 - 任霏的个人博客网站 IT资讯网站 cnBeta.com 网站被关停域名已经被 clientHold - 博客文章 - 任霏的个人博客网站 昨晚接口又被日了,接口被疯狂调用的背后是人是鬼?是道德的沦丧还是人性的扭曲? Mac破解软件站MacWk下线破产了,我想分享Mac破解软件却不太敢 我和极狐GitLab的故事回顾
当你 git push 时,极狐GitLab上发生了什么? - 博客文章 - 任霏的个人博客网站
任霏 · 2022-10-21 · via 任霏博客

本文满足了我的好奇心,并且十分喜欢这类内容和文章幽默的风格,转载过来推荐大家一起阅读,原作者是极狐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

git-data-model红色部分由 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(忽略前面的协议内容)。

  1. 客户端根据返回的 ref 情况,找出那些自己有但是服务端没有的 commit,把即将变更的 ref 告知服务端:
009f0000000000000000000000000000000000000000 8fa91ae7af0341e6524d1bc2ea067c99dff65f1c refs/heads/feat/hello-world

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

  1. 客户端将相关 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
  1. 服务端解包 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-high-level-architecture开始 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-joke如今 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://gitlab.example.com/user/repo.git. 和SSH不一样,HTTP请求是无状态的,而且总是一问一答。在你执行git push时,Git客户端会按顺序和两个接口打交道:

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

  • POST https://gitlab.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://gitlab.example.com/user/repo/-/merge_requests/new?merge_request%5Bs0029\x02ource_branch%5D=feat%2Fhello-world 0000

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

  1. 把请求原样发到Rails,后者会返回鉴权结果、用户ID、仓库对应Gitaly实例信息(有点怪,对吧?Rails的info/refs和git-receive-pack接口居然是用来鉴权的,我猜这后面多少有些历史原因)
  2. Workhorse根据上一步Rails返回的信息,建立与Gitaly的连接,在客户端和Gitaly之间充当中继。

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

  1. 用户执行 git push
  2. Git客户端调用GET https://gitlab.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://gitlab.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-logoGitaly 这个名字其实是在玩梗,致敬了 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 的形式,但是 namespaceproject_name 都可以被用户修改,这带来了额外的运行开销。git push 对应 Gitaly 的 SSHReceivePack(SSH)和 PostReceivePack(HTTPS)方法,它们的底部都是 Git 的 git-receive-pack,也就是说,最核心的 refs 和 object 更新由 Git 二进制来完成。git-receive-pack 提供了钩子使得这个过程能够被 Gitaly 介入,这里面还牵扯 Rails,一个单边的请求(不含返回)流程大概像下面这样:tong-guan-bao-zangGitaly在启动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 的时候。a-picture-of-a-hook图中的这个钩子和计算机科学有着历史悠久的联系……咳咳,好吧我编不下去了,我只是担心你看到这里已经要睡着了,找张图片让你放松一下~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 全程,真是一次伟大的旅程!勇士,下图就是你的通关宝藏!
tong-guan-bao-zang