V3中Storage的整体架构设计
简述:Storage下管理了RAID卡、硬盘、超级电容、RAID组和逻辑盘5大类对象,本文主要介绍在iBMC软件中如何实现这5类对象的相关功能
组件内的分层设计
整个存储APP被分成了3层,分别是公共层、对象层和服务层。这三层的应用关系如图所示,公共层的函数被对象层和服务层调取,对象层的对象和函数被服务层调取。这样设计可以对各个对象进行解耦,保证同级的对象间不存在依赖关系。
对象层被分成了Object和Collection两小阶。Object标定了具体单个对象会做的事情,比如对象的初始化、对象信息的轮询以及对象对自身状态的检查等。Collection则是整个类的对象的集合,在这个小阶内会提供比如创建对象、操作对象以及向其它服务提供对象等服务。下面我们会分别对这三层进行具体的解构。
公共层
公共层提供一些基础类和基础服务,比如common_def这种公共定义类、error_engine这种公共服务类。公共层的基础类和基础服务只会被上层的对象调用,互相之间不会互相调用。
除此以外,还有method_misc这种涉及业务的公共函数类。比如检查RAID卡的厂商类型、检查逻辑盘名称合法性这种函数。
在存储模块中,公共层equipment_customization.md还有一个比较大的部分,则是sml库。
sml库的作用是用来实现对象层的sml接口。sml库的作用是屏蔽各个厂商的RAID卡的SDK在命令名、命令参数、命令收发方式上的不同。例如博通卡使用的是i2c的通讯方式,而华为自研卡走的是mctp协议。同一个命令,博通卡和华为自研卡的命令字与传参都不尽相同。因此,我们使用sml_lsi来重新封装博通卡的SDK方法,使用sml_histore来重新封装华为自研卡的SDK方法。之后我们再使用sml_base来屏蔽两个库在函数命名上的差异。最后使用l_sml来打通C库和Lua服务。
有一点需要特殊说明的是,由于各个厂商的RAID卡的SDK需要调用协议读写函数,比如博通卡就需要调用i2c的读写函数,而这些函数在SDK内只是提供了一个函数指针,真正的函数实现是需要我们帮着实现的。因此我们使用了plugin方式来实现这些协议读写函数
对象层
对象介绍
我们的对象的设计是根据实际的物理链路关系来的,所以需要先知道在物理链路上这些对象相互是怎么关联上的。
根据上图,我们可以发现在物理链路上可以分为两大类,即接在硬盘背板上的Disk类和接在扩展板上的RAID控制器类。我们分别用Drive和Controller来描述这两个类。同时,一个RAID控制器是可以携带自己的超级电容(BBU)的,因此在会有一个Battery类来描述这个超级电容。
除了这3个类之外,我们知道RAID控制器的作用是组RAID,而RAID组是可以被单独处理的,因此我们为了描述RAID组,使用Array类和Volume类来描述一个RAID组。之所以使用两个类来描述,是因为下面这个图:
对用户来说,他们要的和操作的是逻辑盘(Volume),但是一个Volume形成的基础是Array,所以我们使用了两个类来描述。
自此,5个管理对象就确定下来了,分别是RAID控制器类(Controller)、超级电容类(Battery)、硬盘类(Drive)、RAID组类(Array)、逻辑盘类(Volume)。
所有对象之间的关系如下图:
对象的设计
对于模块内的对象而言,当创建这个对象时会触发这个对象的on_add_object信号,跟着这个信号的传参就是base_obj。
代码示例:
-- 声明c_controller类
---@class c_controller: c_object
---@field Id integer
---@field TemperatureCelsius integer
---@field Name string
---@field on_update c_basic_signal
---@field new fun(...): c_controller
local c_controller = c_object('Controller', {'position'}) -- Controller 类,主键是 position 字段
-- 构造函数
function c_controller:ctor(obj)
self.on_update = signal.new()
end
-- 析构函数
function c_controller:dtor()
self:stop()
end
-- 初始化函数
function c_controller:init()
self:set_default_values()
self.on_update:on(function(info)
self:update_controller_info(info)
end)
endc_object有一个静态属性为c_object_collection,这个是用来提供对象查接口的
代码示例:
function controller_collection:get_by_controller_id(controller_id)
local controller = c_controller.collection:find({ Id = controller_id })
if not controller then
log:notice('[Storage]Invalid Controller id: %d', controller_id)
return nil
end
return controller
end对象的实现
我们这里的对象都是要上资源树的。在V3中,一个要上资源树的对象是分成两部分的,一部分是资源树上的对象,另一部分是模块内的对象。模块内的对象会将资源树上的对象作为base_obj进行管理。所以对象的创建方式有两种,
- 一种是自发现模块在解析CSR的过程中帮我们在资源树上创建对象,然后模块内只需要接收资源树上的对象,并将其作为
base_obj来创建模块内的对象; - 另一种则是由模块自己先创建资源树上的对象,然后再创建模块内对象。这两种对象的创建方式我们都有涉及。
根据物理意义,有物理实体的有3个,控制器(Controller)、超级电容(Battery)和硬盘(Drive)。因为超级电容依附于控制存在,因此我们只有控制器和硬盘使用第一种对象的创建方式。因为超级电容、RAID组类(Array)和逻辑盘类(Volume)都是依附于控制器的,所以这三种对象的创建方式我们使用第二种。
控制器的实现
控制器的on_add_object信号是在controller_collection中统一管理的。因为控制器的加载与一般的对象的加载不同,由于mctp协议的特殊性,在加载使用mctp协议的RAID卡时,我们要先确认当前环境的mctp链路已通,然后再将控制器在mctp中注册好节点,之后会将控制器信息注册到sml库中去,最后才会开始轮询控制器的信息。
超级电容的实现
超级电容依附于控制器,因此超级电容的on_add_object信号是由RAID卡触发的,其触发条件是RAID卡注册完成且创建完成控制器信息轮询任务后触发的.
其核心代码如下:
function c_battery_collection:init()
self.on_add_battery:on(function(controller_id, controller_path)
-- 在资源树上创建对象
local obj = self.storage_app_service:CreateBattery(1, controller_path)
self.battery_list[controller_id] = obj
-- 创建受模块管理的对象
self.object_manager.mc:add_object(BATTERY_NAME, obj, controller_id)
end)
self.on_del_battery:on(function(controller_id)
local obj = self:get_capcacitance_by_controller_id(controller_id)
if obj then
class_mgmt(BATTERY_NAME):remove(self.battery_list[controller_id])
-- 删除受模块管理的对象
self.object_manager.mc:del_object(BATTERY_NAME, obj, controller_id)
end
end)
endRAID组的实现
RAID组的on_add_object信号是在控制器更新RAID组列表的过程中产生的,通过对比新获取的RAID组列表和之前获取到的RAID组列表,新增的使用on_add_array信号传递给RAID组服务,减少的使用on_del_array信号传递给RAID组服务。之所以不直接调用方法,而是使用信号的方式来传递新增和减少的信号,是因为希望将控制器的轮询协程与RAID组服务的协程进行解耦,避免因为RAID组服务自身产生问题影响到控制器对象的轮询任务。
逻辑盘的实现
逻辑盘的创建与初始化流程与RAID组的一致,这里不做特殊说明。
硬盘的实现
当硬盘接收到自发现传递过来的on_add_object信号后,硬盘对象就直接创建了,因为这里我们需要考虑直通盘(即不受RAID卡管理的硬盘)的场景。此时,这个硬盘对象是没有办法更新自身的信息的(受控制器管理的硬盘),因为当前并没有将其与控制器进行关联。因此,当硬盘对象创建后,我们需要将它和控制器读到的受控制器管理的硬盘信息进行一一映射,这个步骤就叫做硬盘的定位。这个流程是在服务层做的,流程如下图:
当一个drive和一个pd绑定后,硬盘对象就可以调用自己所属的控制器的方法来刷新自己的属性和设置自己属性的值了。
服务层
我们在设计服务层时,就是为了让对象间解耦,让对象只专注于自己这个对象的事情,所有需要和别的对象沟通的事情都由一个个的service来干,从而实现对象间的解耦。这个部分我们主要分析1个服务,来体现我们的架构设计。这个业务是link_volume_array_drive_service。在存储模块中,不同对象之间是有关联关系的。为了尽可能地解耦,我们在服务层去实现这个不同对象间关联关系。
以逻辑盘更新热备盘列表和硬盘更新关联的逻辑盘列表为例,看看这个业务是怎么实现的。
已知逻辑盘只可以通过sml库获得热备pd列表,我们之前已经说过了,pd并不是实际的硬盘对象,因此我们要根据pd信息获取与之映射的硬盘对象,然后将硬盘对象的Name属性插入到逻辑盘的HotSpareDriveList属性中。如果将这个操作放在逻辑盘的轮询过程中,即获得了热备pd列表后,我们就去遍历列表,然后获取与之映射的硬盘对象,然后再更新属性。如果这样做,我们就会去引用drive_collection。
硬盘更新关联的逻辑盘列表,首先得要看硬盘的关联Array是否还存在,如果不存在,那么这个硬盘的RefVolumeList属性就被清空。如果Array还存在,那么就检查这个硬盘的RefVolumeList里的逻辑盘是否还存在,如果我们将这个操作放在硬盘的轮询过程中,那么我们就要去引用volume_collection。
这样就会产生下图的循环引用:
如果我们用信号的方式,即在逻辑盘和硬盘的轮询过程中,我们触发对应的更新信号,然后对象该干嘛干嘛去。之后由link_volume_array_drive_service监听信号后执行对应操作,这样就能避免这样的循环引用。