天天看点

大话存储系列22——存储系统内部IO 中

4、卷管理层IO

卷管理层在某种程度上来讲是为了弥补底层存储系统的一些不足之处的,比如LUN空间的动态管理等。卷管理层最大的任务是做Block级的映射。对于IO的处理,卷层只做了一个将映射翻译之后的IO向下转发的动作以及反向过程。另外,应用程序可以直接对某个卷进行IO操作而不经过文件系统。我们所说的不经过文件系统,并不是说Bypass系统内核缓存的Direct IO,而是完全不需要FS处理任何块映射关系。这时就需要由应用程序自行管理底层存储空间,而且此时不能对这个卷进行FS格式或者其他未经应用程序运行的更改操作,一旦发生将导致数据被破坏。

卷管理层将底层磁盘空间虚拟化为灵活管理的一块块卷,然后又将卷同时抽象为两种操作系统设备:块设备和字符设备。比如在AIX系统下,/dev/lv 、/dev/fslv 、/dev/hdisk等字样表示块设备,而/dev/rlv、/dev/rfslv、/dev/rhdisk等带有r字样的设备一般就是字符设备。同一个物理设备会同时被抽象为字符和块两种逻辑设备,用户程序可以直接对块设备和字符设备进行IO操作。这两个设备也是用于上层程序直接对卷进行访问的唯一接口,有各自的驱动——块设备驱动和字符设备驱动。在IO路径的层层调用过程中,IO

Manager 访问卷的时候其实就是访问对应的设备驱动(这一点在系统IO模块架构图中并没有体现),向他们发起SystemCall的。

1、块设备:

    最近一段时间一直在做一个块设备模块的性能分析、优化工作,因此,对Linux文件系统层的代码进行了初略的阅读,颇有心得。Linux的文件系统的确非常庞大,层次也非常清晰。以前一直将注意力集中在了设备管理的层面,所以,通过这次的阅读分析,可以从用户程序的system call开始,将读写请求整个贯穿到底层硬件的DMA。下面对块设备与文件系统之间的接口关系进行阐述,主要解决“文件系统是如何访问块设备的?”这个问题。

    块设备可以在两种情况下发起probe(探测)过程,一是当总线驱动发现了块设备,回调该块设备驱动的probe函数,例如磁盘设备的驱动;二是驱动程序主动probe块设备,例如软RAID驱动程序,在insmod该程序后,会主动调用probe函数。Probe函数是设备驱动程序的核心函数,对于一类设备驱动而言,该函数的实现方式是雷同的,具有固定的格式。

    在块设备的probe过程中,首先需要实例化一个对象——gendisk,该对象在内核中对块设备进行了抽象。Gendisk对象初始化成功之后调用add_disk()函数将gendisk添加到系统中,在add_disk过程中需要生成bdev_inode结构,该结构维护了一个文件系统所需的inode节点,以及块设备的更高层次抽象bdev。该结构描述如下:

struct bdev_inode {

    struct block_device bdev;   //块设备 

    struct inode vfs_inode;     //文件系统inode节点

};

    如下图所示,在inode节点中一系列ops指针,指向具体的操作函数集。

如果inode描述的是一个块设备,那么ops函数集指向标准的块设备操作函数集。当用户open一个块设备时,会找到对应的inode,生成一个file对象,并且将inode中的file_operations(i_op)赋值给file对象中的f_op,从而建立了文件系统接口与块设备之间的接口。

如果inode描述的是一个文件系统,那么inode中的操作函数集将指向文件系统具体的操作函数集。从这个思路来看,inode是vfs层的接口,block device是一个特殊的文件系统,具体文件系统在注册时,需要将回调的方法注册到inode中。/fs/Block_dev.c文件就是block device这一特殊文件系统的具体实现,其中实现了与文件系统之间接口的各种函数,包括file_operations函数集。大家感兴趣不妨研究一下。

在UNIX类操作系统下,块设备表现为一个文件,而且应用程序可以向块设备发起任何长度的IO,就像对文件进行IO时一样,比如512B、1500B、5000B等、IO长度可以为任何字节,而不需要为磁盘扇区的整数倍,然而,块设备也是由底层物理设备抽象而来的,而底层物理设备所能接受的IO长度必须为扇区的整数倍,所以块设备具有一个比较小的缓存来专门处理这个映射转换关系。

块设备一般使用Memory Mapping的方式映射到内存地址空间,这段空间以Page(一般以4KB)为单位,所以访问块设备就需要牵涉到OS缺页处理(Page Fault)方式来读写数据。比如应用程序向某个块设备卷发起一个程度为1500B的IO读,卷管理层接收到这个IO之后将计算这个1500B的IO所占用的扇区总数以及所落入的Page地址,并且进入缺页处理流程从底层物理设备将这个Page对应的扇区读入,这里的IO请求为1500B,所以OS会从底层物理设备读取对应的1个Page大小的数据进入缓存,然后缓存中再将对应的1500B返回给应用程序。

应用程序对块设备发起读IO,块设备就得同时向底层物理设备发起对应的转换后的IO,不管应用程序向块设备发起多少长度的IO,块设备向底层物理设备所发起的IO长度是恒定的(一般为4K,即缓存Page大小)。所以块设备向底层设备发起的读IO属性永远是小块IO,而且对同一个线程发起的IO不会并发只能顺序,对多个线程共同发起的IO才会并行,也就是说每个线程在底层的IO都为顺序执行(限于读IO)。这一点是块设备非常致命的缺点,比如一个应用程序256KB的读IO操作,会被块设备切开成64个4KB的读IO操作,这无疑是非常浪费的,会更快的耗尽底层存储的标称IOPS。但对于写IO来讲,块设备底层会有一定的merge_request操作,既可以对写IO进行合并、覆盖、重排扥操作。

其实UNIX类系统下的设备与文件系统管理下的一个文件无异,唯一区别就是直接对块设备进行IO操作的话,无需执行文件——块映射查询而已。

读操作对于块设备来讲还不至于产生太过恶劣的性能影响,而写IO则会更加严重地摧残存储设备的性能。由于块设备向底层发起的所有IO均以缓存Page大小为单位,现代操作系统的Page一般为4KB大小,如果某应用程序需要写入0.5KB数据,或者4.5KB数据,那么块设备不能直接把对应长度的数据直接写入底层设备。其浪费可谓是惊人而且无法容忍的,我们来看一个例子。下列数据显示了AIX系统上使用IO测试工具对一个块设备进行4096B、2000B、2048B、5000B写IO时系统底层向物理磁盘的IO统计情况如下图:

程序发起的IO时候,不对齐4KB的IO Size会导致OS首先读入对应的Page数据,修改,然后再写入对应的Page数据,所以可以看到写动作伴随了一定程度的读动作,也就是写惩罚。

2、字符设备:

传统的字符设备本来是专指一类接收字符流的设备比如物理终端、键盘灯,这种设备的特点是可以直接对设备进行最底层的操作而不使用缓存(但是必须有Queue),而且每次IO都必须以一个字符为单位(卷所抽象出来的字符设备以一段连续扇区为一个单位)。所以具有这种特点的实际设备或者抽象设备都被称为字符设备。而将卷抽象为字符设备并不是说将IO从扇区改为字符,而只是抽象出字符设备所具有的的特点。

在任何操作系统下,对字符设备进行IO操作必须遵循底层的最小单位对齐原则,比如对于卷字符设备来讲,每个IO长度只能是扇区的整数倍,如果IO长度没有以扇区为单位对齐(比如513、1500),那么将会收到错误的通知而失败。虽然unux类操作系统下的字符设备也表现为一个文件,但是这个文件却不像块设备一样可以以任意字节进行IO,因为OS没有为字符设备设置任何缓存(但是存在Queue)

3、裸设备与文件系统之争:

字符设备又称为裸设备,应用程序可以选择使用文件系统听的各项功能进行对文件的IO操作,当然也可以选择直接对裸设备进行IO操作,只不过直接对裸设备操作需要应用程序自行维护数据——扇区 映射以及预读缓存、写缓存、读写优化等。比如数据库类程序自身都有这些功能,所以没有必要再使用文件系统来读写数据。而由于块设备的诸多不便和恶劣性能影响,不推荐直接使用。那基于文件系统的IO和基于裸设备的IO,我们可以着重讨论一下哪个好:

文件系统拥有诸多优点是毋庸置疑的,但是对于某一类程序,FS提供的这些“方便”的功能似乎就显得很有局限性了,比如缓存的管理等,由于文件系统是一个公用平台,同时为多个应用程序提供服务,所以它不可能只为一个应用程序而竭尽全力服务;况且最重要的是,FS不会感知应用程序实际想要什么,而且FS自身的缓存在系统异常当机的时候最容易造成数据的不一致情况发生。其次,使用缓存的IO方式下,对于读请求,系统IO路径中的各个模块需要将数据层层向上层模块的缓存中复制,最后才会被OS复制到用户程序缓存;对于写请求,虽然缓存IO方式下,写数据被OS接收后即宣告完成,但这也是造成Down机后数据丢失的主要原因之一。所以对于大数据吞吐量IO请求,

避免内存中多余的数据复制步骤是必要的(此外还有另外一个原因后续介绍)。但是对于一般程序,是完全推荐使用文件系统进行IO操作的。

另外一个最重要的原因,在使用内核缓存以及文件系统缓存的情况下,容易发生读写惩罚,这事非常严重地浪费。

对于这类IO性能要求非常高而且对缓存要求非常高的程序,他们宁愿自己直接操作底层物理设备,也不愿意将IO交给FS来处理。这类程序典型的代表就是数据库类程序。虽然这些程序也可以使用文件系统来进行IO操作,但是这个选择只会给程序带来一个方面的好处,那就是文件管理会方便,比如可以看到数据文件实实在在的被放在某个目录下,可以直接将数据文件复制出来做备份,将文件系统快照保护等。而选择文件系统所带来的坏处也是不少的,比如最大的劣势就是重复缓存预读,FS预读了数据,数据库程序依然自己维护一个预读缓存,这两个缓存里面势必有很多数据是重复的,增加了许多空间和计算资源开销,而且这些数据不见得都会产生Cache

Hit效果,所以这类程序宁愿使用裸设备自行管理数据存储和数据IO,所带来的唯一缺点就是数据管理很不方便,除了程序自身,其他程序只看到了一块光秃秃的裸设备在那,里面放的什么东西,怎么放得,只有程序自己知道。

4、Directl IO与裸设备之争

有没有一种方法能够结合FS和裸设备带来的优点呢?有的,为了即享受文件系统管理文件的便利同时而又不使用FS层面的缓存,将缓存和IO优化操作全部交给应用程序自行处理,FS只负责做  文件—扇区  的映射操作以及其他文件管理层面的操作,节约内存耗费以及太高处理速度,操作系统内核提供了一类接口,也就是前文汇总出现的FILE_FLAG_NO_BUFFERING参数。当然,这个参数只是Windows内核提供的,其他操作系统也都有类似的参数。这种Bypass系统内核缓存的IO模式称为DIO,即Direct  IO模式。在UNIX类操作系统下,在mount某个FS的时候可以指定“-direct”参数来表示任何针对这个FS的IO操作都将不使用内核路径中任何一处缓存。当然,也可以在应用程序层控制,比如打开文件时给出O_SYNC或者O_DIRECT、FILE_FLAG_NO_BUFFERING之类的参数,那么不管目标FS在mount是给出了何种参数,这个程序的IO都将不使用文件系统缓存。