黑白色的华为(7)- 敌人是规模

大规模软件开发思考

通常一个大型的,具有生态系统的软件平台都是非常复杂的,功能繁多的。即使公司对平台软件有了清晰明确的定位,也按照软件规律来进行开发。如何开发一个大型的软件系统本身也是一个难题。这个难题在华为又呈现出如何的一番景象呢?让我们做一次探索。

 

这个章节将是所有章节中唯一具有“技术含量”的章节,霍金说过,书里每多一个公式,读者减少一半。同样,这篇每多一个命令,读者减少一半,每多一个黑底白字屏幕输出的解读,读者减少一半。不过,华为一直宣称,我们一直在转型,我们的软件工程师和硬件工程师已经是9:1的比例了,我们在转型软件,我们要做出世界顶尖的软件产品来。这倒是一个好机会来测试一下华为现在到底有多“软”。如果很多人都看不懂我在讲什么,可能我们在“变软”的道路上任重而道远。

 

在IT的世界里,规模是一切复杂度的根源,微观如处理器的设计,如果只是单core,事情会很简单,但是演进到多core,甚至是众core,即使i++这样的简单计数运算在多core上都变得非常不一样(参见)。同样在宏观的云计算中,1000台机器组织成的集群和100万台服务器组成的集群的差别天上地下了。

 

因此,如何组织,实施,开发一个大型的软件系统就变得了一门学问,通常叫做软件工程。这门学问很多时候演变成了一种“玄学”,研究的文章汗牛充栋,书也越写越厚,方法越来越多,码农们越来越不知道怎么办。甚至,我印象中在公司内还看到过探讨佛学和软件工程关系的文章。这种独具中国特色的“学术探索”在中华大地倒也屡见不鲜。

 

黑白色的华为(7)- 敌人是规模

 

放开学术研讨不论,在一个大规模软件的开发过程中,如果没有明确的方法,公司的各级软件会面临下面的情况:

 

l  做不大:系统规模膨胀到一定程度,很容易“失控”。

l  做不快:系统演进慢,而且越来越慢,消耗的人力越来越多,沟通成本非常高。

l  易出错:很多软件系统,即使规模不太大的时候,就陷入了漏洞百出,总出问题的境地。

 

其中,做不大,做不快主要是软件设计和实现方面的问题,而易出错则是偏重于开发流程管理方面的问题。我们先从设计和实现层面来分析一下做不快和做不大的问题。后面再讲一下如何避免易出错的流程问题。

 

我并没有系统的学习,梳理过软件工程学在这个方面的论述和成果,不过幸运的是:

 

l  我所从事的OS行业本身就是一个非常庞大的软件系统,而且在OS之上构建的软件大多数都是复杂的软件系统,比如容器全栈,比如大数据系统,比如k8s。。。等等。这些大规模软件系统本身就非常复杂。我们从这些系统上可以来汲取一些营养。

l  我一直工作在一线,也还始终保持一定的代码能力,时不时还帮团队写点小东西。所以还有一些直观的切身体验。

 

我就结合上面的两点幸运来解析一下大规模平台软件的组织模式吧。

 

对于一个规模性,复杂性的问题,解决的办法也无外乎化繁为简,将一个复杂系统分解为容易控制的小单元,小模块,按照层次关系来进行软件开发。这在华为有一个耳熟能详的词语:“解耦”,公司的各种软件,硬件,系统以极其精巧的方式耦合在一起,成为一个性价比极好的脆弱系统,这个在之前已经有过描述。大家似乎都意识到了系统大了以后需要解耦,可解耦喊了这么多年,依然在喊,看起来还是除了一些问题,问题在哪里,我们不妨做一个探讨。

 

现实中华为多数软件系统的模式如下:

1.       设计好软件整体架构。

2.       依照架构将软件系统按照特性进行划分。

3.       特性和特性之间协商好接口API

4.       按照特性来对应开发组。

5.       开发组开发出来特性代码。放入CI

6.       CI将特性代码进行编译,做成部件。

7.       测试系统,测试团队进行分部件测试。

8.       将不同的特性集成为一个系统,通常是tar包。

9.       集成测试,测试团队进行整体测试。

10.   交付用户

11.   用户使用的时候解压tar包,然后使用。

12.   在使用过程中提出新的需求,解决新的bug,发现新的安全漏洞,做出新的feature。然后执行下列程序:

If __builtin_expect(need_refine_arch, fase) {

       Goto 5

} else {

       Goto 1

}

 

看起来上面的流程完美无缺,似乎所有的软件开发也无外乎这样做了。不过这里面有隐藏的问题:

l  步骤3中的接口API能否做到一次性定义清楚?所有人都希望一次性把模块分好,API定义好以后不再改变,这在工程实践上是做不到的,无论找多么有经验的架构师也做不到这种级别的预见性,接口API的变化是常态,因此需要经常goto 1,系统简单的时候还OK,但是当系统复杂的时候,只能强行goto 4来打补丁解决了。

l  步骤9中如果发现了bug,则需要返回到步骤5中,从5到10再重新走一遍。

                         i.              当系统较小的时候,从5到10的代价还比较低,但是如果系统复杂以后,从5走到10就会代价非常高昂,甚至变得不可能了。

                       ii.              同时,当系统规模大了以后,客户规模上去以后,需要更新的概率就大大提高,更新变成了一种常态。那么可能每天都面临要执行那段程序的问题。除非从5-10的步骤的时间能压缩到小于发现的问题时间。而这,几乎是不可能的了。

l  大多数情况下,以一个大tar包的形式交付一个完整的系统,意味着,任何的一个变动都会生成一个新的tar包,想象一下,为了两行安全补丁,重新制作出一个2G的tar系统的难度。整体tar包模式使得业务末端的一个微小变动会产生巨大的蝴蝶效应。

 

所以,当API的变动不可避免,当软件更新是一种常态的时候,如何合理的将系统进行拆分,拆分成什么样子就变得非常重要了。

 

Linux—复杂的简单系统

 

几乎所有的大规模软件,特别是平台软件都存在这样的问题。这个问题没有100%完美的解决方案,不过我们可以参考一个标准的操作系统来看看Linux系统是如何处理这样的问题的。

 

宏观上,大家直观看到的OS是一张光盘,但是微观上,如果你打开光盘仔细看,在数据目录里存放了几千个软件安装包。所谓OS的安装,是按照一定顺序,一个个的把这些离散的软件包安装到磁盘上。注意这里,OS的安装不是安装一个系统,而是一个个的安装各个部件。这是一个非常重要的差异点,我后续会来解读这个差异点为什么这么重要。

 

我们再进入的微观一点,看看一个软件包长什么样子,我们以一个很简单的软件包来看起。Readline软件,这个软件是干啥的呢?如果熟悉Linux的兄弟应该都用过tab键补齐的功能,还有bash的history功能,这些功能就是这个软件提供的。对OS来说,这几乎是一个小的不能再小的功能了。我们看一下最新的CentOS 7.6中的readline软件包吧。

黑白色的华为(7)- 敌人是规模

但是这个功能对于OS来说有四个软件包对应,前两个是readline的基本软件包,分为i686架构和x86_64架构,也就是一个是32位的,一个是64位的(因为历史原因,x86架构下32/64是兼容的,不过一般都只是用64位的软件包)。

 

对于这个小的不能再小的OS原子功能来说,仔细分析一下其实是非常有趣的。我们就先从命名来说吧。

 

见微知著—小软件,大学问

对于这么一个软件包,readline-6.2-10.el7.i686.rpm,命名分成了几个部分,readline是软件的名字,6.2是他的版本号,10是它的release号,el7表示是属于enterprise linux 7系统,i686表明是32位的软件系统,rpm表明是rpm格式的安装包。井然有序,其中el7, i686,rpm这些后缀的意义是显而易见的。我们就重点讲讲前面的三项,软件名称,版本号,release 号。

 

软件名称就不用说了,这是代表这个软件的唯一标示,在Linux的世界里,当说到tab补齐,history功能,没人会想到第二个软件,只有readline这个软件包。如前面所述,名称是一个软件成为一个软件的先决条件。否则,软件包如何命名呢?

 

版本号,一般的软件都遵循major.minor这种版本命名规范,相当于大版本号和小版本号,和华为大多数人的认知不同的是,一般性软件的大版本和小版本没有那么严格的界定,没人能说清楚为什么是6.8以后升级成为7.0,而不是6.9。而且有很多软件,minor版本的变动也会影响API。我印象中python的2.6版本和2.7版本之间就经历了一次剧烈的跳变。

 

如果版本号这么不靠谱,那么如何保证一个OS系统的稳定性呢?当一个软件出现了bug,或者出现了CVE漏洞需要修复的时候怎么办呢?这个时候release号就粉墨登场了。我们来看一下Centos 7.3版本,这个距离7.6版本早了差不多两年的版本的readline是什么样子的?

黑白色的华为(7)- 敌人是规模

 

很明显,除了release号从9变成了10以外,其它的东西都没有任何的变化。如果再进一步看看release 9和release 10之间的差别,基本上就是一些小的修正。

 

那么再查看一下CentOS 6 和 CentOS 5,如下图所示,从el6和el5的后缀很容易哪个是CentOS6,哪个是CentOS5。

黑白色的华为(7)- 敌人是规模

CentOS 6

 

黑白色的华为(7)- 敌人是规模

CentOS 5

 

但是如果你再仔细比较6.0, 6.1, 6.2, 6.x,你又会发现对于CentOS 6这个体系来说,不同版本readline的release号会有不同,但是readline的版本号却保持恒定。CentOS 5也是这样。

 

所以总结一下:

黑白色的华为(7)- 敌人是规模

在一个大的架构版本中,readline的版本号是不变的,变化的是release号。不同的架构版本中,选择的readline的版本号是不一样的,但是一个架构版本一旦选定了readline的版本号,那么一般来说,这个readline的版本号就不会再进行更改了。

 

软件世界中最重要的一根线

以上我们分析了OS中的一个非常简单的软件包,事实上,组成一个Linux系统的所有的软件包基本都遵循这个规则,所不同的是,readline这个包很简单,很少出问题,所以release号的变动比较小,但是诸如内核kernel这样的复杂软件包,其release变动的频繁性会让你怀疑Linux是不是一个值得托付身家性能的系统。但是无论release变动,各个软件包的版本是不会变化的,比如CentOS 7的内核kernel版本是3.10,这个这么多年都不曾变过。

 

所以,当我们把视角放得更为宏观一些,把readline这个软件的规则放在一个OS的层面来看的话,实际上,大家看到的应该是下面的一幅图景,一幅波浪起伏的版本线。

 

黑白色的华为(7)- 敌人是规模

 

微观上,每一个软件包都有自己独立的版本和roadmap,他们在不停的开发,不停的演进,各自拥有自己的开发计划,有自己的社区,自己的开发团队。

 

宏观上,所谓的一个OS的版本,实际上是在这些独立的软件上画一条线,分别摘取各个软件的某个特定版本,将这些版本连接起来就形成了一条版本线,这条线就是这个OS版本的基准。

 

然后OS厂商将这些软件版本做成pacakge安装包,组成了一个OS的发布版本。所有的操作系统都是这样组合起来的,这些软件的具体表现形式就是一个个软件包package,如果在windows下,这些软件体现为一个个的安装程序。在Linux下,如果以CentOS为例,则是一个个的rpm包,如果是Ubuntu,则是一个个的deb包,android也是一个个deb包。

 

这根线是随便画的么?当然不是。软件和软件之间有这复杂的依赖关系,比如某个软件只能依赖python2.7的版本,高版本或者低版本都会有问题。画这条线是一门学问。在OS行业,这根线本身就代表了事实上的工业标准。以Redhat为例,一旦它确定了7.0的版本基线,也就是图上的线,那么相关的硬件厂商,软件厂商就会依照这个稳定的基线进行驱动开发,软件适配,接口对接等等工作,最后,在这个线上聚集了大量的Redhat的生态伙伴。其它公司可以很容易再画一条线,但是却无法说服其它公司将海量的软硬件系统搬迁到自己的线上。

版本基线就是工业标准,基线永远不是一个人在战斗,软件的基线,也就是我们在各个独立的软件上画的这条线实质上就是生态。生态在技术上并非是一个虚无缥缈,看不见摸不着的东西。它实实在在的体现在这条版本线上。所以讲这条线是软件系统中最为重要的一条线毫不为过。

 

我为什么花了这么大的篇幅讲软件版本的问题,甚至是把一个readline的小软件解析的这么细致。因为一个明确的软件名称加上清晰的版本序列是构成大规模软件有序开发的基础。但是在这一点上,华为恰恰是混乱不堪的。

 

品牌与版本—华为独特的存在

一个软件产品,top 2的两个要素是品牌和版本,任何一个软件产品,被人熟知的也只有这两个要素。

 

当一个软件逐步摆脱零件状态,独立演进以后,这两个要素会越来越重要。而这两个软件产品的基本要素在华为的状态却和外部公司的状态截然相反。

l  外部:对品牌要像眼睛一样呵护,版本的演进策略比较宽松。

l  华为:软件本身的没有品牌意识,品牌很随意。但是软件的版本控制却异常严格,但是版本的编码却自成体系。

 

对这两个要素,内外的做法是完全相反的。

 

对于软件的品牌问题,我前面已经讲了非常多了。整体上公司的软件没有品牌的概念。一个软件的品牌(或者叫做名称)的处置非常的随意。存在两个方面的问题:

l  品牌/名称的含义任意变化,一个名称下会包含很多很多的东西,使得具体的产品没有办法和名称,品牌对应起来。

l  以项目名称代表产品名称,因为整体上华为软件的定位是硬件附属,所以大多数的软件没有自己的名称,都是依附于一个项目,软件的名称是项目名称,项目结束,软件的代号就结束了,没有办法进行持续的积累。

 

再说说版本问题。华为的软件的版本也是依照项目来的。

 

l  VxxRxxCxx的版本定义适应项目,不适应产品,更不适应品牌。对于需要对外发布的产品会产生很麻烦的问题。一个是外部软件都是以数字分隔来表示软件版本,因此需要有内外的称呼。更麻烦的是,由于我司对4的忌讳,3-5之间有空档期,但是对于外发软件这样,就会导致很多困惑。甚至导致外发的软件版本和内部需要错位对应。随着越来越多的软件需要对外发布,甚至获取安全等认证,这种命名方法会带来很多的麻烦。

l  第一个问题倒还不是大问题。但是另外一个问题就显得更为tricky了。软件大版本号的升级必须意味着技术断代。我不知道是什么原因导致公司把版本和技术断代联系起来了。

 

一种可能的解释是:大版本的升级意味着接口的变化,会导致下游产品的重大影响。因此要慎重。任何一个大版本的立项在华为都是一件极其隆重的事情,换句话说,难度极大的事情。Sounds reasonable。

 

但是这里有一个隐藏的矛盾点。架构变化未必导致接口的变化,接口的变化也不一定意味着架构有了翻天覆地的变化。那么如果架构没变,接口需要变化,版本应该怎么办?由于立项的难度非常大,最终变成了其实软件从实现层面已经有了非常大的变化,但是版本上只能体现小版本。所以,在华为,V1R2C00和V1R3C00实际上是两个可能差别很大的东西。V1R3C00和V2R1C00倒是有可能其实没有什么大差别,用起来也没有什么特殊的感受(这点听起来很难理解却又经常发生,这是因为在华为,架构变化更多的是体现在PPT上,而不是工程实践上。)

 

V1R3C00到V2R1C00没变化没啥影响,但是V1R2C00到V1R3C00变化很大却会导致很多的confuse,这也是为什么我们的所谓平台不是真的平台原因之一,宏观上看是一个平台,微观上看是一个个平台,宏观上看是一个大版本,微观上看是一个个大版本。

 

管理层的初衷可能是好的,希望架构稳定,接口不变。但不幸的是,软件自有软件的特质,这个特质还是我前面讲的:“快”。在这种快速迭代的时代,无论架构师的能力多强,也很难预测未来的变化,导致实际的设计中,变化是经常发生,如影随形的。

 

实际上软件的变化,特别是大规模软件的变化,都不是一夜之间变化的。都是集小胜为大胜的快速连续迭代过程。以非常复杂的Linux内核kernel系统来说,从十几年前的2.x到现在的5.x,变化可以说是沧海桑田,但你如果只摘取其中一段时间,比如半年,一年来看,你又看不到有所谓的“架构变化”。更重要的是,在2/3/4/5这些大版本的跳变时间点上看,你也看不出这个时间点和当时的变化有任何的联系。全看Linus本人心情。我模糊记得有一种说法,为什么从2.x到3.0的原因是:“Linus同学有一天早上醒来觉得2.x变成3.0是一件很sexy的事情”。

 

综上所述:

l  每一个独立的软件需要有一个属于自己的unique的名字,便于持续的演进。

l  版本号不应是界定软件架构,接口变化的特异性指标,版本号更类似一个人的年纪标记。三十而立和四十不惑,到底是架构发生了变化,还是年龄的自然增长呢?三十一定而立,四十一定不惑么?

 

由于华为独特的软件的定义方法,使得当我们开始开发一个大的软件系统的时候,你很难将一堆堆的软件堆叠在一起,然后画一条线。通常你看到的一个个软件群。当你把这个软件群打开以后,你看到的是另外一堆堆的软件群。不过他们可能都叫同一个名字。So Confused。

 

在这种软件名字都没有办法对应到一个原子的软件部件。可想而知当我们组织一个大规模软件系统的时候,会变成什么样的局面。

 

讲讲软件包的安装

 

讲了很久大家都不在意的软件名称和版本问题,我们再讲讲软件的安装。很大程度上,可安装是一个大规模系统是不是真的能做到解耦的前提。是一个大规模系统是否能够拆分成模块的一个特异性指标。

 

这听起来怪怪的,一般解耦我们所想到的都是API之间的划分,模块之间的切分。和安装有毛关系呢?

 

这里有一个不为人所注意的细节点。模块切分,API的定义是一个静态的事情。什么叫做静态的事情。我们想象一下,当我们拿到一个软件以后,无论是一个可运行的程序,还是一个动态链接库。你能从这些个二进制文件中看到任何API信息和模块划分信息么?如果API不对应,除非业务跑起来,并且出错。否则你永远无法获知这个二进制文件是不是你所需要拿到的那个二进制。也就是说,任何一个系统在运行起来前,都要确定所有的部件都是它自己所宣称的自己。这种“宣称”不是我们维护一个excel表,或者用一个文本文件所能跟踪的。它应该是和这个软件形成一个整体。这种“宣称”的动作必须是自动化,和可追溯的。

 

好吧,我都觉得太拗口了,我们还是看看readline的这个具体实例把。

 

黑白色的华为(7)- 敌人是规模

 

上面这幅图是从readline-6.2-10.el7.x86_64.rpm这个软件包中解析出来的软件包的信息。大家可以看这些信息的完整程度,其中除了软件包名称,版本,构建日期,签名信息,说明以外,甚至还还包含这个软件的开发网址,和联系的邮箱。所有的这些信息都被规范化,格式化了。命令是rpm –qpi readline-6.2-10.el7.x86_64.rpm

 

再看下面的这幅图,命令是rpm –qpl readline-6.2-10.el7.x86_64.rpm

黑白色的华为(7)- 敌人是规模

这个图显示了readline软件包里面包含了什么问题件。显然,这个是一个很简单的小软件,里面只包含了一些动态链接库,还有一些说明文件。而且清晰的显示了这个软件包安装以后这些文件会放在那些目录下。

 

让我们在这里稍微走的远一点。上面的这个图列出来的是x86_64的包,也就是64位的包,对于一个需要混合支持64位,32位的系统来说。我们在用同样的命令来看看32位软件包的文件吧。执行rpm -qpl readline-6.2-10.el7.i686.rpm

黑白色的华为(7)- 敌人是规模

 

可以看到,除了那些无关紧要的说明文档以外,前面四行的动态链接库,也就是要提供给客户用的库的路径不一样了。这样,在安装的过程中,两个不同的软件包,一个支持64位开发,一个支持32位开发,他们会被安排到不同的目录下,而不是放在相同的目录下互相覆盖。

 

这里有一个非常重要的信息,一个单独的库文件是不会附着任何的路径信息的,如果没有一个完善的安装系统,那么一堆堆的库文件,甚至是名字相同的文件很有可能就会产生冲突,覆盖等。而引发的问题,不运行起来是根本没有办法发现的。在华为,我斗胆猜测一下,文件重名覆盖这种看似“低级”的错误可能在一线屡见不鲜把。而且这种问题一旦出现都是大问题,因为不运行,不出故障就没有任何人能发现。

 

再看另外一个从软件包中能解析出来的信息。执行rpm -qpR readline-6.2-10.el7.x86_64.rpm可以看到下面的列表

黑白色的华为(7)- 敌人是规模

 

这个列表显示了readline这个软件包如果要能安装在系统中,需要的前提条件,比如它需要有一个sh脚本(第1行),比如它需要libc库的版本是6(第6行),比如它需要rpmlib这个软件包的版本不能高于5.2-1(最后一行)。

 

这是非常重要的内容,软件包里的信息不但包含“我是谁”,“我有什么”,“把我放在那里”,还有“我需要什么”。所有的这些信息都没有办法通过一个库文件来提供,也没有办法维护一个excel表或者文本文件来提供。这些信息必须要和我们所提供的软件形成一个不可分割的共同体。否则,所有的这些信息都会在传播的过程中,CI的过程中,安装的过程中,维护的交接过程中,口口相传的过程中丢失掉。也就是说,除了代码,二进制程序以外,一个软件还有很多附属的信息,这些信息必须电子化,并同时随着代码和二进制程序一起迁移。

 

如果类比一下,这也是为什么容器在当下的世界能大行其道的愿意之一。除去运行效率较VM高以外(其实也没有替代VM),一个容器就是一个“自包含的运行实体”是核心的原因。

 

回到刚才我们看到的许多软件组成的OS图,正是通过各个软件包暴露出“我需要什么”,才能使得这些软件之间相顾独立,各自演进。每当一个软件被发布出来的时候,他们绝对不会是一个个独立的文件,它们必定要讲清楚我提供了什么,我还需要什么。才能使得各种不同的软件协同在一起成为了可能。让我们再看另外的一个例子。

 

下面是从Ubuntu系统中找的一个软件库为例,这个库的名字是libopencryptoki0,提供pkcs11安全规范中的库和接口。我们可以看到这个库是有那些文件组成的。同时下图中包的信息中得到维护的网站在哪里,甚至维护者是谁。这个库依赖版本的glibc库。

黑白色的华为(7)- 敌人是规模

对于这个2.3.1版本的libopencryptoki0来说,他需要libc6库的版本大于等于>=2.14, libssl的版本>=1.0.0等等(中间depends行)。

正是由于这种互锁关系,使得各个软件之间形成了既独立演进,又互相依赖,互相制约的发展关系

 

很大程度上,这就是Linux这样一个庞大的系统,团队之间甚至都没有见过面。人员分属不同的公司,感觉好像这个开发会乱成一团。但事实上井然有序,保持了一种很有意思的动态平衡。其中包的管理起到了决定性的作用。

 

一个大规模软件系统解耦的前提是:必须有一种机制保证拆分出来的各个部件的所有信息在传播的过程中,在组合的过程中不丢失。而华为很多情况下,各个部件交付的是一个个tar包,一个个file文件。本来需要依附在这些tar包,file文件上的诸多信息都会丢失掉,最好的情况也就是通过文件名区分一下版本,arch信息等。但文件名是易变的,也是没有办法管理的。而且承载的信息量太少。这也是我们的系统在规模大了以后会非常的无序和混乱的原因之一。

 

以上就是现代大规模软件开发的基本方法之一:解耦不是API层面的事情,首先是Pacakge层面的事情,如果软件部件无法以独立软件包的形式进行发布和演进,那么解耦就只是一种幻想。只有将一个庞大的系统拆解成为一个个可独立演化,可独立安装的软件产品,才能够真正做到解耦,才能真正实现业务的快速演进,才能保证架构的灵活性,软件才能做到大而不乱。

 

这种设计思路并不是只有OS行业独有的,比如python系统中的pip系统就是一个模块的安装系统。Java的maven也有点这种味道。

 

解决问题了么?

经过这样的改造,我们的开发流程会变成什么模样呢?可能会变成了这样的过程。

 

1.       设计好软件整体架构。把框架分解成N个独立的软件系统。

2.       每个分系统的团队独立进行软件开发。指定各自的版本节奏。

3.       对于某一个特定的软件版本,协商好API原则。

4.       分系统提交的不是tar系统,而是该软件的安装包。

5.       整个系统是若干个安装包系统的特定版本集合。

6.       整个系统的交付是一个安装包的集合。

7.       交付用户

8.       用户使用特定的安装规则进行安装,使用。

9.       在使用过程中提出新的需求,解决新的bug,发现新的安全漏洞,做出新的feature。然后执行下列程序:

If __builtin_expect(need_refine_arch, fase) {

       Goto 4

} else {

       Goto 5

}

 

整体的区别在于:

1.        如果不是影响系统大的架构,那么所做的改变只是升级相关的软件包而已,而不是从头到尾走一遍流程。你从来不会因为微软发布一个安全补丁而重新安装整个系统。

2.        如果是架构层面的改变,更多的依据这些分系统软件的最新版本,重新再画一条线,来重构系统。

 

功能还是bugfix

我们从宏观上讲了一个大规模软件要能拆解成为可独立演进,独立安装的软件。但是这里只讲到了大的软件版本。我们清楚了当CentOS 7.X这个软件系列对应的readline的版本是6.2以后,基本上CentOS 7系列就明确readline的版本就锚定在6.2了。那么有如下的问题:

 

1.         如果出现了bug,CVE漏洞修补等问题以后怎么办。

2.         如果需要增加feature怎么办。

3.         如果确实由于某种原因需要更改API怎么办。

4.         如果确实由于某种原因需要升级到6.3怎么办。

 

要回答上述的这些问题,我们需要回到代码的源头。看看代码在代码库中呈现是什么样子。因为只有明确了代码在代码库中的布局,你才能明确bugfix, 特性开发才能放在什么地方。

 

还是以readline为例子。让我们打开一下它在git库中的代码布局示意图吧。

 

黑白色的华为(7)- 敌人是规模

 

对于一个软件来说,都有一个主干,若干个分支,主干走大版本,接纳大的需求开发,接纳大的特性。而分支通常是指小版本更新。那么我们就以readline 1.0开始做一次开发之旅吧。

 

1.         黄圈代表了开发的起点时间。从这一个时间点开始软件开发。

2.         从黄圈到绿圈,这段时间是允许进行特性开发,允许大批量代码进行提交的。

3.         绿圈这个时间点就到了frozen时间点,绿圈以后就不允许接纳,合入任何特性开发了。但是允许合入bugfix。

4.         阶段3一直延续到红圈,红圈代表整个readline 1.0开发的结束。对外发布的软件就是从红圈处编译出来的软件。

5.         在红圈处,git会拉出来一个分支,这个分支代表着readline 1.1开发的开始。同时主干(横向的线)开始readline 2.0的开发。自此井水不犯河水(不完全准确)。

6.         Readline 2.0的开发流程和readline1.0的模式完全一样,重复进行。

7.         从红圈处拉出来的分支(向上的折线),开始接纳bugfix,CVE安全补丁等。但是在这个分支上,原则上是不允许合入特性features的。

8.         如果某些bugfix和安全漏洞也影响2.0的开发版本,那么也会同时回合到主干上。

9.         在蓝圈出发布readline 1.1,然后拉出一个分支,开始readline 1.2的开发。周而复始。

10.     当然,有些情况下,由于某些原因,在1.1的地方会再次分叉,出现诸如1.1.1这样的分支(这种情况比较少见)。

11.     还有一些情况,会在某些地方拉出给某一个特定客户的定制分支,这个在外部,一般都是需要收取高昂的服务费用的,因为每一个定制分支其很大程度都需要将主干和维护分支的特性和补丁回合,代价极其高昂。

 

这是一个有趣的图景,对于不同的开发团队来说,各自会在各自的开发分支上进行开发,互不干扰,而且还能形成一定的协同。

 

这只是一个单体软件的开发。想象一下如果是一个平台软件,成千上万的软件都是以这种张牙舞爪的形式在git库中不断分叉狂奔是这么样的一种景象。很有趣。

 

但是,恰恰是这样的一种组织形式,使得各个团队之间能保持很好的隔离关系,还能有一定的协同关系。配合上面章节讲的软件形成一个个软件包。那么一个大规模软件系统的长相是这样的。

黑白色的华为(7)- 敌人是规模

 

不解释了。相信能看懂的就能看懂了,看不懂的也就看不懂了。

 

实际上,在华为的软件开发过程中,更多是按照特性的维度来划分开发团队,同时进行技术演进的。这是软件作为硬件附属品时代的开发组织模式。但是华为的软件也越做越复杂,本身就是一个庞大的体系了。这个时候,需要做出一些调整。所谓特性,所谓模块,更多是一个个独立的软件实体(产品)呈现的。同时,每一个软件实体在代码库中应该有自己的演进逻辑,同时要对应到git的代码组织中。在这种前提下,开发团队的划分更需要依赖软件实体进行划分,进一步按照git库中的分支进行划分,而不仅仅是笼统按照所谓“特性”进行划分。

 

代码复用—从解耦到耦合

虽然我感觉前面的章节已经吓跑了很多读者,但是我还是决定再深入的讲一下代码复用,这个话题也是在公司耳熟能详,但是似乎也没讨论出个所以然的话题。

 

Ctrl-C + Ctrl-V是程序开发的第一宝典,google, 百度是程序员真正的老师,把百度放在这会引起很多码农的极度不适,虽然百度经常搜非所问,但在你搜索一个go语言语法的时候,它给你推送一个美女图片让你适时的放松一下也是极好的,我不得不说,没有谁比百度最懂码农了。

 

传统上,Ctrl-c, Ctrl-V所带来的问题基本上有两点:

1.         人力重复消耗,这个毋庸多言。

2.         代码错误的扩散,一个代码片段如果有问题,会通过Ctrl-V的模式传染到其它的系统中。但是代码的修复却无法即使同步给所有Ctrl-V的用户。

 

事实上,当一个代码片段被Ctrl-V到其它任何一个部件的时候,实际上两个软件就已经解耦。和规模软件的模块解耦相反的是,在代码层面,反而需要的是高耦合。耦合度越高,则代码开发效率越高,代码的稳定度越好。

 

这种代码高耦合的模式在传统的C/C++主导的世界是以动态链接库实现的,在windows下是DLL库,在Linux/Unix世界是.so库的形式。这种模式存在了几十年,它有自己的优势,但是也存在问题,最重要的是会将系统切分的越来越碎,而且使得依赖关系越来越复杂。这也是为什么出现了docker容器这样系统,索性将所有的.so和业务打包成一个系统。这样就在一定程度上脱离了平台的舒服,实现了另外一种层面的“解耦”。

 

Docker解决运行面二进制的打包和解耦。在程序层面呢?是否也存在这样的一种机制呢?答案是肯定而且简单的,许多新兴的开发语言都已经在这条路上走很远了。解决程序高内聚,高耦合的方法是:如果不是万不得已,最好选择go, rust, python等新型语言来写程序,传统的C/C++是没有办法做到真正代码复用的。

 

在go, python中,会有一个很有意思的语法import(rust语言用use来展现自己的卓尔不群,超凡脱俗)。Import这个简单到不能再简单的syntax是将外部的一个代码片段引入到自己的程序中,很有一点C的#include包含头文件的味道,但是这里面的重大差别是,import是真的把代码包含进来的,而不是简单的只把声明包含进来。既然把代码都包含进来了,和我们用Ctrl-V有啥区别呢?区别仅仅在于这个syntax是电子化的。如果你引用的一段代码被修正了一个小bug,你是可以仅仅重新编译程序就能够完成修复工作。而不是手工再merge相关的patch到自己的代码中。仅仅就这个差别就会极大的消除“忘了回合补丁”这样的“小问题”。

 

除了便于进行补丁回合,import机制才真正实现了将代码进行了归一化的管理。让我们看一个实际的例子吧。一个go语言的小例子。

黑白色的华为(7)- 敌人是规模

在最后几行,我们引用了github.com/urfave/cli这个功能模块,这个URL是这个代码在全世界的唯一标示。事实上,在所有新兴语言的世界里,都试图构建一个“零件的世界”,我们看到的是一个个的软件零件。每一个零件都有一个唯一的URL,甚至有版本的概念,比如最后一行gopkg.in/yaml.v2。

 

因此,在用这些新兴语言进行开发的时候,更多的时候是在github上搜索各种零件,然后将这些零件粘合在一起。这不但大大提升了开发效率,更是一个积累的过程,当这些零件越来越多,越来越丰富,功能越来越强的时候,甚至一个大型软件都能在1-2周之内做出一个原型框架。

 

看到这里,如果稍微联想前面章节我所述的OS的开发和演进过程,是不是有些眼熟的感觉。在用这些新型语言在进行开发的时候,是不是也是一个个独立的源码部件在自我开发,自我演化,自己拥有自己的版本。而一个单体的大型软件的开发也是类似在这些部件的某个时间点上画一条线,获得某个稳定的源码基线,最后粘结成为一个软件系统。唯一的区别是宏观上的软件系统是二进制部件的划线,而微观上是源代码的划线。颇有点佛曰:一花一世界,一叶一菩提的味道。好吧,最终我也无可避免的滑入了佛学的深渊。

 

而在华为内部,我们却没有这样的一个统一的源码仓库,没有这样对于开发者来说的讲零件放在唯一URL的地方。举个例子讲,如果我开发了一个很好的go语言的模块,我放在哪里呢?我如何共享给别人呢?别人怎么引用呢?难不成我把公司的代码放在github上,然后让其它的开发部门来进行import引用么?因此,公司需要有这样的一个公共代码仓库,同时提供相应的工具来服务于这些新型的开发语言。真正使得开发变成用胶水拼零件的苦力活,而不是一个智力活。这是解决公司代码复用的不二法门。那么C/C++怎么办?没办法,按照原来的路子走吧。并不是所有的业务都能享受世界发展带来的红利的。唯一能做的是,后续尽量减少C/C++这样的语言开发量吧。

 

这些新型语言显然不只是有import这样的能力,go,rust这样的语言也都逐步拥有内嵌的测试框架,使得开发和测试真正能成为一个整体,而不是分离。还有很多很多。我就不在这里一一展开了。

 

码农是一个艰苦的职业,它的艰苦很大一部分是由搞编译器的家伙们制造出来的,不过也恰恰是搞编译器的这些家伙,他们逐步把一个高科技的脑力劳动变成了一个体力活。所以,如果想做到高内聚,代码复用。华为的码农们,学点新东西吧。

 

总结一下

这一章是所有章节中最难以表述,最具“技术深度”的一个章节。好在这也是唯一这样的章节。希望大家能看的懂我在讲什么。

 

宏观上,大规模软件系统的开发首先是一个组织问题,而不是技术问题,更不是简单的API解耦的问题。大规模软件解耦的核心是功能软件实体化,可独立演进化,可安装化。

微观上,利用新型的语言,尽最大可能减少代码量,提高代码复用度,从解耦到耦合,实现高内聚。

总体上呈现的是外解耦,内耦合,低外联,高内聚。

 

探讨完解耦这个能让软件做的更快,做的更大的话题,在读者散干净之前,我们尽快进入探讨组织开发大规模软件的第二个话题,如何少犯错。下一章:从加法到减法。

 

 
上一篇:BufferedInputStream和BufferedOutputStream的使用极其方法


下一篇:BufferedReader导入踩坑