[原写于2012年10月]
介绍flashcache的文章很多,我就不废话了。使用上,有余峰老哥的文章;原理上,有ningoo同学的flashcache系列。但是ningoo同学漏掉了device mapper和flashcache的动态原理,只讲了静态的数据结构。我就钻个空子补充一下。
一般来说,我们对磁盘的read和write最后都会走到kernel里的submit_bio函数,也就是把io请求变成一个个的bio(bio的介绍看这里),bio是linux内核里文件系统层和block层之间沟通的数据结构(有点像sk_buffer之于网络),到了block层以后,一般是先做generic_make_request把bio变成request,怎么个变法?如果几个bio要读写的区域是连续的,就攒成一个request(一个request下挂多个连续bio,就是通常说的“合并bio请求”);如果这个bio跟其它bio都连不上,那它自己就创建一个新的request,把自己挂到这个request下。合并bio请求也是有限度的,如果这些连续bio的访问区域加起来超过了一定的大小(在/sys/block/xxx/queue/max_sectors_kb里设置),那么就不能再合并成一个request了。
合并后的request会放入每个device对应的queue(一个机械硬盘即使有多个分区,也只有一个queue),之后,磁盘设备驱动程序通过调用peek_request从queue里取出request,进行下一步的处理。
之所以要把多个bio合并成一个request,是因为机械硬盘在顺序读写时吞吐最大。如果我们换成SSD盘,合并这事儿就没那么必要了,这一点是可选的,在实现设备驱动时,厂商可以选择从kernel的queue里取request,也可以选择自己实现queue的make_request_fn方法,直接拿文件系统层传过来的bio来进行处理(fusionio好像就是这么做的)。
我曾经弱弱的问过涛哥:既然bio有bio_vec结构指向多个page,那么为什么不干脆把多个bio合并成一个bio呢?何必要多一个request数据结构那么麻烦?
涛哥答曰:每个bio有自己的end_io回调,一个bio结束,就会做自己对应的收尾工作,如果你合并成一个bio了,就丧失了这种灵活性。
linux kernel有一个device mapper框架(以下简称dm框架),linux上的软RAID、multipath等都是通过此框架实现的。dm可以将多个设备聚合成一个虚拟设备提供给用户使用,其原理就是把这个虚拟设备的make_request_fn方法实现成了自己的dm_request,这样所有发往这个虚拟设备的bio都会走进dm_request,然后dm通过判断这个虚拟设备是基于request(request based)的还是基于bio(bio based)的来分别处理:
如果虚拟设备是request based,则和磁盘设备一样走generic_make_request把bio合并成request(如上),注意这些request最后放到的是虚拟设备的queue里,等到虚拟设备通过kblockd调用dm_request_fn时,dm_request_fn里会调用peek_request,从虚拟设备的queue里拿出request,将其clone(clone后的request里的bio指向的page是同一个page,只是分配了新的bio和新的request),然后调用map_request对request做映射(map_request里把map_rq接口暴露给了使用dm框架的开发者),最后把clone后的request发向低层的真实设备。
如果虚拟设备是bio based,就更简单了,调用_dm_request函数,一样要clone bio,然后调用__map_bio对bio做映射(__map_bio里把map暴露给了使用dm框架的开发者),最后把clone后的bio也是通过generic_make_request发向低层的真实设备,这次generic_make_request生成的request就是放在真实设备的queue里了,这是与request based的不同之处。
flashcache是基于dm框架实现的,很自然的,是把一个SSD盘和一个机械硬盘聚合成一个虚拟设备供用户使用。
flashcache把cache(指SSD盘)分为多个set,每个set里有多个block(默认一个block是4KB,一个set包含512个block,即2MB),set里的block是用lru链表组织起来的,每个block还记录了自己存放的是disk的哪个sector起始的位置里对应的内容(这个起始的sector编号在flashcache的文档里被称为dbn)。
disk(这里指机械硬盘)也虚拟的分为多个set只是为了方便做hash。hash算法非常简单,先看访问的是disk的什么位置,相当于在disk的哪个set里,然后模上cache里的set数,结果就是在cache里对应的set编号了。找到cache对应的set后,继续在set的lru表里挨个儿block的比对dbn号,对上了就成功,对不上说明cache里没有缓存要读取的disk内容。
例如cache大小为10G,disk大小为100G,用户要读取磁盘上偏移54321MB处的2K内容,那么首先对54321MB这个位置做hash,2MB一个set,对应的set number是27160,cache的总set数为5120,那么 27160 mod 5120 结果为1560,也就是说应该去cache的第1560个cache去找,然后来到cache的1560 set里用 dbn 28479848448 遍历查找lru。
flashcache主要是实现了dm框架暴露出来的map接口(参考flashcache_map函数),收到bio后,先做hash,然后在cache(这里指SSD盘)里查找:
A. 如果是读bio
1 如果查找成功,直接将结果返回
2 如果查找失败,则找set内空闲的block(如果没有空闲的,则用最“旧”的block),直接读取disk里对应的内容返回给用户,返给用户后设置延时任务将读取的内容放入这个空闲block里
B. 如果是写bio(我们仅列举writeback情况)
1 如果查找成功,拿到对应的block
2 如果查找失败,拿到对应set里最“旧”的block
直接将数据写入此block,返回给用户(用户的write系统调用就可以返回了),完成后将该block的状态设为DIRTY并设置延时任务,任务内容为将cache里的内容写往disk(这样既能让用户的写请求迅速完成,又能一定程度保证数据最终被写往disk)。延时任务完成后,便可以去掉DIRTY标记了。
flashcache还会不时的将cache的set里长期没被访问的DIRTY的block写往disk,以保证有足够多的干净的block供以后使用。这个“不时的”不是靠定时器实现的,而是通过在flashcache_write_miss、flashcache_read_miss等函数里调用flashcache_clean_set来做到的。
你好,有个问题想请教一下。“找到cache对应的set后,继续在set的lru表里挨个儿block的比对dbn号,对上了就成功,对不上说明cache里没有缓存要读取的disk内容。”这一句中你提到比对dbn号,按照我的理解,一个bio里有请求的开始扇区号,同时还有请求的扇区数,假设这个block的起始扇区号为0,一个block内有8个扇区(512Byte/扇区),而bio请求的起始扇区号为2,请求大小为3,那么这种情况应该也算命中,但是单纯比对dbn号又是不命中的。能否解释一下,3q!
@罗毅:如果bio的起始扇区号是2,大小为3,那flashcache根本不找它,也不cache它,直接写往硬盘,因为它的size小于flashcache的block size(可以配置,默认是4K),我刚做实验验证了一下(你也可以试试:)
./flashcache_create -v -p back cachedev /dev/sdc /dev/sdd
dd if=/dev/zero of=/dev/mapper/cachedev seek=1 count=1 bs=1536 oflag=direct
然后用dmsetup status可以看到,disk write多了一个计数,而不是ssd write
代码就在flashcache_map()里
对于size小于dmc->block_size的,直接走flashcache_start_uncached_io了。
如果上层发下来的是大于4K的io,那么device mapper层会把它split成不大于4K的io,然后再走flashcache的逻辑。
您好!有两个问题想请教您。为什么flashcache要设置dmc->block_size,只缓存dmc->block_size大小的bio请求而忽略小的bio请求呢?对于size大于这个值的请求,您说是device mapper层会把它split成不大于4K的io,这个是在哪处体现的呢?谢谢!
@张晨:第一个问题,为什么忽略小于block_size的bio?我觉得应该是为了节约SSD空间,如果小于block_size的bio也直接存进去并占一个block,那空间就浪费了。之所以用flashcache就是因为SSD空间宝贵,这样浪费着用就不符合本意了。
第二个问题,在哪里把bio给split成不大于block_size的io?是因为dm框架给每个dm_target都提供了一个split_io参数,意思是凡是大于split_io的都得切开,flashcache_conf.c里是把这个参数设成了block_size:
ti->split_io = dmc->block_size;
然后,在dm框架处理过来的bio时,会走 _dm_request():
_dm_request() --> __split_and_process_bio() --> __clone_and_map()
__clone_and_map()函数里会判断io的大小有没有超过max_io_len(),超过了就要用split_bvec()来切io,而这个max_io_len()里就是根据split_io来做判断处理的
不一定回答的正确,欢迎讨论
queued = flashcache_inval_blocks(dmc, bio);
spin_unlock_irq(&dmc->cache_spin_lock);
if (queued) {
if (unlikely(queued < 0))
flashcache_bio_endio(bio, -EIO, dmc, NULL);
} else {
/* Start uncached IO */
flashcache_start_uncached_io(dmc, bio);
}
楼主有个问题想要请教,
queued = flashcache_inval_blocks(dmc, bio);
如果没有申请到job memory返回应该为-ENOMEM(-12),如果返回为-12又如何进入
if (queued) {
if (unlikely(queued < 0))
flashcache_bio_endio(bio, -EIO, dmc, NULL);
这段代码呢?
如果queued =0,盖在代码中如何进行?
关于为什么忽略小于block_size的bio,我觉得主要还是考虑到SSD在处理小写请求的时候性能不高以及SSD的使用寿命,SSD有一个特性,写前之前需要檫除,如果处理大量的小写请求,那么檫除操作就会很多,檫除操作的花销很大,这一方面很影响性能,另一方面,SSD的檫除次数是有限制的,这也会缩短SSD的使用寿命。
在您上文举的一个例子中:例如cache大小为10G,disk大小为100G,用户要读取磁盘上偏移54321MB处的2K内容,那么首先对54321MB这个位置做hash,2MB一个set,对应的set number是27160,cache的总set数为5120,那么 27160 mod 5120 结果为1560,《也就是说应该去cache的第1560个cache去找》,然后来到cache的1560 set里用 dbn 28479848448 遍历查找lru。
应该改为:也就是应该去cache的1560个set去找
另外 dbn 的值我觉得应该是 54321*1024*2 = 111249408(dbn是以扇区 512个字节为单位的)。
顺便想再问您一个问题:
flashcache的设计文档中有如下一段话:
On an clean cache shutdown, metadata for all cache blocks is written out to flash. After an orderly shutdown, both VALID and DIRTY blocks will persist on a subsequent cache reload. After a node crash or a power failure, only DIRTY cache blocks will persist on a subsequent cache reload. Node crashes or power failures will not result in data loss, but they will result in the cache losing VALID and non-DIRTY cached blocks.
为了保证缓存数据的持久性,缓存块的元数据信息不仅仅会在内存中存在,在SSD上也会有一份。
cache设备正常关闭的情况下,会将内存中的元数据信息写进SSD,那么下一次cache设备重新载入的时候,又可以将SSD中保存的元数据信息调入内存。
问题是cache设备没有正常关闭的情况下,为什么只有dirty缓存块可以保持持久性?或者说dirty缓存块是如何保持持久性的?
谢谢!