Git 协议版本 2 宣布推出:Git wire protocol

本文作者是Git核心团队的Brandon Williams。

今天我们宣布推出Git协议版本2,这是Git的线路协议(wire protocol)的一次重大更新。该协议明确规定了在客户端与服务器之间如何传输克隆(clone)、拉取(fetch)和推送(push)。这个最新版摈弃了Git协议中最低效的部分之一,消除了可扩展性瓶颈,为将来线路协议方面的更多改进打通了道路。

协议版本2规范可在这里(https://www.kernel.org/pub/software/scm/git/docs/technical/protocol-v2.html)找到。主要改进包括如下:

  • 为引用(reference)提供服务器端过滤
  • 为新功能提供易于扩展的特性,比如ref-in-want以及拉取和推送symrefs
  • 简化HTTP传输的客户端处理

开发新协议的主要动机是能够为引用(分支和标记)提供服务器端过滤。在协议v2之前,服务器以初始引用通告来响应所有拉取命令,列出代码仓库中的所有引用。即使客户端只关心更新单个分支,比如`git fetch origin master`,也会发送这整个列表。针对含有数十万引用(Chromium代码仓库就有超过50万个分支和标记)的仓库,服务器可能最终发送数十MB的数据,但这些数据却被忽略。这在拉取期间通常占用大量的时间和带宽,尤其是当你远程更新只有几个提交的分支时,或者甚至当你只检查是否最新,因而导致无操作拉取时。

我们最近在谷歌推广了支持协议版本2的工作,看到在含有50万个引用的仓库上对单个分支执行无操作拉取的性能提高了3倍。协议v2还使从googlesource.com服务器发送的开销字节(non-packfile)减少了8倍。这种改进主要归功于将服务器通告的引用过滤成客户端表示感兴趣的引用。

克服障碍

这些年来,Git 项目作过多次尝试,试图限制初始引用通告,或者完全改用一种新的协议,但依然碰到了两个问题:(1)初始请求僵硬,又不包含可用于在不破坏与现有服务器兼容性的情况下请求新服务器修改响应的字段。(2)错误处理机制的定义不够完善,无法安全地使用现有服务器不了解的新协议,并快速退回到旧协议。为了改用新的协议版本,我们需要找到现有服务器会忽略,但可以用来安全地与新服务器通信的一条旁路(side channel)。

有三种主要的传输方式用来支持Git 的线路协议(git://、ssh://和https://),我们用来请求v2的这条旁路在通信时需要确保旧服务器会忽略发送的任何额外数据,且不会崩溃。http 传输方式最简单,因为我们只需在请求中加入一个额外的http报头(如“Git-Protocol: version=2”)。ssh 传输方式要难一点,因为它需要将环境变量(“GIT_PROTOCOL=version=2”)发送到远程终端上。这个更具挑战性,因为它需要服务器管理员配置 sshd,以便在服务器端接收新的环境变量。最难的传输方式是匿名Git传输(git://)。

使用匿名 Git 传输方式向服务器发出的初始请求使用了单个 packet-line 的格式,这包括请求的服务(用于拉取的git-upload-pack和用于推送的git-receive-pack)以及后面跟着一个NUL字节的仓库。后来添加了虚拟化支持,主机名参数可以由NUL字节来附加和终结:“0033git-upload-pack /project.git\0host=myserver.com\0”。理想情况下,我们可以添加一个用来请求v2的新参数,只要像添加主机名那样来添加:“003dgit-upload-pack /project.git\0host=myserver.com\0version=2\0”。

遗憾的是,由于 2006 年出现的一个 bug(https://github.com/git/git/commit/49ba83fb67d9e447b86953965ce5f949c6a93b81#diff-f1c031e638a78dd61428fbec6d53dd09R405),我们无法添加主机名以外的任何额外参数(由NUL隔开),否则解析这些参数会进入无限循环。这个 bug 在 2009 年得到了修复(https://github.com/git/git/commit/73bb33a94ec67a53e7d805b12ad9264fa25f4f8d),当时添加了一个检查机制,不允许添加额外参数,因而新客户端无法触发旧服务器上的这个 bug。

幸运的是,该检查机制不会注意到我们是否发送了隐藏在第二个 NUL 字节后面的额外的请求参数,这个问题早在2009年就指出来了。这允许像“003egit-upload-pack /project.git\0host=myserver.com\0\0version=2\0”这种结构的请求。通过将版本信息放在第二个 NUL 字节的后面,我们就可以同时绕过无限循环这个bug和明确不允许主机名之外的额外参数。只有新服务器会知道寻找隐藏在两个 NUL 字节后面的额外信息,旧服务器不会发出任何警告。

现在,无论在什么情况下,客户端都可以发起使用v2的请求,使用针对特定传输方式的旁路;v2 服务器可以使用新协议来响应,旧服务器会忽略旁路,就用引用通告来响应。

亲自试一下

若想亲自试一下协议版本2,你需要最新版本的Git(支持v2 的功能最近已合并到Git的主分支中,预计会成为Git 2.18 的一部分)以及支持v2的服务器(googlesource.com和Cloud Source Repositories上的仓库支持v2)。如果你启用了追踪机制,运行“ls-remote”命令以查询单个分支,就可以看到服务器使用协议版本2时,发送的引用数量要少得多。

# Using the original wire protocol(使用原来的线路协议)

GIT_TRACE_PACKET=1 git -c protocol.version=0 ls-remote

https://chromium.googlesource.com/chromium/src.git master

# Using protocol version 2(使用协议版本2)

GIT_TRACE_PACKET=1 git -c protocol.version=2 ls-remote

https://chromium.googlesource.com/chromium/src.git master
余下全文(1/3)
分享这篇文章:

请关注我们:

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注