3.281. 架构和装修
最近自以为看明白了一个问题,这里总结一下。我的描述比较极端和绝对,只是为了突出问题,不指向任何具体的人和事。
有些工程师刚刚从代码维护上转来做架构设计工作,设计拿出来总让很难受。我明确知道这个设计是错的,但很难给他和其他不是做架构设计的人解释他“哪句话说错了”,因为问题不是这句话对不对,而是这句话该不该说。
我总结这种问题是这样发生的:
比如他习惯了维护一个网卡,这个网卡需要向一个内存池里面填数据报文,内存池分成多个子池,网卡基于对端的mac地址对内存进行分类,多个子池共享一个中断源,在任何一个子池超过水线的时候都会产生一个中断。
他平时很熟悉这些中断,水线等等的行为,所以他也很擅长如何把哪几个子池合并,也知道如果增加一个硬件故障中断应该把这个中断和水线中断合并还是独立出来。
现在如果让这位工程师设计一个新的网卡,如果他缺乏架构设计的经验,他可能上来就开始说,内存池可以合并成一个让多个CPU无锁共享,为此需要再引入一个内存分区的概念……等等。
作为一个架构师,我看到这种描述就脑子痛,因为在我看来:你做一个新的网卡,我用不用内存池都没有打算呢,你上来给我谈什么内存池的,我的逻辑应该建在哪里?
如果原来的概念空间仅仅需要改进,我何必再做一张网卡呢?我把原来的网卡升级一下不就完了?我必然是基本条件发生了改变了,我才另外做一张网卡呀。所以,作为一个架构师,我第一件事,是要找的新的房子的地基在哪里。什么内存池,什么中断源,都不是我的逻辑地基。因为不是我做了这些事情客户就会埋单的啊。
所以,前面说的那位虚拟的设计师的行为其实是在做装修队的事情,是在已经完成的构架上做装饰而已,最大的动作可能就是打掉一些非承重墙,加个吊顶,是肯定不会动承重墙的。但构架设计是建房子,你不能把装修队的设计思路放在建房子上。
对我来说,做一张新的网卡,核心的,不变的逻辑地基是“把网络上的消息送到CPU上去处理”,这是干这件事最坚实的,最不可能改变的逻辑。后面有什么搞不定,都可以放弃,只有这个事情搞不定,这个事情就不用干下去了。然后我才会开始建我的其他逻辑,比如,我可能识别到另一个需求:这张网卡大部分流量都是转发报文。好了,我们的逻辑可以向上长了,如果我把路由判断下沉给网卡,网卡和CPU的通讯压力就可以降下来,但路由算法如何表达就成为一个关键问题了。是在网卡里面加NP或者MCU好呢?还是使用一个硬件加上一组可配置参数更好呢?这些问题哪个都比用不用内存池问题大。
这就构成一个房子一样的结构,网络消息转发和发给CPU是地基,路由判断下沉是地基上的设施,用NP表述转发语义是再高一层的逻辑,地基和底层的设施很简单,但越往上展开,依赖的逻辑越多,上层依赖它们的精巧逻辑就会越复杂,如果下层的地基没有打好,上层那些精巧的逻辑就越不稳固。
这些东西都看不见呢,你给我大谈内存池,我看见就脑子乱,你内存池赖以生存的那些逻辑还存在吗?就好比我们正在讨论房子怎么建呢,你跟我谈三楼的厕所下水管道必须从西侧外墙出去?然后问我“这难道不是这个房子设计的一部分?”我就很晕,不知道和你说什么。
装修和架构的区别在于:装修大部分不变的东西都是固定了的,你根据一大堆固定的条件决定怎么做就好了。架构不是这样的,架构关键是找到每一层的根基。我们对确定那些条件是否稳固的很看重。对架构来说,只有地基是稳定的(甚至可能地基都不是稳的),然后我在地基上建立一层的框架,然后我思考第二层的时候,发现第一层没有实现对第二层所需的承重,我会拆掉第一层重建的。所以在架构眼中,每个逻辑都是有强度的,这个强度被承载它的其他地基所保证。我们需要一个NP/MCU,是被“路由下移”这个条件所支撑的,而“路由下移”这个条件,是由“网卡的大部分流量都是转发”这个条件支撑的,如果“网卡要用于另一个市场,大部分流量是要CPU处理的,而且这个市场的CPU主频很低,分布这些流量是第一需求”,很多逻辑就会崩塌,我们就需要把依靠这个逻辑的一整套逻辑拆掉,然后加入其他支撑,去保护其他的逻辑投资。
逻辑也不仅仅是客观事实,也有可能是架构师发明出来的,用来支撑上层设计的。比如,他决定:“我们赌明年的内存价格会腰斩,所以我们把网卡上的缓存换成10GB的内存”。这样我们很多设计就可以利用上这个优势了,原来放不进网卡的设计都放到网卡上完成了,甚至为此我们还在网卡上放CPU,这都有可能。这个东西如果赌错了,你要不就是逻辑崩塌,要不就是你的网卡根本卖不出去。
而且这里面有很多技巧,不能用死板的细节思维去看待它。比如为了让你看到整个架构逻辑,我可能在地下室放了一个暂时放了一个光凸凸的柱子表示这里需要承重,这不影响你后面换成一个框架来支撑。同样,我架构上描述一个调度程序,里面只调度了一个任务,不表示后面加逻辑的时候换成调度10个任务,我的架构好不好,不是看它现在说的那个样子,而是看你基于这个修改的时候,是不是无法修改了。架子本身是控制住整个系统的模式不变,不是让架子取代整个系统本身。我做一个中断控制器只支持消息中断,如果它不阻碍你补线中断进去,这个架子就还起到架子的作用。
所以,让逻辑尽量正交也是我们的期望,比如“大内存”撑起“大缓存”特性,但没有大内存,一点都不影响其他特性,这样的架子就比较好用。
也正因为如此,架构设计非常关心抽象的范围。我们给一个总结:“所有中断全部通过中断路由器收集,根据中断路由表转发给不同的CPU”,我们是mean to的,是真的认为所有的中断都是这样的。因为我们下一层设计就可以完全依赖这一点了。我们会说,因为所有中断都经过中断路由器,如果我们初始化的时候把所有中断路由给CPU0,那么我们就认为只要我们没有进行中断路由的初始化,CPU1肯定不会收到中断的。构成这个逻辑后,我说不定因此就开始为此实现一个无锁算法……架构设计上一点点的假设,都会成为非常高层的逻辑的依托,一旦你不成立,这些逻辑全部都会崩塌。比如我已经设计了一个非常精巧的无锁算法,要求周边4个模块给出了保证,它们为了这一点也作出了很多保护性的设计,最后你告诉我,不好意思,RAS和CPU IPI中断可以越过中断路由报上去,我这些工作就全部白干了。
而这种抽象设计,也是架构设计全部经验和技巧所在。我们不少人建第一层架构逻辑,一旦说不清楚,就开始说“请参考后面的细节”,不!我们不看你后面的细节,我们只看你这一层建立的逻辑是不是通的。细节是要依附到这个抽象来的,否则要你抽象干什么?
如果你抽象这一层总说不清楚逻辑,反复说我“后面说了”,那这一层就细节展开就没有支撑的作用,这一层就没有意义了。整个架构设计就失败了。
你做一个中断控制器,说好中断源有多少种,分别怎么报给那个中断路由装置,然后怎么调度给CPU,CPU按什么原则来排队。我们就谈这样的一个结构是否满足我们所有的要求。你这个东西说不清楚,就给我谈“如果qemu virtio里面要产生一个中断,我们可以这样这样,再这样这样……”,你一旦进入这种细节,那你的地基稳不稳这件事就没有了,这种情况下,我根本不知道这个房子会不会塌。而一个下水道从左边下还是右边下的问题,就算解决了又如何呢?
要谈virtio的时候怎么办,你先把抽象的,每种(高层抽象的)中断都是如何被从源调度给CPU的,这一层稳了。你再给我说,virtio产生哪种类型的中断源,对于这种独特的中断源(我说它独特不是说它不在抽象范围内,而是说它在通用的属性中加了额外的属性),额外需要来调度器的路由表中增加什么说明,从而实现某某特殊的效果。所以virtio的行为,是建立在房子下一层的逻辑上的。这个前提还是你的房子的下一层(也就是高层逻辑的抽象设计)本身就稳了。
3.281.1. 附录
3.281.1.1. 关于不能用细节去补充说明高层逻辑问题在进一步讨论
原文最后讨论到的问题,是我和别人交流,别人经常理解不了的问题,请允许我再换一个角度表述一次:
我用房子去比喻逻辑的一层层的架构,这其实在某些情况下是容易引起误会的。因为这样比喻和我们平时的用词是反过来的,我们平时做架构设计,说高层设计的时候,其实是这里房子比喻的低层和下层。这是一种自顶向上的设计模型。而且我认为架构设计只有这一种模型。我们平时也听说有“自底向上的设计”,但这根本不是架构设计,这只是发展系统的方法,因为它其实根本不考虑未来,而我说的架构设计是为一个产品考虑长远发展,考虑如何约束更多的开发资源的投入方法的一种设计,要提前对不存在的东西进行约束,自顶向下设计是唯一的选择。
既然是自顶向下,那么每一层,肯定是一个在这层抽象上完全自洽的一个设计空间,细节的各种可能性都在它的控制范围内的,否则它没有什么用。所以我们需要夯实这一层了,才会进入细节的,我们做这一层,怕就怕我把所有细节都放上去才发现这一层根本不能用来承载这些细节,那细节工作就全部浪费了。
比如还是说那个网卡的问题,你决定放一个NP在网卡上,高层设计的时候你认为“NP可以有很多处理核,可以分100个数据流分别过滤数据包”,结果你没有注意还有一个条件:这个网卡处理的流量,集中到一两个流上,后置数据对前置数据具有依赖性,这个条件。好了,前期你没有把所有的条件都Apply到实现中,你的整个团队全力开动了,做NP的开始做模型,写Synopsis,做网表,做NP软件的设计算法,做精巧的锁机制,做OS驱动的开始做npoll模型,开始和做硬件设计复杂的runtime_pm机制了……做这些工作的时候,他们是完全不会给你考虑这些数据流原来无法展开到100个NP上的。最后这个团队会是个什么结果?这个项目是个什么下场?
备注
其实我告诉你吧,这个项目不会有什么下场,因为允许立项的领导为了不背锅,不会提起这件事,做这个项目的工程师会说自己很辛苦,996,老婆生日都没有来得及去参加。
但作为一个诚实的人,这个项目失败,它就是失败了,你骗不了自己。
所以,我们说我们做高层设计,就是像建房子一样,夯实每一层,然后我们才去建下一层。这种情况下,当我们质疑你这层逻辑的时候,你不能告诉我你在细节上的机巧。你必须用粗糙的高层逻辑去解决高层逻辑的问题。
比如你的网卡插在一个NUMA Node上,我质疑你的流量转发到另一个NUMA的CPU上处理太慢了,你要明确告诉我你是打算让用户绑定NUMA Node使用,或者你做大缓冲区通过流水线弥补这个性能问题,又或者你建模证明就算路由到其他NUMA节点,你的性能仍满足要求。你不能告诉我在三个章节以后,在某个角落里,你的NUMA节点其实不是个NUMA节点,而是另外还连了一根线到另外的CPU上。这他么都改变高层设计了,你的高层设计是个摆设吗?
细节设计是对高层设计的逻辑收缩,不是改变。高层设计只是粗,或者是留下明显的桩,让细节设计去补充或者去改变,它也是有确切的因果逻辑的。不是随口口爽的。它会说,A和B之间有消息通讯,而不说,A和B之间的消息通讯按ASN.1编码,但不表示你在细节中可以变成A和B必须互相函数调用。
我们说的夯实工作,其实主要就是建不同的模型去挑战你的逻辑,保证它遇到各种变化都能顶住,这些模型唯一不包括的就是“完成所有的细节设计”。因为架构设计是个“逻辑闭包”的问题,我们就是因为整个系统细节太多,所以不能一次看所有的细节。如果你的逻辑线包含一万个条件,才能证明那个结论,这个证据等于没有。因为谁都不知道你是不是对的。
所以,不要用细节来轰炸质疑你高层架构的人,如果这一层必须用细节才能证明它成立,那这一层已经不存在了,直接变成空中的那一层了。
3.281.1.2. 构造高层抽象的逻辑闭包
高层抽象的逻辑闭包和很多下层设计的逻辑闭包有一定的区别。和所有逻辑闭包一样,高层抽象也可以从问题开始考虑。你做一张网卡,要求是把数据从网络上转发出去或者收到CPU上,基于这个要求我们可以分n个模块,那谁谁负责收发,那谁谁负责Phy管理,那谁谁负责五元组过滤……这是一般的方法。
但这样做高层架构是不够的,因为做功能太容易了,关键在于有些很tough的细节能力你怎么做到,毕竟我们的高层抽象是要承载很多细节要求的,而这个基本功能,实在不够tough,用它支撑第一把模块分解,实在不够看。
所以,我们通常是用最难的几个细节需求去驱动基础框架的设计。比如我们做手机的同学就喜欢用主屏60Hz刷新频率作为第一级建模的基础,为了让这个主频可以在60Hz的速度更改,加上屏幕的分辨率,就可以计算总线的基础带宽要求,只要这种场景能搞定,大部分场景就都能搞定了,这样其他的细节再慢慢补,看看还能否挑战它的破绽,再把这些破绽一点点补起来就行了。
又比如做这个中断控制器,我们有人会用虚拟机迁移功能作为最难的点去建第一个模型。虚拟机迁移的时候,报给它的所有中断都需要被转向到新的CPU上,原来报了一半的中断如何灭掉?迁移了一半的VM怎么收新的中断?要不要缓存?缓存在哪里?Inject到VM中软中断如何随着VM迁移?这些问题能解决,需要加什么数据结构搞清楚了,其他问题就好办了。
说到底,房子的地基是要承载整个房子的,你不知道以后房子4楼会不会放一瓶郁金香,但你肯定要考虑清楚你得架得住4层的房子。