仓库源文

.. Kenneth Lee 版权所有 2019-2020

:Authors: Kenneth Lee :Version: 1.0

自下而上和自上而下的设计


架构设计很大程度上是部件分解和接口设计。部件分解常常有很多其他要素在左右(比如 团队,现有架构,产业情况等),所以,架构设计的很多工作就出现在接口上,接口的性 质决定架构演进,因为接口意味着接口两侧的团队的利益分配和变化,这个最终决定着整 体的发展。

我们通常有两种方法推演我们的接口,一种是自下而上,“我有什么,所以我给你什么”, 另一种是自上而下,“你要什么,我给你什么”。

理论上,显然第二种才能获得实际的商业利益,因为无论你有什么,如果用户用不上,这 个东西都是多余的。但实际上,第一种也能获得商业利益,因为“用户不一定知道你有什么 ”。我常常用的一个例子:用户可能觉得malloc和free做内存分配已经很好了。但没想到你 realloc()的话,可能实现得性能更高。这样,提供realloc()的方案,提供了更优的市场 竞争力。

所以,在实际工作中,其实我们两种方法都会使用。

但今天我要讨论的不是这个问题,我要讨论的是:这两者不是平等的。第一种方法我们会 用,但从架构的角度,这个方案必须非常小心,必须从属于第二种方案。正如这里讨论过 的:

    :doc:`如何说谎`

架构是在圆一个完美的慌,你不能缺乏“人设”,看见什么功能,就往里加什么功能。你可 以做realloc,但一旦你做了realloc,你的设计就被绑定了,你不基于从上往下设计这个 逻辑完满的慌,你的结果就是你下层的逻辑早早就开始自相矛盾了,上面保证给使用者完 整逻辑设计就不存在了。你看见人家可以realloc你就加realloc,看见人家可以 recheck_block_link你就加recheck_block_link,看见人家可以thread safe你也thread safe,你在用户眼中的呈现就是精神分裂的,这样你的慌很快就会圆不下去,然后你的整 个架构也就崩塌了。

同样的,你做一个芯片,对外宣称你既可以支持x86的功能,又可以支持MIPS的功能,还可 以支持RISCV的功能,还有一组协处理器,那你这个方案必然是没有竞争力的,因为你的逻 辑必须互相妥协,你的逻辑就很难很“直”(我有什么我正好在提供什么),不直的逻辑, 演进下去,就必然导致软件的逻辑动不了,这个软件生态就死了。

架构是一个独立于功能设计的逻辑,不能用功能设计的逻辑去想它,对架构逻辑缺乏敬畏 ,会导致我们一次次重建,而我们还不知道为什么。

附录1:一个架构接口决策的例子

为了说明前面说的架构设计是怎么考虑问题的,我给一个过去我们做加速器的例子作为参 考:

我们有一个加速器框架,可以在用户态open一个设备,然后mmap它的硬件,然后通过读写 mmap的内存和设备交互。我们以此为基础设计了一个开发库libacce,里面包含open, close, send, recv这样的接口,为了保证通用性,这个库没有任何锁设计,我们要求用户 自己选择自己的线程库和相应的锁来实现保护,这样这个基础库具有最大的自由度,他愿 意选什么线程库都可以。

后来,我们开始增加需求,如果发生了硬件错误,我们需要反馈给用户进程,为了保证整 个逻辑自洽,我们的方法是,如果发生了错误,我们直接发一个signal给用户进程,让它 自行处理。

有用户觉得这样多写很多的代码,要求我们帮他们解决。所以,我们的工程师就做了这样 一个方案:在libacce里面增加了一个信号处理的函数。在里面重新open一个句柄给用户( 如果设备仍未就绪,等待等操作就可以在这里完成了)。但这样的结果是,他需要把信号 处理和send/recv排队,天然他就需要选定一个线程库,他选了pthread。

我否决了这个方案:你已经承诺了用户无线程依赖的接口,你怎么敢轻易去破坏?你基础 的库里面自动加signal处理函数,如果用户本身有signal处理函数怎么和你共存?

最终的结论是:libacce不改变,我们增加一个依赖pthread的库libacce_ex,后者主动安 装signal,并定义用户的收发模型,实现无锁的一个调度模型,保证用户在这个调度下, 可以自动等待设备重建。

可以看到,架构考虑问题的模型总是丢开具体的功能逻辑,而从整个“用户观感”的角度来 考虑问题的,如果我们缺乏这个角度的控制,随便给接口加功能,很快这个接口就演进不 下去了,整个生态也就维持不住了。

在这个例子中,libacce的口径决定了内核和硬件是怎么圆这个谎的。这个逻辑越少,其他 基于它演化的概念互相冲突的机会就越少,至少一个逻辑可以维持,libacce_ex也可以维 持。但如果你直接把逻辑加到libacce上,libacce的基础就会变得很大,很快你就无法做 到面面俱到了。