操作系统: 10 2012存档

flashcache是bio based,也就是说,做为虚拟设备它是直接处理上层发下来的bio们。具体点说,内核所有的block io操作都要走submit_bio,而submit_bio会走generic_make_request,进而调用每个设备自己的make_request_fn函数(代码里是q->make_request_fn),而dm框架对虚拟出来的设备是实现了make_request_fn的,名为dm_request,这样,所有的bio都进了函数dm_request。那dm_request又干了些什么呢?对于bio based的设备,dm_request会把bio克隆出来一份,这个clone出来的bio跟原bio是共享page的(bio和io_vec是独立的,如图,灰色部分为clone出来的部分),

bio clone
然后把clone的bio传给虚拟设备的map回调函数,而flashcache是实现了这个map函数的,名为flashcache_map。这样,flashcache就靠这一个函数处理所有的bio。

但是,我们在使用flashcache中有了新的需求:要求给不同的进程以不同的io带宽。这就需要用到blkio-controller,但是blkio-controller又需要选择io调度器为cfq,而flashcache是个bio based的设备,也就是说,它不合并bio为request,也就没io调度器什么事儿(io调度器是针对request进行排序和处理的),自然也就无法支持blkio-controller(使用block io-throttle似乎也可以控制io带宽,但是缺乏弹性)。

为了让flashcache支持blkio-controller,我们必须把它改造为request based。改动如下:

首先,dm框架提供了给开发者一个struct target_type结构

struct target_type {                                                               
        /*                                                                         
         * 3rd party driver must initialize 'features' to zero or                  
         * set to any of the Target features listed below.                         
         */                                                                        
        uint64_t features;                                                         
        const char *name;                                                          
        struct module *module;                                                     
        unsigned version[3];                                                       
        dm_ctr_fn ctr;                                                             
        dm_dtr_fn dtr;                                                             
        dm_map_fn map;                                                             
        dm_map_request_fn map_rq;                                               
......
}

如果开发者实现了其中的map方法,那么这个虚拟设备就是bio based;如果开发者实现了map_rq方法,那么这个虚拟设备就是request based(当开发者实现map_rq后,dm框架会帮新设备创建queue等一系列request型设备所需的数据结构)。我们要改造flashcache,那么就得把原先实现map方法的flashcache_map函数改成实现map_rq。
糟糕的是,dm框架对request based设备的支持不够,所以我们还在struct target_type里加了一个mk_rq方法,让虚拟设备可以自己实现“从bio合并为request”的函数。

其次,原先flashcache_map里是靠dm_io函数(dm框架提供的通用函数)把bio直接发到底层设备的,

flashcache_map
  --> flashcache_read
    --> flashcache_read_miss
      --> dm_io_async_bvec(flashcache里的bio大部分都要走这个函数)
        --> dm_io

现在不能这样了,我们修改了dm_io函数,让其可以选择不发bio而是把bio串在一个链表上返回出来。

现在逻辑清晰了。一开始,从上层来的bio都要走target_type里的mk_rq方法——我们通过flashcache_mk_rq实现,在flashcache_mk_rq里面,原先通过dm_io_async_bvec直接发送bio到底层设备现在改为把bio放入虚拟设备的queue里(这里是通过__make_request把bio往queue里放的,所以会同时放入queue和io调度器里,而且真正进入queue的是合并后的request而不再是散装的bio),之后block会根据情况(定时器超时或queue满)把虚拟设备queue里的request取走,做map_rq,然后放入对应的底层设备。

flashcache
改造后的flashcache流程

皓庭同学问了个很好的问题:调用__make_request时,flashcache虚拟设备的请求队列(queue)里面混合了对cache设备和disk设备的请求,来自于不同设备的请求会被io调度器合并吗?
答:不会的,elv_rq_merge_ok里面有专门的判断:
        /*      
         * must be same device and not a special request                           
         */                                                                        
        if (rq->rq_disk != bio->bi_bdev->bd_disk || rq->special)                   
                return 0;
所以把不同设备来的bio放入一个queue是能正常工作的。在做flashcache_map_rq时,我们才把request指向对应的底层设备。

最后,感谢皓庭同学对新版flashcache测试的支持!

flashcache原理

| | Comments (2) | TrackBacks (0)
介绍flashcache的文章很多,我就不废话了。使用上,有余峰老哥的文章;原理上,有ningoo同学的flashcache系列。但是ningoo同学漏掉了device mapper和flashcache的动态原理,只讲了静态的数据结构。我就钻个空子补充一下。

一般来说,我们对磁盘的read和write最后都会走到kernel里的submit_bio函数,也就是把io请求变成一个个的bio(bio的介绍看这里),bio是linux内核里文件系统层和block层之间沟通的数据结构(有点像sk_buffer之于网络),

bio
图片来自 http://lwn.net/Articles/26404/

到了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的结构详细介绍可以看这里 123) 

之所以要把多个bio合并成一个request,是因为机械硬盘在顺序读写时吞吐最大。如果我们换成SSD盘,合并这事儿就没那么必要了,这一点是可选的,在实现设备驱动时,厂商可以选择从kernel的queue里取request,也可以选择自己实现queue的make_request_fn方法,直接拿文件系统层传过来的bio来进行处理(ramdisk、还有很多SSD设备的firmware就是这么做的)。

block
图片来自 http://blog.csdn.net/fudan_abc/article/details/2034264

我曾经弱弱的问过涛哥:既然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的原理,还可以参考尹洋同学的文章


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
3 直接将数据写入此block,返回给用户(用户的write系统调用就可以返回了),完成后将该block的状态设为DIRTY并设置延时任务,任务内容为将cache里的内容写往disk(这样既能让用户的写请求迅速完成,又能一定程度保证数据最终被写往disk)。延时任务完成后,便可以去掉block的DIRTY标记了

在set里查找空闲的block时,如果block都是DIRTY的,那么就只能等(这些DIRTY的block要保证写往disk的,所以不能乱动),等到有某个block被写往disk并清除DIRTY标记了,再拿这个block来用。因此,flashcache还会不时的将cache的set里长期不被访问的DIRTY的block写往disk,以保证有足够多的干净的block供以后使用。这个“不时的”不是靠定时器实现的,而是通过在flashcache_write_miss、flashcache_read_miss等函数里调用flashcache_clean_set来做到的。

第二天,来自淘宝的刘峥介绍一年来ext4文件系统的开发进展。
ext4的bigalloc,增大分配单位,减少元数据,加快硬盘检查和创建大文件的速度。meta data checksum,让文件系统的错误更早更快的暴露出来,有利于数据安全和文件系统本身的稳定,但是打开checksum会造成大约20%的性能损失,这个就看用户的选择了。inline data,将小文件或目录(小于4K)的数据放入inode之中,根据统计,ext4上大约70%的目录都在100字节以内,所以这个特性对小目录众多的应用有较好的性能提升。
NO_HIDE_STALE,当用户用fallocate分配了一个大文件的磁盘空间以后,一旦他开始随机写,由于extent会从UNWRITTEN变成全零,大的extent会被切成很多小的extent,元数据会大量增加,journal会频繁写硬盘,这样性能就大大下降了;现在有了NO_HIDE_STALE,用户可以通过ioctl指定:我在从大文件里读一段内容之前一定会先写入正确的内容,没写过的部分我不会去读,这样,不再有extent的分裂,也没有了频繁的写journal,性能得到了保证。这是刘峥在跟踪TFS使用ext4后性能退化的问题时发现的改进方法。这一方法对像Oracle或IBM的DB2这样的数据库系统也是有效的。
status extent tree,将硬盘上extent tree的信息保留在内存里,这样查找和空间判断都变得简洁了。这一工作还在进行,相关patch还没有merge到ext4里。
做direct io的覆盖写时不拿i_mutex,能减少大文件多线程写时的锁竞争,对于使用SSD的数据库系统来说效果非常明显。
ext4开始支持带meta_bg的在线resize。

clsf 2012
从左到右,淘宝的马涛,淘宝的刘峥(正在喝水),百度的杨勇强,富士通的缪勰,oracle的Jeff Liu(可惜只有背影)

来自fusionio的李少华介绍了这一年来在快速存储设备上遇到的性能瓶颈及相应的对linux block层的改进。
虽然fusionio的卡很贵,但是仍有不差钱的公司采用多块fusionio卡做raid的方案(多数是金融行业),之前kernel软raid的很多代码和配置在高速PCIE设备上遇到了问题,比如机械硬盘做raid时默认stripe是512K,但是在fusionio的raid上这一size过大,不能发挥SSD随机读写的高IOPS特性,所以应该选小的stripe,当然,也不能太小,太小会造成request被分割到不同的设备上。少华在测试中发现理想的stripe大小是8K或16K。
少华提醒:SSD读是不影响寿命的,但是写会,所以要尽可能的减少对SSD的写(这大概是为什么很多人用SSD盘来做系统盘,存放可执行文件、公共库等只需读的文件);在SSD快满时,写操作的throughput会下降(主要是因为SSD自己在做gc),而适时的做trim操作(告诉SSD的firmware某块区域不再需要被使用)可以避免这个问题。
coly问:trim以后的那片区域是能保证read全零吗?
少华:没有这个保证
少华还提到,由于RAID是将多个设备聚合成一个,这个新设备只有一个flusher进程来写脏页,性能不够。吴峰光说将来有计划实现一个device多个flusher。
大家还讨论到了fusionio(也包括其它一些SSD厂商)的PCIE-flash卡支持atomic write,由firmware加硬件来保证一个写请求要么成功,要么没发生。这是个杀手级的功能,文件系统为了实现写事务,都是用的journal,不仅代码多而复杂,且对性能也有损失。如果firmware支持原子写,那文件系统完全可以把journal拿掉,专心至致的优化磁盘布局了。
其实固态存储对文件系统的冲击还不止这些,比如,SSD里其实有map来记录逻辑存储地址到实际硬件地址的信息(就是FTL,flash translate layer),而文件系统也有记录文件偏移到逻辑存储地址的信息,显然有点重复了,所以fusionio提出了directfs,一个文件一旦创建就给1T的大小,但是这个1T没有任何对应的物理地址,直到用户开始在文件里写了,才利用SSD里的那个map直接把文件偏移映射到物理存储地址,简洁高效,还带点前卫。这是不是文件系统发展的一个方向?

来自IBM的Zhiyong Wu介绍了最近他们在做的在vfs层统计热数据的工作。他们会在vfs层采集数据被读取的频率,以此算出数据的“温度”是热还是冷,并提供给文件系统层去做数据迁移。统计粒度是1MB。Coly认为统计这个对互联网企业来说最大的用处,不是数据迁移,而是找出应用的bug(运转不好的应用会多次读取热数据)。大家觉得统计所有inode的信息太费内存了,最后提供接口,可以统计某几个inode的信息。
我看介绍里有统计热数据的top200,便问这个排序是在什么时候做的,Zhiyong回答说是每次发生io时都会排序。大家觉得这显然太费了,我建议不在内核里排序,直接把数据和它们的访问频率暴露出来,运维人员一个sort -k2命令就可以自己搞定排序的事情 :)

接下来是redhat的Asias He介绍他在virtio里做的vhost-blk。有了host-blk,从guest os来的io请求会被直接map成host上的bio(以前这些guest os来的io会变成QEMU的针对host os的read和write系统调用,相当于走了两遍kernel的io路径)。大家讨论了各个公司的虚拟化方案,现在最热的云计算说白了就是虚拟机集群,xen是企业级用的最多的,一些互联网公司在尝试container的方案以获得最优的性能(如openvz、lxc),而KVM是redhat主推的(在rhel6里已经把kvm设为默认虚拟机,不再支持xen),原因是kvm毕竟是社区比较认可的虚拟化方案,而lxc等container方案安全性不够。

最后由memblaze的LuXiangFeng做业界SSD存储的报告。
memblaze是类似fusionio的国内创业公司,他们对PCIE的SSD设备做了很多软件方面的开发,提供了多种接口,既可以像磁盘一样线性访问,也可以像key/value存储那样直接put/get(这个对互联网应用挺方便的),还可以做raw device操作(比如应用直接去并行操作SSD上的不同channel,或者应用自己去做gc)。对SSD做单线程的读写是无法发挥SSD的高iops特性的,应用应该加大io并行度??或者通过启动多线程读写,或者通过aio来增加iodepth。

clsf 2012
所有参会人员合影

结语:感谢 支付宝 赞助此次CLSF讨论会

10月11日到12日两天在EMC北京办公室参加了CLSF 2012 讨论会。简要记录之,难免碎片,如有错误,欢迎拍砖。

第一天首先是南大富士通的缪勰介绍btrfs这一年的新进展。
btrfs已经具有了在线修改RAID的stripper大小、自动修复RAID1的坏盘等功能,且将meta block的size变为可调(上限64K),这样btree就矮了很多(因为node大了),在删除大文件时seek过程变少了,删除速度更快。
ext4如果发现io error会将文件系统置为read-only,而btrfs以前是没有这个能力的,今年才开始学ext4,加上了坏盘时置read-only的功能。
btrfs还增加了subvolume的quota功能,增加了send/receive对文件系统进行整体备份和恢复(这个备份是备份btrfs的那棵btree,就免得全盘备份了,但是大家觉得,好像这个功能没有太大的用户市场)
还有一些未来的优化工作:
btrfs提供强大的snapshot功能,但是如果一个文件做了太多的snapshot,那么一个node可能有很多snapshot point指向它,如果现在要对这一个node进行操作(btrfs是广泛利用COW的,所以即使是覆盖写,也要copy出来一份新的),那么还需要把多个snapshot指下来的point都改为指向新node,反向查找很费力,以后计划加入了一个中间的ref,snapshot point都指向这个ref,ref再指向node,那么如果node变了位置,只要把ref改一下就行了,不用改所有的snapshot point。
btrfs未来也会采用buddy system来管理free space(类似ext4),以减少磁盘碎片。

线下讨论我向缪勰请教了一些btrfs的问题,才知道btrfs里文件的inode、inode ref、extent等元数据是混合放在一棵fs tree里的,所以对一个文件的查找或对某文件内容的查找都是在一棵btree里,虽然btrfs会尽可能的把一个文件的相关元数据往一个node里放,但是也不保证在插入新pair时node分裂会把元数据给隔开。btrfs在性能上确实还有很多工作要做。

clsf 2012
大家在休息时间各自讨论

来自Oracle的刘博同学主要介绍了一下一年来他在文件系统性能优化方面积累的一些经验和工具。
Chris Mason的seekwatcher是跟踪io性能的好工具,但是它只能处理单硬盘的io数据,所以Chris现在打算开发更先进的iowatcher,能够处理多块硬盘,还能看到io的latency和cpu状态,提供的信息更综合。比如,btrfs在写时throughput还行,但是latency较大(写很多小文件时尤其明显),所以怎么观察和优化这些latency迫切需要强大工具的支持。
刘博同学还提到了ftrace,而我们推荐了朱辉开发的kgtp,可以实时在线调试(很方便我们这些经常需要查线上kernel bug的苦逼码农)且延时很小。但是kgtp一直没有进upstream,淘宝kernel自己backport了这部分代码。
Asias He同学问我们测文件系统用哪些工具,其实我们组根本连一个QE都没有(不能跟redhat、SUSE、Oracle这些做发行版的公司比),文件系统都是靠开发人员自己测(自己吃自己的狗粮,也挺好),我们把iozone、fsmark、xfstest、postmark等一堆开源测试工具用脚本攒在一起(“要你命三千”),对着文件系统一顿高压力的狂轰乱炸几天几夜,如果没有panic、也没有“block 120 seconds“,就算是过了。当然,功能测试还需要开发自己另外写程序来模拟。

接下来是Oracle的Jeff Liu介绍一年来xfs的开发进展。
在各大公司的运维人员和DBA中,都流传着“xfs文件系统对大文件的性能最好”的传说。但毕竟只是传说,coly给出了一个故事,还是比较有说服力的:ext4文件系统刚开发出来的时候(好像是2006年前后),很多评测结果都好于xfs(包括大文件),那时xfs是输在了journal的性能上;两三年后,xfs改进了journal的性能,评测结果超过了ext4;然后,ext4开始学习xfs的部分优化方法,性能又超过了xfs....所以,实际在upstream里,这两个文件系统的性能是在互相追赶,并没有一个权威的标准说明谁比谁更好或者更不好。
ext4和xfs对文件都是使用btree,从设计的角度来谁也没有压倒性的优势,所以关键还看细节处的优化。所以我不完全赞同“架构决定性能”,架构是决定了性能的“天花板”,但是最后能不能到达这个天花板,还看具体开发人员的努力。我觉得架构师们都是非常聪明的人,而好的架构往往是高度相似的,所以靠架构领先来取得压倒性优势并不常见(大家看看现在各大互联网公司的所谓“分布式存储”,是不是架构都长得很相似?架构都相似了,性能也不会差特别大),常见的还是靠在细节处修bug、找性能瓶颈的默默无闻的码农来一点一点的优化,最终靠高稳定、高性能、高便捷来赢得用户。所以,传说中的“xfs性能好于ext4”值得商榷。
xfs最初是从SGI UNIX里移植到linux上来的,当时linux的vfs层做的不好,所以移植过来的xfs代码也包括了一些本来该vfs做的事情,所以linux里的xfs代码非常庞大(10万行),维护是个很大的负担。所以xfs最近的工作都集中在精简代码上,很多patch都是在做删代码的工作。一个很典型的例子:xfs的writeback函数调用路径特别深,linux kernel提供的4K大小的stack根本不够用,于是想出了一个神办法,就是在函数调用路径的中间停下来,把剩下的调用工作放入work queue,让剩下的操作稍后由延时任务自己来完成。这样stack才不会被撑爆。

来自fusionio的李少华:我在测试中发现work queue的扩展性很差,你如果太频繁的往里面放东西,性能是会急剧退化的,而且把work放入queue的CPU和以后将执行此work的CPU很可能不是同一个,这将影响CPU的cache。
Jeff Liu:这些锁竞争和CPU级的速度损失,跟IO的性能相比,不算什么的,一次写盘或读盘,几个毫秒都过去了,省那几百个微秒并不紧要

然后是腾讯的冯小天介绍腾讯内核组的工作。
腾讯的新内核是基于mainline的2.6.32内核,虚拟机用xen,而目前的云平台是用的container。基于mainline带来了很多bug,一路踩了不少雷,要是一开始基于rhel6的2.6.32内核就好了(看来我们是幸运的)。小天在推广新内核上也遇到很多困难:业务方不肯升级OS,甚至重启机器都不愿意,尤其是208天的那个bug搞得腾讯内核组非常郁闷。未来,准备利用腾讯上SSD盘的机会把ext4也推上线。
Coly提到了淘宝CDN使用的SSD盘坏得很少,其实比机械盘耐用。

clsf 2012
中间穿蓝色衣服的是来自腾讯的冯小天

淘宝的余峰介绍了淘宝在使用mysql中对linux文件系统和存储层提出了新的要求。
目前mysql使用的innodb引擎会造成短时的大量IO,性能会有抖动,考虑换成levelDB的LSM引擎。现在SSD的出现让pc server的IO能力变得过剩,通过在一台服务器上启动多个mysql实例来更加充分的利用IO资源。目前SSD卡似乎没有接口看“使用寿命还剩多少”,等出了问题再换盘就麻烦了,来自memblaze的唐志波回答说,一般SSD写10 P的数据(或者使用3年)就会有较多错误出现,而厂家一般是到3年就要给更换的。

最后是EMC的Xuezhao Liu介绍lustre client。最近他们和whamcloud合作,计划把lustre的client推到kernel code里,Peng Tao今年4月份参加LSF的时候提出了这件事,社区表示欢迎,当然,前提是清理代码让其符合kernel code style。推进的工作除了清理代码、整理格式,还要将client部分从lustre代码中剥离出来。
问:lunstre client代码有多少?
答:里面光是通信方面就有三万行代码,总共十万行。
大家惊了。一次往kernel里进十万行代码,这还是相当不容易的。