首页  ·  知识 ·  架构设计
从用户功能开始构架系统框架
in nek  知乎  综合  编辑:Randolph   图片来源:网络
一个软件项目怎么样才会牛逼?良好的架构?优秀的算法?优雅的代码组织?不是的,一个软件项目牛逼是因为它有用,并且把它的竞争对手都逼死了,它就牛逼了。

研究一个项目是否有前途,第一研究它是否必须的,第二研究它如何保证它相对别人的优势。这是要守(关于“守”的概念,参考这里)的第一级要素。比如用OpenSSL加密数据,用Libz压缩数据,这是个刚性要求,你做了一个这样的库,它就具有生存的基础。但实现一个优雅的,计算把《道德经》进行SHA-1加密的库,无论做成什么样子,它都不会牛逼起来。

所以,牛逼,就是有需求驱动你(有人,足够的人,要用你),你比别人好,你就没有办法不牛逼。所以,剩下的问题就是怎么控制你的设计比别人有竞争力。在架构设计上,保证竞争力,很大程度上是如何对资源进行控制。

其实,不但开源项目需要制造真空,所有的架构设计都是在制造真空。作为架构师,你经常需要体验那种从一两个人开始项目,然后慢慢扩展到几十到上百人的规模的经验。我在这个过程中的体会是,你觉得你一开始有满满的自由度,实际上很快你就要面对激烈的冲刷。人力规模一上来,你代码都看不过来,你前期的所有准备,都是怎么防住这些冲击。如果你没有很好的控制逻辑,这些快速增加的代码量,会很轻松冲跨你当初简单的设计,让你的理想落空。而缺乏控制的洪水,是形成不了力量的,这样的项目就不会牛逼。

代码不来自架构师,代码来自资源投入,大家都在拼资源,为什么你的资源比别人高效?那就是你的资源都被用到刀刃上了。 

所以,架构师的时间是很紧迫的,在前期人少时的架构设计不上心,后面死无葬身之地。而且,真的就和我前面说的,大部分人死都不知道自己怎么死的,还在归咎于资源不到位啦,工程师编程水平不行啦,合作伙伴不肯合作啦……作为一个集体,你要批评谁,总能找到理由的,但失败是真真切切的。 

因此,构架设计是当你只有一两个人的时候,你就要花所有的精力来构筑水道,保证洪水进来的时候你能够控制这些资源都转化为你的竞争力。

我们还是拿前面这个OpenSSL和Libz为例,假设你要给这样的库做一个加速器的框架,你首先控制什么?

显然不是你的硬件做成什么样子,那个不是你这个框架生存的原因,你的框架生存的原因是能让libz和OpenSSL跑得更快(注:由于这两个东西的概念可以互相借用,后面暂时先用比较简单的libz来推演),不是因为你有一个加速器硬件。

所以,控制这个框架应该做成什么样子,守的是libz这个库的使用模式,然后才是你硬件的限制。所以,你的接口最优雅的方式应该是这样的:

acce_compress(algorithm, input_buffer, output_buffer)

但这样从libz库来说就不好用,因为压缩算法就有很多种,而且每个算法有参数,更合适的做法是这样的:

init_algorithm(algorithm, parameter);acce_compress(algorigthm, input_buffer, output_buffer);

这是原始需求,也就是前面说的“有用”,这里不考虑任何实现的困难,仅从“有用”这个角度来说,设计成这样才是合理的。这是我们要守的第一层,任何超越这一层范围的,都需要有足够的理由(证明对手不会有可能因为这点超过我)

很多人上来就开始考虑设备管理,考虑打开设备。但这些是错误的方向,设备管理是你硬件本身的事情,不是我libz引入的事情,我可没有要求你有“设备”啊,我只是要压缩,你设备打不打开关我鸟事?

我们从libz这个角度守,就能尽量降低我们自行引入的,破坏最优竞争力的要素,这样我们才不会被后续增加的复杂度左右了方向。所有破坏最优竞争力的要素,就必须有合理的理由,并且保证其他对手找不到更好的手段来超越我。

我们下面来推演一下几个不得不引入的几个破坏性要素:

1. 设备数量限制:如果通过加速器加速,加速设备不足怎么办?

2. 数据传递成本:把数据送入硬件,再从硬件中送出来如何处理

3. 系统调用成本

首先考虑数据传递成本,前面列出的4个破坏性要素中,2,3都是传递成本。这个成本是用硬件加速特有的,如果用CPU来实现这个算法,根本没有这个问题。用硬件加速要赢CPU加速,首先要解决这个问题。我用如下方法来保证这个问题可以被解决:

a. 压缩算法本身不通过系统调用来对硬件发起请求。让acce_compress从用户态直接访问硬件。这样就降低系统调用的成本了

b. 做一个分界线,小buffer压缩仍使用CPU算法,不通过硬件加速

c. buffer不直接放在设备上,而是放在内存中,让硬件直接操作内存来实现对数据的处理

策略b和整个方案没有关系,我们可以用一个highlevel_acce_compress()来封装这个功能,所以暂时可以在推演中忽略它。策略a和c却会带来新的问题。对于策略a,它又带来两个问题:

i. 要求用户态认知硬件,对硬件或者软件的升级带来挑战(硬件升级,用户态的库也要升级)。这个看来无解,只能尽量让硬件统一,还有就是把输入和Buffer分开,减少被迫需要拷贝的可能(也就是在buffer上用两片内存,metadata用一片,数据用另一片,这样,主数据流的数据就有自由度可以放在任何地方,包括放在用户程序的堆或者栈中)

ii. 用户态认知硬件接口,容易带来安全性问题。这个的解决思路是:硬件不依赖用户态提供数据的安全性,只保证对传进来的数据进行处理,而这些数据都在一个进程之内,这个安全性不比你用CPU压缩差,因为你基于CPU的库也可能有bug,弄死一个进程,我只要保证用于加速的硬件死不了,弄死进程并没有带来额外的安全问题。

策略c也带来一个问题:要让设备操作内存,就要处理DMA,要做DMA就需要把内存pin住。所以这可以有两个解决方案,第一个方案是用SMV,也就是从设备一侧发起缺页:你扔一个虚拟地址给加速器,加速器工作的时候如果缺页,请求发回给CPU,CPU把这个缺失的页加载回内存,然后加速器继续工作。但这个方案对总线系统要求很高,我到现在只看过片内设备做成功的,没有见过通过PCIE接口做成功的,所以,完全寄望于这个似乎不是个好的选择。

第二种方法是主动pin住这个内存,Linux中可以用gup(get_user_page)来实现,但这个东西的限制是只能在一次系统调用中有效,不适合我们的情况,要加上VM_PIN才有可能成功,而后者还进不了主线内核。为了让我们的框架可行,我们得拼一把,把这个障碍突破了。但即使如此,我们还是不得不更改了对外接口了:

init_algorithm(algorithm, parameter);acce_pin_memory(algorithm, buffer);acce_compress(algorigthm, input_buffer, output_buffer);acce_unpin_memory(algorithm, buffer);

从这个例子中,我们就可以看到了:只要你开始进入细节,你就不能不引入破坏当初”最优设定“的循环。但有最初设定和没有最初设定,我们眼中看到的设计是不同的,上面的4个函数,有两个属于最初设定,另两个是我们的包袱。如果我们看到了包袱,而不觉得它是高大上的设计。我们就能随时看到竞争对手的优势,保证在面对各种挑战和细节问题的时候,不会离开主航道。这里其实还有不少细节可以抠,比如,有没有必要让acce_pin_memory带algorithm参数?不带这个参数,这个功能就可以独立于加速器框架存在。带这个参数,我们就有机会在提供算法的硬件支持SVE的时候自动把这个操作变成空操作。这也是权衡。这也说明了,为什么构架设计是必须的,这种参数,选和不选都是一种选,选了,后面的设计一旦加进来,就再也改不动了。你后面补工程师,增加算法,增加加速引擎,增加调试功能,优化速度……所有这些算法都补上来的时候,如果你没有守的东西,你的构架就成了一个大杂烩,那时你想改什么都是句废话了。

解决了数据传递问题,硬件数量不足问题也就好解决了:既然我们有highlevel_acce_compress,我们完全可以在这里封装这个差异,当申请不到设备的时候,我们退化为CPU算法就好了。最坏最坏,我们也不会比现有方案差,只要保证硬件处理过程是有竞争力的,这个框架就可以生存下去了。

从这个过程我们可以看到,构架设计的过程,就好像修补一张网。一开始你有满满的自由度,只是找到几个固定的点,然后你不断填补逻辑,越到后面,你的余地就越少。等到你几乎没有余地了,这个时候就是你的系统最强的时候(因为所有的逻辑都修补完了),也是它最弱的时候(因为已经没有面对变化的能力了)。所以,如果你可以看得长远,其实构架设计一开始就没有什么余地,因为在特定的用户需求之下,最优设计也就那么几种。


本文作者:in nek 来源:知乎
CIO之家 www.ciozj.com 微信公众号:imciow
    >>频道首页  >>网站首页   纠错  >>投诉
版权声明:CIO之家尊重行业规范,每篇文章都注明有明确的作者和来源;CIO之家的原创文章,请转载时务必注明文章作者和来源;
延伸阅读
也许感兴趣的
我们推荐的
主题最新
看看其它的