.. Kenneth Lee 版权所有 2020
:Authors: Kenneth Lee :Version: 1.0
概念空间建模要领
概念空间表面很容易,其实是整个建模中最难的部分,没有经验的架构师很容易把抽象变 成具象,导致整个建模失去作用。
本文用Linux内核主线的(作者写文档的时候用的版本是5.9)HNAE3框架作为例子说明这个 建模的要领。
HNAE是Hisilicon Network Accelerator Engine的缩写,3是第三个版本。这个驱动框架用 于驱动海思鲲鹏系列处理器的网络IP(Intelligent Property)。这个网络IP当前主要提 供两种功能:
多种速率的Ethernet(下文简称enet)网络功能
RoCE(下文简称roce)互联功能
这两个功能有不同的通道,你可以同时使用两个功能,但实现的硬件是同一个PCIe硬件设 备。我们预期未来可能还会提供更多的其他网络功能,为了让这些功能相对独立,HNAE框 架负责把这些功能进行抽象分离。它提出了三个关键概念:
Client:这表示一种网络功能,比如enet或者roce
AE(Accelerator Engine),这表示一个硬件,无论它是PCIe设备,平台设备还是其他 设备,也无论它是PF(Physical Function)还是VF(Virtual Function)。
Algo(算法),这表示一个独立的处理特定功能的算法,比如一种复位VF的方法,一种 设置MAC地址的方法等。
逻辑上,我们希望:
| 任何一种设备,无论它本身是PCIe的,平台的,还是其他硬件创建的,
| 都按那个子框架的要求初始化,然后按AE的接口要求,注册给HNAE。
| HNAE根据这个接口匹配自己的Client驱动,找到符合的Client,
| 通过Client把这个功能暴露给enet或者roce这样的平台。
|
| Algo抽象一组具体的行为,AE给Client提供的标准访问接口。
这个逻辑,用UML类图表达,就是这样的:
.. figure:: _static/hnae架构.svg
好,现在我们来解释一下什么是概念空间。
上面我们介绍HNAE架构的时候,解释的Client、AE、Algo是什么意思,它们之间是什么关 系,定义它们的目的是什么,以及后面的类图,都是概念空间的定义。
我没法用严格的语言定义它必须是什么,因为它是我们的感性认识和形式化描述之间的接
口,是我们人脑的恍惚(\ :doc:../道德经直译/恍惚
\ )的名字化(形式化)的过程。
我们在尝试把我们不那么清晰的思考变得清晰,让我们可以像处理代码那样处理它。但这
里更重要的不是我们的描述更加代码化,而是代码化的描述确实捕获了我们的目的。否则
这个建模就没有意义了。
所以,概念空间建模重要的不是那幅UML图(这是新手经常犯的形式主义错误,他们希望画 对图,而不是做对设计),而是你怎么定义这些概念,图只是辅助这些概念的理解。
而且,请特别注意这幅图和开发视图画的类图有什么区别。这里建模的是概念,而不是模 块关系。如果是模块,Client是一个.c文件,它和AE这个.c文件的关系是*-*的关系。它 可以是hns3_client.c和hns4_client.c,但我们这里这个client是一个独立设备创建起来 的,拿来用的那个对象,它是hns3的某个vf创建出来的一个nic功能,或者hns4的某个pf创 建出来的roce功能。这是完全不同的两个概念。
然后,从这个角度,让我们分析一下Algo本质是什么。比如我做了一个hns3的硬件,这个 硬件提供了一个pf,pf注册了一个ae,ae匹配给client,提供一组回调用来控制ae的行为 。这时逻辑已经完满了,我们根本不需要Algo。我们把Algo抽象出来,是我们认为这个东 西可以被一个以上的ae使用。但如果两个ae共享同一个Algo,这就意味着,这两个ae的硬 件设计者会有某种关系,比如,它是一种顺序升级。但如果这样的话,我们应该让这个ae 的驱动直接支持两种硬件,这更容易管理,对不对?
我能理解Algo被发明出来的原因:当Client的开发者一次支持多种硬件的时候,而且这些 硬件看来大流程都是一样的时候,他在做细节设计的时候会忍不住希望用一个不同的函数 表来让流程走不同的路径。但如果他好好对这个概念空间建一个模,他就应该明白,这个 函数表和AE本身的ops(回调表)没有本质区别。那他就不应该发明这个Algo(实际情况就 是,现在没有任何两个AE共享同一个Algo,这就说明这个Algo的抽象失败了),而应该有 选择地把这些函数合并到AE的ops中。
但你说我们现在是否应该进行所谓的重构,把Algo合并到AE中呢?我不以为然。
这个东西虽然不好,但并没有造成构架性的破坏,我在概念空间中把它埋到AE中就可以了 ,也就是说,在做新的功能建模的时候,我根本就看不见它就对了。你编码的时候仍可以 在Algo中加新的回调,我都认为它是AE的ops的一部分就对了。
这时概念空间建模就是这样的:
.. figure:: _static/hnae架构2.svg
这副图还特别强调了client可以被泛化为多少种具体的实现,
这个强调并不是非写不可的,只是看你需要如何进行交流而已,
这里强调这一点,是让读者明白,图不是代码,它的重点不是形式化描述,
而是交流和理解。
这看起来对编码没有任何影响的,但对我们建模,简化对系统的理解,选择在哪里开接口 至关重要。
只有我们这样看待概念空间建模了,这个建模才是有用的,否则编码变成啥样,就画一幅 复杂的框框线线,让它和代码一样。你就知道为什么总有人说“UML图没有什么用,不如写 代码”了——你的模型就不是用来指导编码的,而是用来给代码做注的——代码都写出来了,要 你做注作甚?
前面这个是入门,说明概念空间是什么,如何使用,现在我们往下走深入一点。
我们进行概念抽象,就是要避免我们建高层逻辑的时候,需要关注下层细节。这就好像我 们做一个门禁系统,我们把人分为:管理员,普通有权进入者,无权进入者。我的门禁系 统就只认识刷卡的卡的身份是管理员还是普通有权进入者。我们可不认识你是王董事长, 张经理还是牛博。你王董事长被踢出管理层,变成无权进入者,我只要保证你的卡换了就 可以了,我可不看别的细节。抽象的目的就是:进行抽象后,整个接口变得光滑,这个逻 辑就很容易确定它是能成立的还是不能成立的。否则每次分析一个逻辑的时候要判断你其 他1000个属性。这个逻辑就无法建立了。
这在hnae的系统中是一样的,我们既然抽象了client的概念,client内就只认识提供功能 的ae,我可不管你这个ae提供了一个client还是10个client,也不管这些client是否是同 一种client。
所以,5.9内核中的版本,其实已经破坏了我们这个假设了,因为algo的回调函数使用了一 个这样的处理句柄:
.. code:: c
struct hnae3_handle {
struct hnae3_client *client;
struct pci_dev *pdev;
void *priv;
struct hnae3_ae_algo *ae_algo; /* the class who provides this handle */
u64 flags; /* Indicate the capabilities for this handle */
union {
struct net_device *netdev; /* first member */
struct hnae3_knic_private_info kinfo;
struct hnae3_roce_private_info rinfo;
};
u32 numa_node_mask; /* for multi-chip support */
...
}
请注意了,client认识这个设备的roce和nic信息,而且是完整的整个private_info的信息 。这还哪里抽象了?现在你还可以完全基于你的client接口编程序吗?你敢保证roce驱动 里面动了某些东西,nic client里面一定没有问题?你得把里面的代码统统看一遍,你才 会知道。
而某些工程师,可能会懒得看,他们会“试一下能不能跑”,能跑就算了。但这个过程影响 了什么逻辑,导致那个逻辑不通呢?他们根本不会知道。
这就是失控的开始。
我同样能理解这个错误是怎么开始的:这个核心的逻辑在于:roce和nic client共用了同 一个硬件,他们之间在某些逻辑上无法完全独立。比如nic的pf复位自己的vf,势必导致对 应的roce失效。但如果我们希望他们互相独立,这个互相影响的逻辑就必须被管理。比如, 我们可以抽象一个概念叫:client_broadcast(ae, event),让所有和自己关联的client都 得到某个通知。这样他们的关联就被抽象弱化了。这样我们分析他们各自的逻辑的代码的 时候,这仍是可控的。
同时,这样的控制仍不一定要发生在代码上(当然,很多时候我们会尽量在代码呈现出我 们的架构控制期望),我们可以通过概念控制定义的控制,让我们把这些接口的引入变得 可控。
概念不是代码,不是能跑就行的无抽象逻辑。概念是我们脑子中的“大白话”。
我们再讨论一个相对独立的问题去给读者强化概念空间建模的用法。
在讨论这个框架的时候,我遇到过一个设计,当用户设置mac地址的时候,在设置的目的是 建立Bonding的时候和目的是简单的设置的时候,设置的流程是不同的(比如当设置的是 Bonding的时候,我们可以把这个地址写入一个Cache或者干脆写入特殊的交换机,提升链 路层的调度效率)。
从细节编码上说,一个目标要做不同的行为,我们必须找到分支的条件。但设置mac地址这 个要求到了ae驱动这一层,我们只有这样的接口(实际代码中其实是Algo的回调):
.. code:: c
void (*get_mac_addr)(struct hnae3_handle *handle, u8 *p);
int (*set_mac_addr)(struct hnae3_handle *handle, void *p,
bool is_first);
这里我们没有办法分出这个mac地址的设置目的(到底是普通修改还是为Bond修改)。有人 设计了一个复杂的行为,从而在ae的回调函数中捕获了这个目标的Pattern,在里面分出了 两个分支。这其实就已经不是架构设计了,这是直接编码。
如果我们进行概念空间建模,我们就应该给出一个非常清晰的判断逻辑,比如可以是这样 的:
当一个netdev被设置为Bonding的时候,无论它是Master还是Slave,都会被加上 IFF_BONDING标记。
我们引入一个新的约束,上层模块必须保证先设置这个标记,然后才能开始修改mac地址 。
IFF_BONDING标记被硬件驱动层访问,不常见,但已经有先例,表现为qlogic和 icsci/cxgbit。所以,我们可以冒这个险。
这也属于概念空间建模。也是独立于编码细节,先进行“大道理”上的高层分析。如果我们 仅仅让代码能跑,我们就不浪费时间做什么架构设计了。上面这个设计,虽然不那么美好, 但它:
可行
让我们知道我们冒了什么险
这样才是在做独立于编码逻辑的架构设计。
不知道这样几个例子,能否让读者们看明白我们做各种概念空间建模的目的是什么。架构 不是让你把代码画成图,架构是让你把系统逻辑变得清晰,知道每次改细节的时候自己是 不是把所有关联给弄到一起去了,以后没法维护。