.. Kenneth Lee 版权所有 2020
:Authors: Kenneth Lee :Version: 1.0
处理视图
好吧,既然已经说了两个视图了:
:doc:`开发视图`
:doc:`概念视图`
我们就接着往下说吧,我们再谈谈处理视图(Process View)。看了前面两个视图的介绍 ,读者应该已经注意到了,所谓4+1视图,其实就是4个不同的设计角度的逻辑,开发视图 是写成的那个程序,概念视图是使用的那个逻辑,部署视图是运行的方法,处理视图我们 一会儿再说。我们写的程序是平的,一句一句的,逻辑都组合到一起了,部分逻辑链的独 立信息被丢失了(好比你在白纸上盖戳,只盖一个你是看得见是什么的,如果盖多个,部 分信息就丢失了,至少不明显了),比如只看到你的receive_pkg()函数里面写了一个日志 ,你就没法知道“用户是否可以跟踪关键消息的时延”这个逻辑,你得单独找好几个时延跟 踪点,单独看它们,你才知道你有足够的数据可以看到时延。所以你只有代码是不行的。 你需要其他平面建独立的逻辑链。4大视图并不是所有的视角(及其逻辑链),但确实是非 常重要和基本的视角。而Use Case是相对支离破碎的用户使用场景,我们把这些支离破碎 的场景组合起来,映射为4大视图中的行为,就大概规范住了一个系统的设计方向,就可以 比较放心地进入下一层设计逻辑的细节了。
Process View,是讨论“处理过程”的视图。有人把它翻译成“进程视图”(Process在某些场 合也是操作系统“进程”的意思),这显然翻译错了。但它确实和“进程”有那么一丁点关系 。
处理视图讨论的是逻辑系统的“驱动”问题。所有程序,包括硬化的程序(比如一个PCIE的 Bridge),都是一个逻辑系统,是有一定的先后关系的。先把a加上b,然后把结果做一个 开方,然后取反写到d上……这总是有一个先后关系的,这才是一个逻辑系统。
但单纯的逻辑并不会“动”,就好比汽车轮子本身可以动,但没有引擎,它不会动。
逻辑是一样的,要让逻辑运动起来,你要一个动力源。比如你的头脑。一个连续的逻辑, 需要一个连续的动力源。如果有两个动力源,两者就会有一个匹配的关系。类比汽车,如 果四个轮子有4个不同速度的引擎,你非翻车不可。对于前面提到的逻辑问题,如果我们用 两个头脑来算a加上b和开方取反这两个动作,双方有没有协商,这个计算就乱了。
处理视图解决的就是这么个关系,它分析的是你有多少个头脑,这些头脑怎么配合完成需 要的计算。这个问题,编模块的时候想不清楚,概念视图,开发视图等也考虑不到。但你 做这些设计都受这个打算的影响。所以它很重要,缺乏经验的工程师甚至不知道该怎么入 题。
所以我见过有人是每个模块自动创建一个线程和一个队列,主线程启动所有这些线程,负 责输入的模块(比如从网络上读数据,从磁盘读数据等),读了数据往下一个模块的队列 里丢,这样就构成整个系统了。
这是一种典型的把两个逻辑Mess到一起了。怎么分模块,这很大程序上是概念视图和开发 视图的事情。而怎么决定有多少线程,这是业务压力的问题。把两个逻辑联动在一起,系 统就失去解决问题的活性了,动了这里那里就跟着动了,这怎么都做不好。
如果我们从IO拿数据,然后处理这些数据,然后扔到输出上。这是个串行的逻辑。从处理 的角度,你就只能容得下一个“动力源”,多了不会让逻辑更快。你放三个函数(可以分属 三个模块),调完一个调下一个,这只有函数调用的成本。你放三个线程,每个线程都有 时间片切换成本(线程多了会进一步恶化),线程间有同步成本(这还会造成更大的时延 和确切性下降)。这变成自己给自己找麻烦了。
在处理视图的设计上,我们一般认为函数调用是“硬(同步)关联”,用这种关联连接在一 起的一段逻辑,只能容得下一个引擎,这组逻辑和它们所属的引擎系统,我们称为一个“动 力单元”。而“队列”,是“软(异步)关联”,它可以匹配两个“动力单元”的速率不同步问题 。每个动力单元,都被需求所绑定,我们设计的时候仍使用“不为天下先”的策略,不到迫 不得已,不去增加动力单元。
请注意,动力单元不是模块,它和你的软件模块是交叉的关系。
什么时候我们值得增加动力单元呢?——两种情况:第一种是你可以,而且有必要分开你的 动力源的时候。比如,你从IO拿到10组独立的数据,每组都要花很长才能完成,而你有超 过一个CPU,这好办,你有多少个CPU,就分开多少份,每份负责一组,然后最后聚合回写 就好了。或者你同时还有些维护工作要做,独立放一个线程,和你的主逻辑链分开,这样 就可以了。
这些例子都满足前面说的被现实驱动的条件:
你可以:你的数据是互相独立的,逻辑上本身就没有硬关联
你有必要:你有多个CPU,你要利用他们的能力。或者你的逻辑链基本上不和别人重叠。复 用驱动源反而增加逻辑。
但我们必须清楚,增加动力单元增加成本(比如你最后聚合会需要重新排队和调度),更 多时候,我们看到的问题是,因为线程的调度逻辑在你控制之外,你会调来调去都调不好 ,加高一个线程的优先级,反而导致它得不到调度的事情很容易发生的。所以,如果你知 道你要先做什么,后做什么,就好好用独立的动力单元来干,不要依赖操作系统的调度器 。操作系统的调度根本不知道你要干什么,你肯定是干不过你的。
另一种情况,你的计算属性不一样,比如我见过一种产品,包含有很多向量计算,他们就 把标量计算集中的算法归结为一个线程,向量计算集中的算法归结到另一个线程,然后分 别聚合到不同的CPU/GPU的线程中执行,这也会产生多个不同的动力单元。
所以,其实处理视图也没有什么特别的,用到的UML工具和其他视图(比如概念视图)也没 有什么两样。你其实就是要定义:
你有哪些动力源(或者动力模块)。(注意,和其他视图一样,你可以抽象的。)模块如 何被动力源所驱动。动力源之间同步的方法是什么。
我们值得注意的是,动力源可以非常广泛的,线程,进程,服务,服务器, Signal/Interrupt/SoftIRQ/Tasklet Handler,Task,Timer。都可以是动力源,只是他们 的成本,行为,同步方法不一样而已。这就完全看你对系统的认识程度了。
最后给个例子吧,比如前面这个IO的例子,光作为图示例子我们可能可以这样建模:
当然,正如我们一贯说的,图不重要,重要的是你的逻辑是什么,不要指望就这幅图就叫“ 处理视图”了。
实际设计中我看到的情况常常更多是(故意的)忘掉该有这个视图,特别是在增加新功能 的时候。因为这又是个脏问题。比如在前面这个例子里,可能你本来好好的,现在发现呢 ,系统要关闭部分节点,已有的任务要迁移到其他还活着的节点上。好了,这种就是典型 的商业级脏问题,因为它涉及到已经完成的任务怎么办,完成了一半的任务怎么办,IO读 出来了,但还没有投入运行的任务怎么办。哪些可以拿走,哪些不能拿走,什么时候可以 叫停这些“边缘”问题。某些工程师就开始假装不知道了,简单把数据一拷贝,任务在另一 个节点上重新启动一下,测试一把——刚好没事儿!搞定!
好吧,每次说到最后都说这个也没有意思……
上面说的是架构或者系统一级的情况。在模块一级,我们常常面对的是另一个问题:系统 的处理模型已经定了,我们怎么给我们的模块加功能。比如我平时见的最多的Linux驱动的 模型,你准备增加一个系统调用,修改一下你的驱动配置。这同样需要处理视图来理清你 的线程模型:
系统调用是线程上下文,这也意味这如果多个线程同时来访问,你有可能有互斥的问题。 如果你还有内核线程和中断上下文在起作用,那么这个问题要考虑的就更多了。但仅仅考 虑互斥问题是不够的,因为这里还有一个性能问题,如果你的互斥部分太大,阿姆达的原 理就开始起作用了。你需要想办法把执行流Hash开。需要把执行流Hash开,你就开始需要 Session,Context这种概念了。但一但你引入这种概念,你就会引入相关的动态数据结构 ,然后你就面对这个数据结构的释放问题。要解决这个问题,你就会需要非常小心地设计 这个“什么时候停止线程,什么时候停止业务,什么时候释放资源”的问题。然后使用计数 ,mutex,spinlock这些问题就都起来了。
这里不是要重写一次Linux Locking Book,这里是要提醒你:处理视图可以很复杂,但没 有这个设计,你那些拷贝到设计文档中的代码再多,你的处理视图逻辑都是没有设计的。 如果你开始修改一个这样的模块,而你还没有一个上级设计给你的处理视图模型,你就要 自己还原这个模型,然后讨论你加入的功能是否是可行的。否则设计就是不完整和有漏洞 的。
关键是,一旦模块中加入了你这样的设计,原来的逻辑就无法还原了,后面你再问我:为 什么偶尔会死机呢?怎么修呢?——它么我怎么知道?你已经成了一团乱麻了,我又不是神 仙,怎么知道破镜怎么重圆啊?泼水怎么回回收啊?你还不如重新买一个镜子,重装一盘 水呢。
最后补充两句,本文是这个系列的最后一篇:
:doc:`开发视图`
:doc:`概念视图`
整个系列都是为这个文档服务的:
:doc:`从香农熵谈设计文档写作`
同时,是的,不会有《部署视图》或者《用例视图》这些东西了,如果你看了这三篇你还 觉得需要说说《部署视图》,那么,就算我写了,你也一样听不进去。
这也许算是“大成若缺”的一个例子?