3.95. 架构控制的从权问题

前几天在这个问题下面吐槽了几句:in nek:为什么有些大公司技术弱爆了?,估计不是一直看我的随笔的人很难看懂那个吐槽指向的是什么。正好今天台风天,飞机飞不了了,我来补充一下那个吐槽背后的架构思维。

架构好不好,外行是看不出来的,甚至就算你是内行,不花时间你也是看不出来的。比如我遇到过这么一个案例:老代码支持新硬件平台——我简单一点举例——比如这个新硬件平台初始化的命令字从HW_CMD_START(1),修改成了HW_CMD_START_V2(2)了,这个修改从架构师的角度来说,我们要考虑这么些问题:

  1. 旧平台是否还要兼容?

ii. V1到V2的这个修改,是否仅包含了这个命令字的改变,这些改变,是否对硬件抽象层和软件应用层之间的语义造成了影响?

iii. V1和V2的代码是要共二进制版本(需要启动时动态配置),还是源代码版本(需要静态配置),还是可以同时使用(需要运行时动态配置)?

等等。

不考虑这些问题,老油条或者无经验的新手怎么写这个程序呢?——很简单,直接把HW_CMD_START修改成2,就可以了。你不验老平台的硬件,你都不可能知道,等你知道的时候,成本已经投出去了,你也没有回头的余地了。

也许你觉得这个是个小问题,很容易就改回去了,但把问题稍想复杂一点,硬件的修改从init-start-work这样的初始化过程,变成了pre_init-start-config-work这样的初始化过程呢?你的数据结构必须根据这个过程放到不同的stage上去,这个问题只要不综合考量。而直接根据V2的硬件修改代码,改为以后你发现V1的模式还需要继续使用,你这个代码已经变成一团麻了。

我前段时间还遇到过这么一个问题(抽象过以便好理解,和任何事实无关):一个PCIE设备pdev,分出部分硬件资源,用软件模拟另一个设备vdev,虚拟设备为进程提供服务。方法是每次分配了部分资源给一个硬件文件(file),打开文件的时候分配资源。

好了,现在file被进程打开了,它要锁住vdev不能被释放,而vdev要锁住pdev不能被释放。很显然,这个设计应该是file->open()负责get_device(vdev),vdev->create()负责get_device(pdev)。但有人就可以为了方便,直接在file->open()中get_device(vdev), get_device(pdev)。这样结构的结果就是中间这个vdev层其实根本没有意义。因为三个层次的“语义”完全交联在一起了,要改一起改,不看另一个模块的代码实现,只看接口,你改得对不对,这要看天。

这种情形发展到极致,就是interlace:比如这样,把硬件A1, A2, A3抽象为A,A给B提供接口,B的实现里面还要判断如果是A1如何,是A2如何,是A3如何……然后,B给C提供接口,C里面也要做这个判断。看起来分了三层,其实三层和凉席一样,分层和语义完全正交地交织在一起。不深入分析它的流程,你以为是三层,看进去……你只想操他祖宗十八代。

还有一些更隐性的,表现为一种Careless的文化。我遇到过一个这样的例子:有人做一个Linux解决方案,方案中有一个硬件,需要做一个驱动,这个驱动要交付的时候要交付3个分支,比如3.27-customized,4.10-customized,4.17-cusomized。你让我做这个方案,我会有两个选择:

方案1,先上主线(Linux主线也行,自己的上游主线也罢),然后落地到3个分支上,维护4个分支。

方案2,独立一个驱动目录,编译后在三个分支的某个版本上测试。保证质量。

这两个方案都是程序员要为一个自洽的名称负责的。无论是分支作为一个主题,程序员为整个分支的逻辑自洽负责,还是驱动作为一个整体,程序员为这个驱动的整体逻辑自洽负责。他考虑这个特性的逻辑的时候,都是可以面面俱到的。

但有人会做出这样的方案:

方案3:独立一个驱动目录,可以基于脚本拷贝到三个customized分支目录中,这个驱动同时支持三个分支。

很多人都看不出这个方案背后那个careless的态度:这个方案有人为驱动负责,也有人为没有这个驱动的customized分支负责,但没有人为这两者的“联动”负责。因为你决定怎么拷贝驱动到Kernel Tree的时候,并没有任何机制保证Customized Kernel的Maintainer得到通知(前面的方案2配套的是死分支,是一个tag)。两者没有一个严格的“细节配套”逻辑,维护下去一定会发生滑动,优化不会考虑发展和细节接口(因为根本没得想,对方是动态的)。觉得这个方案可行的人,背后其实根本没有打算做一个严密的语义逻辑,这种烂架构,你也只能见一步走一步,架构控制就没有了。

这些情形,我称为“压扁”。架构本来是立体的:

压扁了,它能跑,但它不再具有活性了,这种代码没有未来。没有未来不表示它活不下去(有钱继续投资,什么都能活下去),没有未来表示它没有办法面对更多的需求。所以,一个架构不好的代码,只要市场还在,它就能活,但你要它做出更多的变化,它就是不行(但这个团队就是会找出各种理由,证明“这个不行是应该的”,“这是我们这个架构的‘特点’,和对手‘各有优势’”。

很多人讨论中国操作系统怎么发展不起来,有一个点是说它没有生态,这句话没有错,但你以为解决生态了这个问题就没有问题了,这完全是幼稚。就算你有很大的投资,很多的用户,但你的代码早早就压扁了,你就只能停在那个版本上不动了,怎么可能有未来?整个Android的代码都给你,专利版权都给你,你以为你能维护下去?你看看Ubuntu,Redhat,Suse,Windows哪个商业发行版没有“生命周期”的概念的?就算是号称无缝滚动的版本,在细节上都是有生命周期的。但你再看看哪个“国产操作”系统有这个概念?跟这些人谈,他们连“生命周期”是什么都没有搞明白呢,他们只能等现在这个版本实在活不下去了,找别人的版本重新来过。以前加进去得意洋洋的特性还能不能用?还是那句话,看天。

扯远了,回到压扁代码这个问题。

这些问题,在你面对着每天几十个Patchset的日常工作中,你根本不可能深入进去一个个看,你就只能挑重点,挑不到的,遇到这样的人(不上心,只要现在能跑就行的人),你就只好忍了。你只能把有担当的放在maintainer的位置,负责做gatekeeper。你才有可能做有效的控制。如果都是这样的人,架构师就只能从权,从架构师变成项目经理,只负责抽鞭子,不要架构,要“结果”,要能运行,这样,短时间内你会有结果(先忽悠住投资人再说吧,投资人是天然的外行,就算他不是外行,在海量细节面前,他也得给我变成外行)。

后面的,如果团队的水平起来了,有未来目标的架构师可能会开始基于前面这笔投资得到的经验,开始整理架构,如果都是些烂泥,那就赶紧找个强势的维护经理接盘吧,反正系统成型以后,就无所谓“架构控制”了。架构控制只能发生在构筑的前期,房子都盖好了,那时是个装饰房间的问题,你再跟我说什么承重结构,什么暗管布线防水?我不会比一个普通的工程师表现更好的,或者说,也许比一个表现更好,但也不会比2个,或者3个的表现更好的。一个架构师的工资顶五六个普通工程师是少不了的,架构师干这个,基本上就是浪费钱,不如把他撤了完犊子,否则这家伙一定会进行各种表演(架构重构啦,清lint告警啦,降低圈复杂度啦,代码锄奸团啦……),基本上有破坏没建设。

所以,所谓一个“求道”(事实成功)的架构师,大部分时候我们要根据团队来“从权”,对这个团队来说,说到底就是求仁得仁,团队从上到下,都没有情怀,都是能跑就好,架构师要成事,就只能一起从权。只有这个团队心里有点星辰大海,你才有可能发生一点点改变。这些东西,领导是“外行”,他们是改变不了的,只有一个个的工程师自己有心,才有可能从星星之火,变成一种文化。

这就叫虚心实腹,九层之台,起于累土,总耍小聪明,谁都救不了你。

好了,前面说的这个逻辑,其实只是我的逻辑的一部分。这里这些头头是道的道理,是“架构”这件事的逻辑根本。现在我们来谈“变数”。

很多领导、投资者,特别是国内的,对“架构目标”不怎么在乎,有一个很重要的原因是:“架构是针对未来的设计”,考量未来是有风险的,如果没有未来,这个根本就是浪费。在团队能力不足,经验不足的时候,强行要求进行架构考量,这很容易造成浪费。这就叫“绝学无忧”——你一堆的理论,成本高昂,但这个理论是否真的可以实现目标,从“结果”上根本无从判断,不如一开始就不要听你这些理论,而是要求你出结果。

这是个平衡的问题。

我现在写模块设计,基本上是先确定API接口,进行功能和成本上的推演后,才进入编码的,这样不容易在参数和接口众多的时候把架构“拍扁”(功能在几个模块之间调配的时候,如果一开始不考虑清楚模块的角色,很容易放错地方,就形成“拍扁”的局面了)。但刚参加工作的时候,我很少这样干,因为我根本就写不出来。我对实现一个API到底需要什么东西都不清楚,定义一个API,到实现的时候发现参数不够,或者发现某个索引隔着两个对象,根本拿不到索引,这些前期的定义都失去意义了。

即使是现在,面对复杂的面向对象系统,我在前期定义API的时候,都不会把全部参数定死,而更关注当前对象的“角色”。“角色”对了,逻辑放错的可能性就低,少一两个索引,调整一下几个接口的参数,就可以关联起来了,重点还是决定某个代码逻辑到底应该放到那个模块中,作为那个模块“身份”的一部分。

而对于初次构建的大型复杂系统,比如某些加速器,在一个很复杂的软件计算模型中,抽出其中一部分到硬件上,首先CPU的流水线就发生了变化,接着是系统总线,内存系统(包括Cache系统)的压力也发生了变化,Thoughput和Latency的控制变量也跟着变了。可能你原来的控制要素是memcpy的效率,现在变成了smp_mb的boardcast的效率。比如,原来你用1024个线程进行分布计算,每个可以并行的流程长,锁冲突不严重,现在很多数据流到了加速器上,CPU线程的核间冲突就变得很严重。这种“经验数据”还没有出来的时候,架构控制就无从谈起了。

这种情况下,对软件接口的要求是要等这些变数都相对稳定了,才会变得重要。

所以,有一些阶段的产品,架构师只能在前期进行最大流量,最大时延,客户群关键需求判定这些方面的推演,之后就只能退化为前面说的拿鞭子的项目经理,保证结果先出来,在出来一波后,才能谈软件方面的架构控制和长远发展。

这种,是需要折腾一两个版本以后才能确定方向的。

这始终是个度的问题,没有什么明显的Pattern可以从表面上看出来的。

说了半天,好像怎么都没说,说到底,对于一个复杂系统,控制要素在不同的阶段可以控制的东西是不同的。对于连运行都没有运行过的新产品,软件架构控制很弱,重点是系统的可行性分析,这时系统工程师逻辑(就是计算“能跑”,“可能”的逻辑的人)会占据优势。等有一定的数据了。系统的长远发展的问题就变得越来重要,这时架构控制的作用才会显现出来。到开始上量卖出规模了,这时这些控制的作用就越来越弱了(但仍然可以有控制,因为控制不好还是可以一次把立体的架构拍扁),看在原来的基础上能活多久了。

所以我才说,强行把不同阶段的产品拼在一起,对两个产品都是伤害。