仓库源文

.. Kenneth Lee 版权所有 2018-2020

:Authors: Kenneth Lee :Version: 1.0

架构控制的从权问题


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

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

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

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)。两者没有一个严格的“细节配套”逻 辑,维护下去一定会发生滑动,优化不会考虑发展和细节接口(因为根本没得想,对方是 动态的)。觉得这个方案可行的人,背后其实根本没有打算做一个严密的语义逻辑,这种 烂架构,你也只能见一步走一步,架构控制就没有了。

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

    :doc:`让代码变立体`

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

很多人讨论中国操作系统怎么发展不起来,有一个点是说它没有生态,这句话没有错,但 你以为解决生态了这个问题就没有问题了,这完全是幼稚。就算你有很大的投资,很多的 用户,但你的代码早早就压扁了,你就只能停在那个版本上不动了,怎么可能有未来?整 个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可以从表面上看出来的。

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

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