C++ 采用"separate compilation"(分离式编译)意思就是说在编译一个 foo.cpp时,唯一的对其他依赖代码的要求就只是看到它们的头文件 (header files),所以,只要每次编译时可以确保 foo.cpp和它 include的所有header files都是一致的就可以了。但是,我们目前并没有做到这一点,因为,
在以上的各种情况下,这些 header文件有可能不同或被其他人更改而无法察觉:
alicpp 意在解决这个问题,因为在 alicpp环境下编译时,所有以上文件,甚至包括编译器本身,都是 alicpp gitrepo里的文件,并且这些文件是只读的(永远不会更改内容)。
假如MyClass.cpp依赖了两个三方库 a和 b,它们又都同时依赖第三个三方库 c,此时我们必须保证 a和 b依赖的是同一个版本的 c,而不能是稍有差异的不同的 c,否则会出现难以查询的 buildproblem,编译会通过,但是生成的执行文件是有问题的。
alicpp 清楚记录每一套三方库的依赖关系,精确到版本号,以确保完全避免菱形依赖关系可能带来的隐患 。
我们有没有问过自己一个问题,那就是线上运行的时候找到的 .so是否是我们研发或测试时使用的同一个 .so文件?这些 .so包括:
对任何这些机载的 .so文件的依赖都会造成程序运行的不确定性,因为任何其他部门的人或任何人的错误操作都有可能对这些文件作变动。
前面提到的三方库菱形依赖问题在链接时依然存在,当两个三方库想要链接不同版本的 .so时,它们在执行时是冲突和危险的。
假如我们用静态链接的方式链接所有依赖的库,突然世界变的非常的美好,
我们对目标机器的依赖性因此降至最低,只要是正确的 Linux大版本,就不会出任何问题,所有其他团队可以升级更换机器上的其他软件而不受其影响。
当然,全静态链接也有一些问题,
无论如何,大家应该看到全静态的美,尽可能的用静态方式链接更多的库,alicpp帮助大家准备全静态的链接指令。
如果我们很在意线上运行时不能链接到错误的 .so,我们可以靠生成特殊的 .so文件名来解决这个问题,比如:
pangu-trunk-3412562.so
<项目>-<repo>-<revision>.so
此时的 .so就成为“只读文件”(意思是名字和内容是一一对应的,没有人可以用同一个名字定义另一个改变了内容的文件),可以确保链接的绝对正确性。
有了严谨的编译和链接守则,我们就可以把大家统一到一个编译和链接的标准和流程上来,我们也就可以统一的来解决我们的编译和链接的速度问题。
alicpp 将致力于建立一套完整和庞大的编译系统来优化我们的日常编译过程 T264,其中会用到,
alicpp 会尝试 Google的 Gold Linker,可以 5到 10倍的提高链接速度 。
可以说我所看到的我们目前的 C/C++团队和模块之间的协作关系是混乱不堪的,因为我们没有遵守应有的法则。这里详细的记录和解释了每一个步骤和理由:
IMPORTANT: 这里的守则是“充分”和“必要”的,换句话讲,没有一个是不需要的,也没有一个是没有提到的。
我们每一个团队对其他团队的依赖性都可以按照进度要求来分成三种情况,
我们对对方的进度要求不高,我们需要的功能目前已经提供了,如果将来有新版本的话,升级了当然好,但是不升级也问题不大,即使升级也是低优先级的工作。典型的例子是对大多数三方库的要求。
我们必须尽量跟上对方的进度,不然就造成软件对接的诸多问题或是线上支持的困难,但是我们又担心跟的太紧会看到对方不必要的新代码带来的 bug,此时我们要的是“尽量跟上,但并不要最新版本”。我们很多团队之间的关系就是这样的,比如 ODPS 软件依赖底层的飞天系统,但是 ODPS有自己的稳定性需求,不能对新写的飞天代码跟进太快。
我们必须和对方是相同进度,因为我们代码的依赖性太大,同时双方又在不断做调整。我们小团队之内就是这种情况。假如两三个小团队之间也相互依赖的非常紧密,也是处于一体状态中。
针对以上三种依赖性,我们就可以直接确立代码库的结构:
IMPORTANT: 明明是强耦合的情况却自己定义成弱耦合是偷懒!明明是强强耦合的情况却自己定义成强耦合是分裂主义!我们要尽可能的把依赖性朝着强的方向确立。
弱耦合的模块可以在不同的 git repo(svn库)里,比如 alicpp的三方库,甚至于一些二方库,它们可以有自己的 git repo,只要我们有办法找到他们的 include 和 lib就可以和它们对接编译和链接,我们也可以从容的针对它们的不同版本进行引进,非常长期的做版本升级工作。
强耦合的模块必须在同一个 git repo(svn库)里开发,编译速度不是我们分属不同 git repo的借口,我们正在解决这个技术问题。代码权限是人为的分属不同 git repo的障碍,我们正在解决这个行政问题。我们之所以说“必须”在同一个 git repo,是因为下面会介绍到 git/svn的命令在做代码操作时,只有在一个 git repo里才能最容易和自然的实现。
强强耦合的模块必须在同一个 git branch(svn branch)里进行,git branch或 svn branch是我们开发的最小单位,在同一个 branch里研发的人员应该坐在一起,有问题可以马上解决,只有这样才能让相互非常依赖的代码以高速前进。
上图中,"aliyun"(阿里云)是多个强耦合团队的总和项目,是一个 git repo(svn库),"pangu"(盘古)是阿里云的一个负责底层库的团队,是一个 git/svnbranch,这张图里列出了所有维护代码库需要的 git/svn命令,
IMPORTANT: 除了这些命令外,不再需要任何其他命令,更加进一步说,其他任何命令都是不允许的。
见上图,多个团队时,每个团队都在做同样的事,他们各自按照自己的进度 git merge和 git cherry-pick。注意,
那么如何保证 git merge后代码是正确稳定的呢?靠两件事,
NOTE: 这就是为什么我们要在一个 git repo里做强耦合的代码开发,因为底层的改动必须自己编译所有上层代码,并负责跑通上层的人写的保护自己的 unit tests。
假如 git merge后master/trunk变的不正确不稳定了呢?那相互伤害的团队必须同时补足各自的 unit test,因为双方都有责任:
久而久之,日积月累,我们的 unit test 就会变的无比复杂和盘根错节,变的让 bug无以遁形。
IMPORTANT: 代码的稳定性来自于天长日久积累的 unit tests,不是靠战战兢兢的研发,慢慢悠悠的发布,代码不是红酒,不是放在那里就会自己变好的,所以把发布时间拖长是不会让代码更稳定的。
一般来讲,我们可以每星期或每半个月 git merge一次,让其他所有团队看到自己的代码变化。刚刚开始 git merge时可能 break master(不是简单的 compilation failure,而是逻辑错误)很多次,那不是因为我们 git merge太频繁了,而是因为我们的 unit test太少了,要在此期间为每一次 break加 unit test,直到稳定为止。
IMPORTANT: 明明已经可以 git merge而不做是偷懒!是耽误其他所有人进度的不负责行为!
必须在所有团队告诫大家master/trunk break是不可饶恕的错误!
每个团队发布的软件都是 Master +Delta,"Delta"是指自己团队的代码改动。不可以有任何其他的组合(比如 master +pangu branch + fuxi branch),原因很简单,因为每次 git merge时每个团队已经努力确保 master是正确的,而 pangu branch + fuxibranch并不是 fuxi团队背书认可的组合。
对于 C/C++ 这门语言来说,再也没有比 unit test更能让它稳定不出错的了。unit test就像马路上的车一样,而 bug就像想要跑到路对面的小老鼠一样,我们在抱怨我们的软件 bug特别多,很简单,因为我们的马路上就没有什么车在跑,好的 C++项目 unit test繁多,小老鼠根本没有机会可以跑到路的对面而不被撞到。
一个特别错误的认识就是把发布的时间拖长,认为这样软件问题就会减少。好吧,让我们来分析一下,
并不是让我们每天都发布,适当的控制风险是必要的,但是可以认为任何超过一个月的发布都是拖沓的和缓慢的,我们控制风险靠的是测试集群的设立,尽可能模拟线上环境的测试,灰度发布,等等手段,其中没有一个是“等待”。
IMPORTANT: “等待”就是浪费生命,浪费公司财产,是 C/C++ 代码研发效率低下的表现,是不知道如何提高系统稳定性的懦弱做法。希望大家真正的提高我们的工作节奏,摒弃等待的消极做法。
IMPORTANT: “迟迟不敢跟进别的团队的代码”是我们长期没有遵守以上共建守则造成的,希望大家达到共识后,敢于 git merge,在 break时耐心增加 unit test来巩固我们的对 bug的防守线,慢慢的我们就会对 master建立足够的稳定度和信任。
alicpp 将着手于建立 continuous buildsystem(连续 build系统)和 continuous testsystem(连续测试系统),真正建立一套完善的 C/C++测试系统。
alicpp 会根据实际需要逐步补充各种线上诊断系统,比如,