flashcache支持资源隔离的改造

去年年底的时候对flashcache做了一番研究,并进行了改造,所谓改造,其实就是希望它能支持cgroup,也就是说,当有在不同cgroup里运行的进程对flashcache进行读写时,flashcache能根据cgroup的不同权重分配这些进程以不同的iops(IO per second)。详细的资料在这里:《flashcache原理及改造》

事情还没有结束,现在能给flashcache设备设io-scheduler了,设成cfq就可以按cgroup weight来分配iops,但是,做为稀缺资源的ssd设备,其cache空间并没有像IOPS那样按比例分配给各个cgroup,所以,今年,我继续把“cache空间按cgroup比例分配给进程”的功能做完了。patch系列在这里

原理:flashcache按hash把io缓存到ssd设备的不同的LRU链表上。我加了一个“struct flashcache_group”,将cgroup的信息存入其中,同时由它来管理”子LRU“。比如,有4个cgroup的进程在对flashcache做读写,那每个LRU都要被拆成4个由"struct flashcache_group”管理的”子LRU“,每个“子LRU”所用的cache空间的大小(即flashcache所谓的block的个数),就是由它所在的cgroup的权重决定的。
当flashcache设备刚初始化好时,所有的“子LRU”都是空的,cache都在根group的手中(正好对应cgroup里的root group概念),一旦有进程的IO来了,才根据它所在cgroup的权重,慢慢将block划入它所属的”子LRU“(当然,不能超过最大比例);当该进程退出后,cgroup即被destroy,于是,对应的”子LRU“也会将自己占的cache返还给”主LRU“。

由于这些改动不仅涉及flashcache本身,还要牵扯kernel里很多block层的代码,所以,目前还不能单独提交给flashcache社区,只能先保留在ali_kernel里,想试用的同学,可以用如下方法:

1. 下载ali kernel的代码并编译dev分支,然后安装内核并重启机器

git clone git@github.com:RobinDong/ali_kernel.git
git checkout -b dev origin/dev
make localmodconfig #根据自己机器的硬件情况选择config文件
make -j20
make modules_install
make install
vim /etc/grub.conf #修改grub以启动ali kernel
reboot

2. 进入ali kernel后,下载flashcache工具并编译stable_v2.1分支

git clone git@github.com:RobinDong/flashcache.git
git checkout -b stable_v2.1 origin/stable_v2.1
make

3. 使用编译的flashcache工具创建设备(加"-r"参数以确保flashcache进去request base模式)

src/utils/flashcache_create -p back -r cachedev /dev/rxdrv /dev/sdf

咱们接着用以下fio文件做个测试:

[global]
direct=1
ioengine=libaio
runtime=60
bs=4k
rw=randwrite
iodepth=8
filename=/dev/mapper/cachedev
numjobs=1

[group1]
cgroup=test1
cgroup_weight=1000

[group2]
cgroup=test2
cgroup_weight=800

[group3]
cgroup=test3
cgroup_weight=600

[group4]
cgroup=test4
cgroup_weight=400

测试结果:

cgroup IOPS
test1 684
test2 581
test3 425
test4 264

再看看SSD设备里block的分配情况:

cat /proc/flashcache/rxdrv+sdf/flashcache_blocks

Group		Weight		Block Count
/test2		800		54865
/test1		1000		68582
/test3		600		41149
/test4		400		27433

看来cache空间的分配比例还是很准确的

Oracle ASM与flashcache不兼容问题

先强调,本文所说的flashcache是指facebook的那款开源软件,不是指“快速设备”。
同事先是反映flashcache创建的设备被fdisk和parted等工具分区后,在/dev/目录下没有出现新设备(通常,对/dev/sdb设备分区后,会出现/dev/sdb1,/dev/sdb2等子设备)
查了一下资料,multi-device系列的设备(包括flashcache和linux下的软raid)都不支持原始的分区方式,只能用lvm创建logic volume来代替分区。
后来问了一下同事,原来是想在flashcache上创建Oracle ASM,按照这上面说的步骤试了一下

create diskgroup data external redundancy disk '/dev/raw/raw1','/dev/raw/raw2';

Oracle居然说/dev/raw/raw1设备不在"discovery set“里,没办法,只好

chown oracle:dba /dev/mapper/cachedev

后改用/dev/mapper/cachedev,但是又报错说ASM加入的单盘不能超过2048G,即2T,而我的flashcache虚拟设备是3T。
查到这个资料,ASM还真有这个限制,于是,耍了个滑头,重新创建flashcache,把它分成两个1.5T的虚拟设备,再:

create diskgroup data external redundancy disk '/dev/mapper/cachedev1','/dev/mapper/cachedev

这次还报错,说

ERROR at line 1:
ORA-15018: diskgroup cannot be created
ORA-15130: diskgroup "DATA" is being dismounted

于是尝试删除"DATA"这个diskgroup:

drop diskgroup DATA;

又报错:

ERROR at line 1:
ORA-15039: diskgroup not dropped
ORA-15001: diskgroup "DATA" does not exist or is not mounted

我骂人的心都有了,又说”DATA“已经dismouted了,又说它不存在,搞什么?
最后也没成功。Oracle ASM + flashcache,够呛,还是lvm + flashcache吧,或者干脆不用卷管理,直接用文件系统。

块设备层死锁

[原写于2012年12月]

​把flashcache改为request-based后,虽然IO数量可以按比例控制了,但是作为“珍稀”资源的cache(通常是昂贵的固态硬盘或更昂贵的fusionio卡),也需要按例分配不同进程以不同的cache空间。近一个月我一直在忙着加这个功能。
新代码写完了,压力测试二十多个小时,出现了一个死锁:

<4>[11957.888102] [] ? nmi+0x20/0x30
<4>[11957.897016] [] ? _spin_lock_irqsave+0x2f/0x40
<4>[11957.906089] <> [] ? flashcache_clean_set+0x9f/0x600 [flashcache]
<4>[11957.915610] [] ? dm_io_async_bvec+0xbc/0xf0 [flashcache]
<4>[11957.925377] [] ? flashcache_io_callback+0x0/0x4a0 [flashcache]
<4>[11957.935318] [] ? flashcache_read_miss+0x9e/0x150 [flashcache]
<4>[11957.945378] [] ? flashcache_read+0x138/0x330 [flashcache]
<4>[11957.955511] [] ? flashcache_mk_rq+0x165/0x1d0 [flashcache]
<4>[11957.965677] [] ? dm_request+0x5d/0x210 [dm_mod]
<4>[11957.975911] [] ? generic_make_request+0x261/0x530

应该是有进程拿到了request_queue的锁 queue_lock 却再没有释放。
由于出现了“flashcache”的字样,有很大嫌疑是我新加的代码有问题,但是不管怎样,二十多个小时的重现步骤太慢了,得找到一个快点的,于是花了几天尝试不同的压测脚本(一边尝试,一边把我的cubieboard刷成了android,我的天,android比linaro好用多了,而且是刷在cubieboard自带的NAND里,可以不用SD卡,刷机所需image见这里),终于把重现步骤缩短为2个小时左右,接下来把我加的代码全回退,依然有死锁——看来是redhat-2.6.32内核的问题了。

我在内核报死锁的地方watchdog_overflow_callback()函数里调用panic()之前加了一句show_state(),即把死锁时所有进程的stack信息统统输出到dmesg里去(再依靠netoops把消息传到另外一台机器上,因为出现死锁的本机已经登不上去了)。
跑了不到两个小时,死锁出现了,我在netoops吐出来的一堆stack信息里一个个找,看谁像是那个拿到锁不还的人,发现一个可疑的:cfq_get_cfqg()里调用blk_init_rl()函数。blk_init_rl函数里面要做一个内存分配,但是传给kmalloc_node的gfp_mask却是GFP_KERNEL,带上GFP_KERNEL后在分配不到内存时进程会去睡眠直到别的进程释放了一些可用内存,blk_init_rl()已经拿到了queue_lock,现在却一睡不醒(系统内存紧张,迟迟没有可用内存),当然别的进程来取queue_lock就等疯了。
看来不是我加的代码造成的,也与flashcache没有关系,是block层的问题。

这种情况一般有两种修复办法:一是在分配内存之前把queue_lock释放掉,分配后再重新拿锁;或者,把分配内存的gfp_mask设为GFP_ATOMIC,就是没内存就直接返回NULL,不睡。
不过我看了一下upstream代码,这个问题已经修复了。参见Tejun Heo的两个patch,12,第二个patch的头几个改动才是把GFP_KERNEL改成GFP_ATOMIC。
没想到netoops加show_state()调试死锁这么方便,当然,难的是找到快速重现bug的办法。

flashcache原理

[原写于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

之所以要把多个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来做到的。