.. Kenneth Lee 版权所有 2018-2020
:Authors: Kenneth Lee :Version: 1.0
从香农熵谈设计文档写作
我在架构师的岗位上干了十几年,这里有一个Everyday的工作是评审设计工程师的设计文 档,虽然我一直呆在同一个单位,但也接触了来自不同其他组织和国家的工程师写的设计 文档,在我的经验中,文档写得烂是个大概率事件,而且,感觉商业公司比开源社区的设 计写得更烂,国内背景的工程师比欧美背景的工程师写得更烂。所以,我一直尝试找方法 说明一个设计文档应该怎么写,如果读者看我这个专栏,就会发现我尝试过用所谓的守弱 ,逻辑链,立体代码,设计和科普的区别等一系列概念来说明这个问题。但感觉离关键的 问题还是有点远。最近在一些设计讨论中,我开始引入“香农熵”的概念,我自己感觉这个 效果要好一些,所以在这里把这个逻辑表述一下。
首先理解一下香农熵的概念,我自己是在学习神经网络的时候接触这个概念的,在实践上 ,它被用于梯度下降的时候比较两次收敛的效果从而得到Loss值的。但香农熵本身是一个 更为普适的概念。
假设你要决定明天干什么,只有出门,在家两个选择,我给你提供一个信息:“选择出门” ,这里就为你提供了一个“信息”。假定我是上帝,我给你的信息是对的,如果你选择在家 ,你就会死于地震。这个信息对你的价值,我们称为a。
好了,同一个问题,假设你有3个选择:出门,在家,坐在门口。我还是给你提供一样的信 息:“选择出门”。这个信息也对你有价值,这个价值我们称为b。
很明显,b的价值更大,因为你没有给我提供a,我选错的可能性是50%,但你没有给我提供 b,我选错的可能性就是2/3了。
如果我给你提供的选择是:“选择出门,在家,坐门口都挺有道理”。这个信息,如果从“你 应该做什么选择”这个主题的角度,信息量为0。
香农熵,本质是用数学方法来定义这个信息量的方法,它是所关注的随机变量的数学期望 规整后的结果。我们这里不用那个结果,我们这里只取a,b,0这个概念。
也就是说,我们取其中的这个信息:
如果我们就一个决策来讨论问题,对决策产生影响的信息,在这个决策上,具有香农熵。 香农熵决定了这些信息价值的大小。如果某个信息对决策没有影响,则其香农熵为0。
我在知乎上经常用“神棍”这个词,如果用香农熵来理解,我们很容易定义什么是神棍:提 供大量香农熵为0的信息帮助你决策的人,就是神棍。现实中那些一般意义的装神弄鬼的, 买保健品的,算命的,传教的,自称国学的……大部分都是玩这个技巧的。而在企业中,被“ 赶鸭子上架”的工程师,大部分也是玩这个技巧的。所以我个人是非常反感“过程绩效主义” 的,因为这种手法导致我经常要看垃圾设计文档,这种文档洋洋洒洒几十上百页,等你看 完了,你才注意到,他么的一个有用的信息都没有。你的时间本身就不多,每天浪费在这 种文档上,你不想杀他就有鬼了。以前看《明朝的那些事》,说朱元璋看大臣送上来的报 告,写了一大堆,一点实际的东西都没有,他就要把那人拉上来打,我是很能理解那种心 情的。
设计必须在讨论的主题上给出选择,它的香农熵才不等于0,它才有实际的价值。说“正确 的话”本身不是!
所以,不要来问我,“难道我说得不对吗?”,我不关心你说得对不对,我关心你的香农熵 是否为0。我问你晚上到哪去吃饭,你给我回答“饭是一种碳水化合物”?然后问我“难道这 句话有错吗?”,你说你是不是欠揍?
对于设计文档,我们一般关心的东西是“你如何选择,从而达成需求上要求的目的”,在这 个主题上,我们一般会需要如下有效的信息:
你所认可的目标是什么?
你如何分解多个功能、角度和步骤,逐步实现所述的目的
这些功能和步骤之间的关系细节,特别是逻辑关系无法在下一层设计中体现,只能成为 “原则”的那些关系细节。
第一点不用解释,但我还是要提出来,因为很多人认为它显而易见,所以就不提了,但其 实如果你从“信息有用”这个角度来看这个问题,你会发现很多人写的还是废话的。比如:
| 本文描述XXXX设计方案,用于指导后续设计以及编码开发工作。
| XXXX可以支持PCIe的性能调优和维测,为系统维护人员提供强大的调试功能,
| 为增强YYYY设备的软件生态提供有力的支持
| ……
这种鬼话哪里是什么“目标”啊,这根本就是“广告”。看了这个东西你能知道具体要设计什 么?
有用的信息都不需要好看和高大上,而是聚焦到你要干什么,比如我们的目标可能是这样 的:
| 硬件团队预计在YYYY V2设备上实现了一个可以收集、统计、
| 调整PCIe桥和RC/RP的的设备,称为PCIeTuner,
| 本文讨论把该设备暴露为用户可见的功能的软件方案。
|
| PCIeTuner和PCIe总线Topo的关系如下:
| ……
| 其中a接口仍在讨论中,未确定。b接口认为只有可能为两种方案:
| PCIE endpoint,或者RC设备IO空间中的一个Region
| ……
这才是你要面对的确切的问题。要求你写个“目标”,你就说写大而无当的话来“填空”,这 香农熵还是0啊。
所谓空话,还有一种常见的情形,就是名字指向不明确。比如你谈开发视图,说你要开发 一个SDK,然后开始SDK如何如何。问题是什么是SDK呢?OS你也算SDK,编译器你也算SDK, 你的库你的SDK,用户开发的部分东西你也算SDK……这不算啥,可以,但在另一些上下文里 面,你的SDK又不算OS了,只算OS的某些驱动,这样说了半天,其实只是“看着有理”,“看 着有信息”,其实什么都没有写。
不完整也是这种情况的一种特例,你说你的SDK,用户驱动,配置脚本可以满足需求。但其 实你说的东西根本就不能满足需求,还要加上BIOS才能满足需求。这种东西,也不包含香 农熵,因为缺失的信息(特别是听众不知道它缺失的时候),这个信息根本就“不对”了。 就好比医生跟你说,“这个病要吃药”,你以为他提供信息给你了,结果他的意思是:“这个 病要吃药,吃药了有50%的机会活,不吃有50%的机会死”,那他的信息有什么用啊?
这种问题无处不在,不仅仅出现在第一点上。
第二点和第三点是一个问题,我们一起讨论。我们先理解一下第三点的概念:
什么是“逻辑关系无法在下一层设计中体现”?——我们用最简单的“设计-编码”这样的分层关 系来理解,比如你做一个RoCE的驱动,这个驱动必然和你的网卡驱动发生关系(如果你复 用相关硬件的话),然后你要用起来,你还需要增加用户态OFED驱动,然后你可能还需要 调整OpenMPI的一些参数,才能保证你的业务流的性能……这些逻辑,在你具体写某个内核模 块或者用户模块的时候,逻辑链已经断了,你没有了一个完整的数据流是怎么工作的上下 文了,这种东西,就必须设计。否则下一层就会丢掉这个信息。
我们还有更多这样的东西,比如:
模块之间传递信息的充要性:比如你在OpenMPI层提供新的用户接口,你要求哪些参数?这 些参数是否足够让中间的模块拿到足够的参数来完成需要的功能?
模块之间的依赖关系:比如你打开了一个RoCE的QP,你就必须锁住网卡驱动(get_module) ,这个,在你写RoCE驱动的时候你就不可能考虑,因为那个逻辑上下文中,在某个流程中 突然调一下get_module()是莫名其妙的。这个get_module的必要性是在讨论模块间的依赖 关系的时候暴露出来的,不是在你“如何打开一个文件,填充其fd句柄”这个逻辑中暴露出 来的。
锁 你有多个模块,多个资源,不同的锁,这些锁经过多个模块,可以从不同的接口 进来,是否会产生互锁的情况?是否会导致主业务流的多个线程被同一个 critical区限流的情况?
状态机 软件和硬件的内部状态切换,是否会导致系统进入不可恢复的状态?是否有可能 让系统的状态不可判断?提出功能的时候,系统是否有可能处于不正常的工作状 态?
……
这种,都是编码阶段无法或者难以判断的决策,它们就是设计阶段必须提供的信息。缺了 这部分信息,编码就很难可靠,等大量的细节逻辑补进去以后(比如cache效率不高,加了 preload。插入一个初始化到init和start之间保证硬件和用户程序的同步等),这些问题 解决起来的成本就非常高。这种决策的信息香农熵,就很高了。
说到底,我这里要强调的是:
不要重复代码的逻辑到设计文档中,因为这两者提供了一样的香农熵,加起来等于0,还增 加了维护成本。(这种问题通常发生在要“让设计文档正确”,而不是“让设计文档有用”的 人身上。要找到对产品功能和质量很重要,但代码无法表达的逻辑在设计文档中单独设计 。
所以,其实我们需要在设计文档阶段具体增加什么逻辑,完全取决于你代码走的逻辑是什 么,有那些逻辑是代码无法表述,由必须保证“逻辑合理”的。
如果你没有思路,那就可以从4+1视图来进入思考,因为通常4+1视图的逻辑都是编码阶段
建不出逻辑的。比如,对于简单一点的模块,首先应该考虑开发视图::doc:开发视图
。因为对于简单模块,你首先应该关心的而是你改的是哪个版本的那段代码,有几个场景
,对吧?这个东西编码的时候是考虑不清的。你调通你实验室那个软件,不见得可以运行
在客户的机器上。更复杂一点的系统,比如上面提到的那个PCIETuner,你会涉及到你怎么
定义一个PCIE Topo,你的硬件如何连在这个Topo上,哪些Switch是被你监控的Switch,那
些是你和主机通讯要用的Switch。Tuner什么时候指硬件,什么时候指你的用户态的App,
什么时候又是你的sysfs控制接口?这就需要逻辑视图去表述。这个东西到了编码的时候也
是说不清楚的……
这个不能告诉你有什么固定的套路,如果告诉你固定的套路,你又变成“填空”,最后又写 成一个没有香农熵的垃圾了。
我们对一个设计(或者说设计文档)进行评判,分两个部分,第一个部分是信息是否具有 香农熵,这个判断本身包括两个方面:
第一:讨论目的是否是我们的目的。所以,我们一般会在设计文档中复述我们要实现的功 能。比如,我们会说:“我们的RoCE驱动必须支持Memory Window原语,包括……”
第二:这个信息是否支持这个目的(是否具有香农熵)。比如,我们需要说:“因为我们的 RoCE驱动支持Memory Window原语,所以,驱动中必须具有alloc_wm……”
第二部分是这个信息是否正确。所谓正确也包括两个方面:
第一:这个信息是否违背事实。比如,“我将通过gup(get_user_page)锁住内存页进行 DMA操作”,这种情况,如果这个过程中发生fork,这个页面将因为COW(Copy-on-Write) 而丢失,导致这个DMA操作失败。一般来说,我们同行评审,主要是发现这种类型的错误。
第二:这个信息是否具有充要性。比如,“我在ioctl的时候将使用读写锁保护对qp(RoCE 的概念,Queue Pair,一个通讯通道)进行保护”,这个地方,是否只有ioctl这一个入口 ?如果不是,这个设计不具有充要性,这个信息丧失其价值。
很多时候,有理智的人和别人讨论问题,或者一个称职的工程师和别人讨论设计,是在预 期进行第二部分的讨论。但我们常常碰到神棍和为了证明“我还能干这个工作”的工程师, 然后我们永远就停在第一个问题上了。
对于一般的神棍,我们没有什么办法,心里有气,能揍他就揍他,不能揍他(通常都不能 :)),就躲远点吧。对于工程师,我做个简单的思想工作:我们常常希望掩盖自己的无 知而把自己真正的有知覆盖了,这是一个失败的战略,我希望你可以认清这一点。我们很 多工程师太小看自己的能力了,在一些无谓的地方(通过熵为0的信息)表演自己的高大上 ,本质是,是因为你认为你真有能力的地方“不值一提”。但你要注意到,能让你去干这个 事情,必然是你有不可取代的能力,可能这个能力不是毁天灭地,不可取代,但如果你可 以取代,当初为什么要让你去干?
所以,“我这也不知道,我那也不知道”不要紧,要紧的是,你把你你不可取代的部分给他 干好了。我不知道gup在COW的时候会失效不重要,重要的是我现在把没有COW的场景给他做 了。我的逻辑还是我原来逻辑,在这个特定的场景下还是有用。我不懂文件open的时候不 需要get_module不重要,重要的是我在这个上面有逻辑,我就有可能在后面的推演中改变 它。
当然,这仍是个稽式,也许你的领导,你的客户就是这样的傻逼,你需要用神棍学忽悠他 。但当你忽悠别人的时候,不要忘了,你别连自己一起忽悠了,否则你就真的成了神棍, 只能混神棍的日子,而不是工程师的日子了。
补充1-20181007:讨论中不少读者都认为“逻辑链”的概念已经把这个问题讨论清楚了,没 有必要引入香农熵的概念。我个人觉得,这是因为你还没有引用这个概念,或者没有尝试 去面对把你的概念推给你的团队这么一个过程。
我之所以要在一个独立的博客中把设计的逻辑抽出来,是因为一个独立的逻辑你看到的“名 ”,都是和这个逻辑相关的,你很容易就接受它了。但如果这个名在一件具体的事里面,你 就不一定能守稳它了。
在具体的设计中,你有大量的其他逻辑,比如工期不足,能力不足(比如你并不确定你的 某个假设是否成立),需求不确定(比如高级领导,基层领导,客户一线和客户领导的意 见就不一致),你自己的考评压力,声誉压力(同行觉得“他不行”)等等。这些技术的, 非技术的要素在左右着你,让你拿不稳你到底以哪个为主。
守弱,逻辑链等观点,最大的问题是缺乏清晰的判断边界,相对来说,都是一种“守”的态 度,这对工程师的要求很高,得神智很清明才能做出正确的判断,更难以进行有逻辑的思 考。而香农熵的概念就相对简单一些,它有比较容易“机械化”的一面:你的目的是什么, 你的诉述是否对这个目的有利,这个比较死板,所以它就更容易推广一些。