【老万】谷歌对微软:代码管理工具哪家强?
我在中关村扛活的时候还年轻,不知道世界上有代码版本管理系统(version control system, VCS)这样一种东西。同事们都直接编辑同一份源代码,一不小心就会误删或者互相覆盖,“雄关漫道真如铁,而今迈步从头越,从头越。”可以想象,我们程序员整天生活在恐惧当中。三十年前国内的软件开发水平就那样。
后来到了美国,在微软实习才发现有个好东西叫 Visual SourceSafe,它保存了代码修改的全部历史,每次提交代码的时候看得见跟上次比有什么变化,改错了可以无限 undo 回滚到以前的状态,从此再也不怕丢代码了。这样开发起来没有任何精神负担,我们昂首走进新时代。
长话短说,进入谷歌之后,亲身经历了公司代码版本管理系统的演进,回头一看,有些意思。今天就跟大家掰扯一下。
世界上的代码版本管理系统分为两大类:集中式和分布式。集中式的 VCS 里有一个中央数据库(repository)为人民服务,代码的历史都存在这个库里。开发的时候,程序员们从这个数据库导出(check out)一份代码到本地的机器上,改完再把改动导入(check in)到中央数据库。
大家可以想象,要是中央出了故障或者网络不通,每个人都不能提交代码或者查看历史版本,因为在开发者的本地是根本没有代码历史的。中央数据库非常关键,就像国际象棋里的老王。
常用的集中式 VCS 有 CVS, Subversion, 和 Perforce 等。我前面说的 Visual SourceSafe 也算集中式,曾经做为微软 Visual Studio 的一部分昙花一现,现在已经被历史淘汰,没几个人知道了,唯有白头码工闲坐说 VSS。
分布式 VCS 不一样。以 Linus 大神发明的 git 为例,分布式 VCS 系统由一个个节点组成,不存在一个绝对的中心,每个节点都保存了代码的历史信息。开发者在自己的节点完成修改后,再把改动合并到相邻节点。即便是网络断了,在本地进行各种代码版本操作也没有任何问题。
当然,实践中不是所有的节点都对等。我们还是会把某些节点指定为“官方”节点。但这只是一个共识,不是绝对的规则。任何人都可以分叉一个官方节点,另立中央。要是官方节点彻底坏了,其它节点也保留了项目的大部分历史,不会像集中式 VCS 那样老王驾崩就颗粒无收。
今天世界上绝大多数开源软件都托管在微软旗下的 github,而 github 采用的主要版本管理系统是 git,所以,以 git 为代表的分布式 CVS 越来越为程序员所熟悉。听说微软内部也越来越多地使用 git 了。
~~~~
git 是 2005 年面世的。我刚加入谷歌的时候还没 git 呢,那么谷歌用的是什么 VCS 呢?
是 Perforce(又叫 p4)。英雄所见略同,当时微软用的 Source Depot 其实就是对 Perforce 略作改进的一个内部版。从微软跳到谷歌,我只把 sd 命令换成 p4 就上岗了,因为它们除了名字不同,命令格式是一模一样的。
谷歌觉得 p4 不够好用,于是加了一层包装叫 g4,简化了一些比较繁琐的操作。
大多数公司受 VCS 和构造系统(build system)性能的制约,会把代码按项目分到几个不同的代码库(repository),分开管理。不同项目之间鸡犬之声相闻,老死不相往来,重用代码非常困难。一个真实的笑话是微软的 C++ 代码库一度有十几个不同的 string 类,比如 string, String, TString, CString 等等。
而谷歌的大部分代码都放在同一个代码库里,这在大公司里是比较少见的。这样的好处是方便代码重用,但对开发系统的可扩展性(scalability)要求高,否则代码多了撑不住。对开发者的要求也比较高,否则大家会整天我 break 你,你 break 我。总体来看,我觉得这种选择利大于弊,对谷歌来说是正确的决定。
后来谷歌果然遇到了代码库可扩展性的难题,不过他们没有知难而退,而是用一系列创新解决了问题,保持了单个代码库的架构。我喜欢早期谷歌的一点,就是工程师们坚持从第一性原则出发,有条件要上,没条件创造条件也要上。
即便是在谷歌早期,代码库的大小也是一个问题。要是把全部代码拷贝到本地,一来要很多时间,二来会占满本地硬盘,让本来就不富裕的程序员捉襟见肘。好在 perforce 允许大家指定自己对哪些文件和目录感兴趣,创建一个工作空间的时候只拷贝这些,其它跟项目无关的文件就不用下载了。
问题来了:代码重用的结果就是一个项目会依赖于其它项目,而那些项目又会依赖于更多其它项目。开发者要编译成功,必须要下载全部相关项目的源码,也就是说要找到这个依赖关系的传递闭包。
这不是件容易的事,谷歌的工程师却都没有哭泣。他们的解决方法是写了一个工具(名字记不清了,好像叫 gcheckout):你告诉它你对哪些项目感兴趣,它就会通过代码之间的依赖关系找出你需要的全部文件目录,下载这些文件,再帮你生成一个巨大无比的 Makefile。然后就可以用 make 命令构造你的项目了。
~~~~
这种方法虽然凑合能用,实在太慢了:每次代码依赖关系变化的时候,就需要重新生成 Makefile,这时很多源文件都需要重新编译。即便是用了 distcc 分布式编译,经常要等几个小时才有结果。
怎么办?谷歌设计了一个全新的构造系统来解决这个慢的问题。它叫 blaze,也就是后来开源的 bazel。
make 的重编译机制非常 naive:只要输入文件的时间戳变了,哪怕内容没变,也要重新编译。这很没必要。比如你往一个 .proto 文件加了一行注释,会导致重新生成一个 .h 文件,而这个 .h 文件跟前一个版本是一模一样的,只是时间戳不同。但 make 不知道啊,于是所有依赖于这个 .h 文件的源文件都会被重新编译一遍,编译产生的 .o 文件又会被重新链接,...... 像多米诺骨牌一样,触一发而动全身。
而 blaze 更聪明:只有输入文件的内容真正变了才会触发重新编译。在上面这个例子里,blaze 只会重新生成 .h 文件,然后发现它其实没有变,于是后面的事都省了。
跟 blaze 差不多同时推出的还有两个系统:forge 让编译工作在云端进行,只要有需求,可以让很多机器同时编译。objfs 文件系统则给编译结果提供了一层 cache:如果你需要编译一个文件,但最近有人也编译过这个文件,只要你们的文件内容和编译器参数都是一样的,blaze 就可以跳过编译,直接用上次编译的结果。
blaze, forge, objfs 三剑客齐出手,编译速度嗖地就上去了。
~~~~
用 gcheckout 管理项目依赖很不方便,只能说是权宜之计。能不能不事先下载这些源文件,编译的时候按需分配,用到哪个文件再去取哪个?
这个问题很适合用一个特殊的文件系统来解决。谷歌有一言不合就发明一个新文件系统的传统。除了前面说的 objfs,大家知道谷歌早期有一篇著名论文叫《谷歌文件系统 GFS》,解决了在 Linux 上面没有一个好的分布式文件系统的问题。GFS 让远程的文件也可以被当成本地文件一样打开处理,即便是在亚特兰大的机器也能访问到俄勒冈数据中心的文件。
谷歌的想法是建立一个虚拟的文件系统放源文件。平时在本地硬盘不放任何文件内容,这样每次 check out 代码时速度飞快,因为并没有真正去取任何文件,只是在本地做了一个标记,记住本地的源码对应于代码库的哪一个版本,“纸上得来终觉浅”。到编辑器或编译器需要读一个源文件的时候,系统再“绝知此事要躬行”,悄悄去 perforce 服务器下载这个版本的文件。用不到的文件永远也不会下载。这个虚拟文件系统就是 srcfs。
利用 srcfs,创建一个新的工作空间变得非常轻量,很好地解决了同时开发多个不相干功能的需求。比如我有 N 件事要做,要是这些事之间没有依赖,我大可以同时创建 N 个工作空间,不但可以同时编辑代码,编译的结果也不会互相干扰,切换任务快如闪电。
这个问题 git 的解决方案是在同一个工作空间内创建 N 个本地分支(branch),再用 git checkout branch-name 命令在分支间切换。但问题是一次只能激活一个分支,其它分支的文件都处于不可见状态,不是真并行,无法肩并肩同时开发。而且 git 分支只包含了源文件,不包括编译生成的文件。这导致每次切换都要重复编译很多文件,相当低效。当然,你也可以创建多个工作空间,但 git 工作空间实打实地包括了代码库的全部文件及历史,要占用相当多的存储空间,而且每创建一个都需要很长时间,并不现实。
所以,在这一点上,我觉得 srcfs 可以轻松吊打 git。我在谷歌时经常同时开几十个,甚至上百个不同的工作空间同时做很多事情。在新公司改用 git 后,效率大打折扣。这是我离开之后不习惯的地方之一。
~~~~
随着谷歌代码库的不断扩大,perforce 的局限性越来越凸显。一是系统不够稳定,二是不能支持大量的吞吐,工程师们经常在提交代码的时候遇到故障,影响了生产力。于是谷歌决定开发自己的系统替换 perforce。
为了减少学习和迁移成本,新的系统被设计成和 perforce 的模型和命令行接口完全兼容,以前的命令和流程可以继续用,不需要任何修改。在程序员看来,唯一区别就是新系统速度更快,稳定性更高,性能更强。这个新系统叫做 piper。HBO 有一部讲硅谷公司的喜剧《Silicon Valley》很受欢迎,剧中的创业公司叫 Pied Piper。不知道谷歌是不是在玩这个哏。
piper 加 srcfs 已经让代码版本管理非常流畅了,谷歌又乘胜追击,推出 CitC(Clients in the Cloud,云端客户端),让大家可以在云上创建工作空间,没有本地机器也能开发。配合在浏览器里运行的集成开发环境 cider,我在开会的时候也能写代码,爽到飞起。
~~~~
Piper 和 perforce 这样的集中式系统还有一点小小的好处:所有提交的代码改动(CL, change list)都是有全局编号的,而且这个编号是顺序增长的,看两个 CL 编号的大小就可以知道哪一个是先提交的,非常直观。这个编号数值也比较小,即便一万员工每人全年无休每天提交 10 个 CL,十年下来 CL 号也才会涨到九位数,使用起来比较方便。
相比之下,git 因为其去中心化的设计,不可能有一个中央机构来分配单调增长的 CL 编号,所以每次提交都会被分配一个巨长的 hash ID。这个 ID 不但冗长,而且没有任何直观的意义,给出两个 ID,人类是无法直接看出谁先谁后的。
这也不是什么太大的问题,但相比之下我觉得还是 CL 编号的可用性更高。而且,我们有时候还会玩一下抢 CL 靓号的游戏,比如看谁能提交第 1000000 号 CL。这样的游戏用 git 是没法玩了,因为它产生的 ID 在人类看来都是一长串随机数。
~~~~
当然,git 还是有它的优势,那就是做一连串有依赖性的改动(stacked changes)。比如要干一件大事,一步到位容易扯着,分成一、二、三… 几步比较容易。你可以做完第一步,等同事审查后提交,再做下一步,直至结束。这样做没什么问题,就是有点慢。要是你可以让 N 位同事分工,每人审查你的一个步骤,同时进行,岂不妙哉?
如果用 git,你可以先建立分支一做第一步,再以分支 i - 1 为基础建立分支 i 做第 i 步。每个分支的代码可以独立修改,改好之后把改动合并到下一个分支。这样可以比较好地支持 stacked changes。piper 遇到这种情况就不太方便。不过,以我的经验这种情况并不多见,不是太大的问题。而且,谷歌内部也在拓展 piper 的分支功能解决这个问题,相信在这一点的体验上可以追上 git。
~~~~
那么谷歌用的集中式 VCS 和微软用的分布式 VCS 到底哪种好?
我感觉 perforce/piper 概念简单,上手非常容易,掌握基本流程后日常操作不容易出错。缺点是对服务器的要求很高,像谷歌这样的大公司有专门的团队运营这个系统,确保它的高可靠性和高扩展性,并配合先进的 srcfs 文件系统,可以得到很好的用户体验,完全适合于谷歌内部开发的需要。一般的小公司,没有实力开发运维自己的集中式 VCS,不得已只能用 git。我个人还是偏爱谷歌的系统。
git 除了比较复杂,学习难度高,还有一个对大公司严重的问题:每个开发者都要在本地复制代码库中的全部文件和至少部分历史,像谷歌这样巨大的代码库是根本在本地装不下的,只能人为拆成多个代码库,增加了开发的代价。
我给谷歌云支个招:把 piper, blaze, forge, srcfs, objfs, citc 和 cider 这套开发系统集成到谷歌云的解决方案中,让谷歌云的用户也可以用谷歌内部的先进开发工具,确实能解决很多开发的痛点,成为一个卖点,顺便也降低了新员工的培训成本。说不定这样一搞,集中式 VCS 还能从 git 的包围中杀出一条路,重新流行开来。
有人说公司能够放心把自己的核心资源源代码托管到谷歌云吗?这其实不是问题,今天很多公司已经把自己的代码放在 github 的私有代码仓库了。github 是微软的。如果用户可以信任微软,他们也可以信任谷歌。
~~~~~~~~~~
猜你会喜欢:
谷歌新语言 Carbon 能干翻 C++ 吗?- 深入浅出分析 Carbon
程序员护发秘籍 - 掌握这些工作技巧,包你不脱发
程序员的核心技能 - 以脱口秀的方式讲解程序员最重要的技能
如何做出保鲜十年的软件 - 老码农冒死披露行业内幕系列
dongbei 语言满月记事 - 一种基于东北方言的娱乐式程序设计语言
我在谷歌弄啥咧之十四 - 拿奖到手软
~~~~~~~~~~
关注老万故事会公众号:
本公众号不开赞赏不放广告。如果喜欢这篇文章,欢迎点赞、在看、转发。谢谢大家🙏