Linux操作系统的存储子系统应该是Linux中最为复杂的子系统了。其实很多子系统都认为自己是最复杂的子系统,比如内存子系统和网络子系统也这么说。无论如何,存储子系统在Linux中是比较复杂的。今天我们就介绍一下Linux的存储子系统中的硬盘与RAID的相关内容,后面再写一篇关于LVM与文件系统的内容。
硬盘
在Linux的存储子系统中,最底层的就是硬盘了。这里的硬盘并不是指我们看到的硬盘硬件,而是指在Linux内部看到的硬盘设备,或者说是块设备。如果我们在/dev目录执行以下ls命令,就可以看到很多设备。在这些设备中以sd开头的就是基于SCSI协议的硬盘。
图1 Linux中的块设备
无论是基于SAS、iSCSI还是FC的磁盘设备,大概都是这个样子。形似dm-X的是Device Map块设备,也就是通过LVM进行管理的设备,这种设备是一种逻辑设备。
在Linux操作系统中块设备的种类很多,有本地磁盘设备、有SAN设备还有基于网络的块设备。在虚拟机中块设备又呈现为另外一种文件名,比如在Xen虚拟机中为xvdX。
虽然名称差异很大,但是在Linux操作系统内核中的实现却非常简单。在内核中任何磁盘块设备都是通过调用add_disk函数完成的。在《Linux设备驱动程序》这本书对块设备进行了详细的介绍,并且可以通过非常简单的代码实现一个自己的块设备。
图2 最简单的块设备驱动
这里面有2个函数,也就是alloc_disk和add_disk。前一个函数是分配一个通用块的结构体,后者则是将该块设备添加到内核,也就是在/dev目录下生成一个“文件”。以上述代码为例,执行后会生成如下块设备。
brw-rw---- 1 root disk 251, 0 Jun 16 09:13 /dev/sbulla
这里我们自定义了一个设备名称sbulla。其实我们看到的SCSI设备也是这样定义的,只不过其定义名称的时候是通过sd字符。
以上述代码为例,在块设备中比较重要的地方是初始化了一个队列处理函数(sbull_full_request)。所有从上层访问该块设备的请求都会转发到该处理函数进行处理。
所有块设备都要初始化这个队列,并且提供一个请求处理函数。不同的块设备的请求处理函数略有不同。比如常见的SCSI块设备,其处理函数初始化过程如下:
q = __scsi_alloc_queue(sdev->host, scsi_request_fn);
而nbd(网络块设备,通过网络的方式将服务端的文件映射为客户端的块设备)设备的初始化队列的代码如下所示:
disk->queue = blk_init_queue(do_nbd_request, &nbd_lock);
类似的例子还很多,本文不再一一介绍。这里我们需要理解一点,核心问题在于注册处理请求的回调函数,以及通过add_disk就可以在/dev目录下面创建一个块设备。
另外一点,对于任何类型的块设备,无论是本地硬盘,还是经过网络的NBD和iSCSI,还是FC设备,最后都是/dev目录下的一个文件,而这个文件其实就是块设备。我们可以通过对该文件的读写实现对块设备的访问。
RAID
作为普通用户使用单个硬盘是没有任何问题的,但是作为企业应用使用单个硬盘存在很大的风险。这时因为硬盘随时有可能损坏,因此我们需要一种机制来保证即使出现硬盘故障的情况下,数据不会丢失,且业务仍然可以正常工作。
RAID正是解决上述问题的技术。RAID的全称为廉价冗余磁盘阵列(Redundant Array of Inexpensive Disks),从字面可以看出其基本原理就是通过廉价的磁盘组成一组磁盘。RAID不仅仅可以通过冗余的方式解决数据可靠性的问题,还可以提高性能。其主要原理就是将请求拆分到多个物理硬盘来执行,性能自然比一个硬盘快了。
在Linux操作系统层面,其实就是将物理磁盘通过软件抽象为逻辑磁盘。以RAID1(两块磁盘存储相同的数据,在出现一块磁盘故障的情况下,数据不丢失)为例,通过Linux内核中的软件创建一个虚拟的块设备,而该块设备中记录了底层对应的物理设备及相关参数。
图3 RAID1 示意图
因此,从用户层面来看就是一块普通的磁盘设备,而在底层却是2个独立的物理硬盘。当用户向逻辑磁盘写数据的时候,其中的软件会通过参数进行计算,并将数据重新定向到底层的物理设备。通过这种方法可以保证即使出现某个物理磁盘损坏,用户的数据仍然完好无损。
除了上面说的RAID1外,还有很多RAID类型。不同的RAID类型实现不同的功能。比如RAID0实现条带化,主要是提升性能;RAID1则是实现数据的冗余,防止磁盘故障导致的数据丢失;由于上述RAID只能解决一方面的问题,因此有人讲两者结合,出现了RAID10和RAID01,这样既能保证数据的可靠性,又能提升性能。
由于RAID1是一份数据写到两个设备,因此只有50%的有效数据。为了提高有效数据率,于是发明了RAID5和RAID6等类型。其中RAID5通过增加一个校验数据来保证数据的可靠性,以5块盘的RAID5为例,其中有效数占4块盘的空间,有效数据80%。但是RAID5有个问题,就是一组磁盘中只能坏一块,如果损坏的磁盘超过1块就会导致数据丢失。RAID6的算法与RAID5类似,它的特点是可以容忍2块磁盘故障。
在实现层面,Linux的RAID实现在用户态和内核态都有涉及。其中用户态主要进行RAID的管理,而内核态一方面配合用户态进行RAID管理,另外一方面则实现对IO的处理,这部分才是RAID最为核心的内容。
图4 软件架构
对于基于SCSI物理磁盘的RAID来说,Linux环境下整个软件架构如图4所示。其中虚线以上的为用户态的软件模块,虚线以下的为内核态的软件模块。这里比较核心的是RAID公共层,在这里主要创建md设备,该设备是一个逻辑设备,也是用户可以看到的RAID设备。其下则是具体的RAID模块,用于实现不同的RAID级别(算法)。
再往下就是通用SCSI驱动层了,也就是图中的SCSI磁盘驱动这一层的内容。该层其实是SCSI系统的上层驱动(SCSI子系统分为上中下三层)。RAID模块通过调用该层的数据访问接口就可以实现物理磁盘数据读写了。