深入浅出Flashcache(一)

Cache is king.

在计算机系统中,cache的魔爪无处不在。CPU中有L1,L2,甚至L3 cache;Linux有pagecache,MySQL有buffer cache/query cache;IO系统中Raid卡/磁盘也有cache;在大型互联网系统中,数据库前面一般也都有一层memcache。Cache是容量与性能之前取平衡的结果,以更低的成本,获得更高的收益,是系统设计时应该遵循的原则。

传统机械硬盘几十年来,容量不断翻倍的增长,相比较而言,性能的增长就慢的像蜗牛了。对于依赖IO性能的应用,典型的如数据库,一直在等待新的技术来拯救。在此之前,身躯庞大的高端存储,动辄重达几吨。相比于存储里带的硬盘来说,价格贵得离谱,而存储的附加价值,在于io在大量硬盘之间的均衡分布,以及IO链路的多路容灾,以及部分固件层面的优化和数据保护等。

Flash disk(SSD/FusionIO等)的出现,改变了这一切。Flash disk将硬盘从机械产品变成了电气产品,功耗更小,性能更好,时延更优,看起来传统硬盘已经不堪一击,数据库欢欣鼓舞,新的革命似乎将一夕成功。但新东西也有它致命的缺陷,价格和经过时间检验的稳定性。

所以Facebook的Mohan Srinivasan在2010年开源了Flashcache,将Flash disk做为普通硬盘的cache,这个思路,目前一些尝试也在raid卡硬件层面做尝试,例如LSI的CacheCade Pro,不过之前版本新浪的童鞋测试过似乎性能没有想象的好。Flashcache在淘宝一些核心数据库中已经在线运行了大半年,经过调优后的表现稳定。Flashcache利用了Linux的device mapping机制,将Flash disk和普通硬盘的块设备做了一层映射,在OS中变现为一块普通的磁盘,使用简单,是一个值得推荐的方案。Flashcache最初的实现是write backup机制cache,后来又加入了write through和write around机制:

  • write backup: 先写入到cahce,然后cache中的脏块会由后台定期刷到持久存储。
  • write through: 同步写入到cache和持久存储。
  • write around: 只写入到持久存储。

在详细的介绍Flashcache之前,需要先了解一下Linux的block device和device mapper相关的知识。

1. Block Device
块设备最初主要是依据传统硬盘等IO操作较慢的设备而设计的,所以Linux中为块设备的IO操作提供了cache层,所以基于块设备的请求一般是buffer io,当然后来由于数据库等自己有cache机制的应用,os/fs层面的cache就成了多余,所以出现了绕过os/fs层cache的direct io。

块设备在设备确定层和kernel之间,为Kernel提供了统一的IO操作接口,同时隐藏了不同硬件设备的细节。当有多个并发IO请求到块设备时,请求的顺序会影响IO的性能,因为普通的机械硬盘需要移动机械臂,所以kernel一般会对IO做排序等调度后再发送到块设备层。IO调度算法是一种电梯算法(elevator algorithm),目前主要有cfq/deadline/anticipatory/noop,其中cfq是Linux的默认策略;anticipatory在新的内核中已经放弃;deadline在大部分OLTP数据库应用中更具优势,IO的响应时间更稳定些;noop只对IO请求进行简单的合并,其他不干涉,在FusionIO等IO性能很好的设备上,noop反而更具优势,所以FusionIO的驱动默认使用了noop。关于IO Scheduler,后文会有更详细的解释。

块设备在用户空间是一种特殊的文件类型,由(major,minor)来标识,major区分disk,minor区分partition。Linux中一般把设备文件放在/dev目录。实际上你完全可以将块设备文件创建到其他地方,只要(major,minor)唯一确定,块设备文件最后访问的起始同一个块设备。

$ls -l /dev/sda1
brw-rw—- 1 root disk 8, 1 2011-12-03 01:00 /dev/sda1

$sudo mknod /opt/sda1 b 8 1

$ls -l /opt/sda1
brw-r–r– 1 root root 8, 1 2011-12-03 11:54 /opt/sda1

由于块设备处于文件系统和物理设备驱动之间,在这一层做一些工作可以对所有IO产生影响,因此很多优秀的产品都在这一层做文章,除了Flashcache,还有一个比较著名的就是DRBD(DRBD已经进入2.6.33内核)。

在Linux内核中定义了一些操作块设备相关的结构体和函数,下面的信息基于2.6.32.49:

1.1 gendisk

gendisk保存了一个具体的disk的信息,包括该disk上的请求队列,分区列表/第一个分区,块设备操作表等重要信息。

struct gendisk {
    struct request_queue *queue; 
    struct disk_part_tbl *part_tbl;
    struct hd_struct part0;
    const struct block_device_operations *fops;
    ...
};

1.2 hd_struct

hd_struct保存一个分区信息,包括起始扇区,扇区数,分区号等基本信息。

struct hd_struct {
    sector_t start_sect;
    sector_t nr_sects;
    int      partno;
    ...
};

1.3 disk_part_tbl

disk_part_tbl保存磁盘分区表的信息

struct disk_part_tbl {
    struct rcu_head rcu_head;
    int len;
    struct hd_struct *last_lookup;
    struct hd_struct *part[];
};

1.4 block_device

block_device可以是整个磁盘,也可以是一个分区。如果是一个分区块设备,则bd_contains会指向分区所在磁盘的block_device,bd_part则指向分区信息结构hd_struct。。

struct block_device {
    dev_t                    bd_bdev;
    struct inode            *bd_inode;
    struct list_head         bd_inodes;
    struct super_block      *bd_super;
    struct block_device     *bd_contains;
    struct gendisk          *bd_disk;
    struct hd_struct        *bd_part;
    struct list_head         bd_list;
    struct backing_dev_info *bd_inode_backing_dev_info;
    ...
};

1.5 buffer_head

顾名思义,在内核层对块设备的IO请求是以块为单位的。buffer_head是一个块在内存中的元数据信息。b_data指向该块数据的实际地址。b_this_page则将通过一page中的块连接起来。以前版本的buffer_head是fs到block device的io请求单元,现在已经改为bio了。

struct buffer_head {
    unsigned long        b_state;
    struct buffer_head   *b_this_page;
    char                 *b_data;
    sector_t              blocknr;
    struct block_device  *b_bdev;
    bh_end_io_t          *b_end_io;
    ...
};

1.6 bio

bio封装了一次实际的块设备io请求。这是块设备io请求的基本单位。bi_vcnt表示bio_vec的数目。

struct bio {
    sector_t             bi_sector;
    struct bio          *bi_next;
    struct block_device *bi_bdev;
    unsigned short       bi_vcnt;
    unsigned short       bi_idx;
    struct bio_vec      *bi_io_vec;
    ...
};

1.7 bio_vec

bio_vec表示一次bio涉及到的数据片段(segment),由所在内存页地址,长度,偏移地址等定位。一次bio一般包含多个segment。

struct bio_vec {
    struct page            *bv_page;
    unsigned int            bv_len;
    unsigned int            bv_offset;
};

1.8 request

块设备层IO等待请求(pending I/O request)。内核中的bio请求在经过io调度排序后进入块设备层,会尝试合并到已有的requst。bio结构中的bi_next将队列中的bio请求串成一个队列。bio/biotail域指向队列的首尾。

struct request {
    struct list_head            queuelist;
    struct bio                 *bio;
    struct bio                 *biotail;
    void                       *elevator_private;
    void                       *elevator_private2;
    struct gendisk             *rq_disk;
    request_queue_t            *q;
    ...
};

1.8 request_queue

request_queue维护块设备层IO请求队列,队列中包含多个request。request_queue同时定义了处理队列的函数接口,不同的设备注册时需要实现这些IO处理接口。

struct request_queue {
    struct list_head            queue_head;
    struct request             *lastmerge;
    elevator_t                 *elevator;
    struct request_list         rq;
    request_fn_proc            *request_fn;
    make_request_fn            *make_request_fn;
    prep_rq_fn                 *prep_rq_fn;
    unplug_fn                  *unplug_fn;
    merge_bvec_fn           *merge_bvec_fn;
    prepare_flush_fn        *prepare_flush_fn;
    softirq_done_fn         *softirq_done_fn;
    rq_timed_out_fn         *rq_timed_out_fn;
    dma_drain_needed_fn     *dma_drain_needed;
    lld_busy_fn             *lld_busy_fn;
    struct blk_trace           *blk_trace;
    ...
};

1.9 submit_bh

submit_bh是内核发送IO请求给块设备的函数,目前较新版本的内核中该函数会调用submit_bio执行实际请求。

int submit_bh(int rw, struct buffer_head * bh)
{
    struct bio *bio;
    int ret = 0;
    ...
    bio = bio_alloc(GFP_NOIO, 1);

    bio->bi_sector = bh->b_blocknr * (bh->b_size >> 9);
    bio->bi_bdev = bh->b_bdev;
    bio->bi_io_vec[0].bv_page = bh->b_page;
    bio->bi_io_vec[0].bv_len = bh->b_size;
    bio->bi_io_vec[0].bv_offset = bh_offset(bh);

    bio->bi_vcnt = 1;
    bio->bi_idx = 0;
    bio->bi_size = bh->b_size;

    bio->bi_end_io = end_bio_bh_io_sync;
    bio->bi_private = bh;
    bio_get(bio);
    submit_bio(rw, bio);
    if (bio_flagged(bio, BIO_EOPNOTSUPP))
        ret = -EOPNOTSUPP;

    bio_put(bio);
    return ret;   
}

1.10 submit_bio

submit_bio函数会调用generic_make_request执行实际的bio请求。generic_make_request则循环处理bio链表,针对每个bio调用内联函数__generic_make_request来做处理。__generic_make_request则最终调用request_queue中的make_request_fn处理函数处理实际的IO请求。

void submit_bio(int rw, struct bio *bio)
{
    ...
    generic_make_request(bio);
}

...
static inline void __generic_make_request(struct bio *bio)
{
    struct request_queue *q;
    int ret;
    ...
    do{
      q = bdev_get_queue(bio->bi_bdev);
      ... 
      ret = q->make_request_fn(q, bio);
    }while(ret);
    ...
    
}

2. Block device相关的工具

Linux提供了一些工具来操作和查看块设备,如果你的系统中没有,可以安装最新版本的util-linux-ng来获得,实际上很多常用的工具都是出自整个工具集,本文后续也会用到其中一些有意思的工具。

2.1 lsblk

RHEL6.1中已经带有该工具。下面是一台已经配置好Flashcache的机器上执行的结果:

$lsblk
NAME                MAJ:MIN RM   SIZE RO MOUNTPOINT
sda                     8:0    0   200G  0 
├─sda1                8:1    0   128M  0 /boot
├─sda2                8:2    0  14.7G  0 /
├─sda8                8:8    0     2G  0 [SWAP]
sdb                     8:16   0   1.5T  0 
└─sdb1                8:17   0   1.5T  0 
  └─cachedev (dm-0) 253:0    0   1.5T  0 /opt
fioa                  252:0    0 300.4G  0 
└─cachedev (dm-0)   253:0    0   1.5T  0 /opt

2.2 blkid

blkid可以块设备的属性,不带参数也会列出系统中所有的块设备。

$ sudo blkid
/dev/sda1: UUID="0ff3ff63-d214-4d32-8633-66a4333fece9" TYPE="ext4" 
/dev/sda6: UUID="d328b838-9043-438d-81b8-6a96454def3c" TYPE="swap" 

2.3 blockdev

blockdev,不仅可以查看,也可以设置块设备的一些属性。

$ blockdev

用法:
  blockdev -V
  blockdev --report [devices]
  blockdev [-v|-q] commands devices

可用的命令:
	--getsz                        获得512字节的段大小
	--setro                        设置只读
	--setrw                        设置读写
	--getro                        获得只读
	--getss                        get logical block (sector) size
	--getpbsz                      get physical block (sector) size
	--getiomin                     get minimum I/O size
	--getioopt                     get optimal I/O size
	--getalignoff                  get alignment offset
	--getmaxsect                   get max sectors per request
	--getbsz                       获得块大小
	--setbsz BLOCKSIZE             设置块大小
	--getsize                      获得 32-bit 段数量
	--getsize64                    获得字节大小
	--setra READAHEAD              设置 readahead
	--getra                        获取 readahead
	--setfra FSREADAHEAD           设置文件系统 readahead
	--getfra                       获取文件系统 readahead
	--flushbufs                    刷新缓存
	--rereadpt                     重新读取分区表

2.4 fdisk

当然常用的fdisk也是管理块设备的利器。

$ sudo fdisk -l /dev/sda4

Disk /dev/sda4: 136.5 GB, 136492089344 bytes
255 heads, 63 sectors/track, 16594 cylinders
Units = cylinders of 16065 * 512 = 8225280 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk identifier: 0x00000000

2.5 blktrace

blktrace是跟踪块设备IO请求情况的利器。

blktrace is a block layer IO tracing mechanism which provides detailed information about request queue operations up to user space.

核心系统部的褚霸童鞋详细的介绍了这个个工具,有兴趣的移步这里这里,还有这里

2.6 lscpu

顺带说一下,lscpu也是一个很有用的工具,下面是2路intel L5630的主机上打印出来的信息,L5630是intel的低功耗CPU,额定功率只有常用的x5620的一半左右。

$sudo lscpu
Architecture:          x86_64
CPU op-mode(s):        32-bit, 64-bit
Byte Order:            Little Endian
CPU(s):                16
On-line CPU(s) list:   0-15
Thread(s) per core:    2
Core(s) per socket:    4
CPU socket(s):         2
NUMA node(s):          2
Vendor ID:             GenuineIntel
CPU family:            6
Model:                 44
Stepping:              2
CPU MHz:               2127.973
BogoMIPS:              4255.85
Virtualization:        VT-x
L1d cache:             32K
L1i cache:             32K
L2 cache:              256K
L3 cache:              12288K
NUMA node0 CPU(s):     0,2,4,6,8,10,12,14
NUMA node1 CPU(s):     1,3,5,7,9,11,13,15

未完待续

参考:
[1]. Linux Block Device Architecture
[2]. Block devices and volume management in Linux



无觅相关文章插件,快速提升流量

4条评论

  • At 2011.12.03 16:55, 深入浅出Flashcache(二) said:

    […] 前文简单的介绍了block device,别急,虽然这个系列的主要目的是介绍Flashcache,这一篇还是不会切入正题,因为我们还需要先了解下什么是device mapper。 […]

    • At 2011.12.10 01:52, 深入浅出Flashcache(三) said:

      […] 前文简单介绍了block device和device mapper。有了这两个基础,再来看flashcache的代码,就容易理解多了。Flashcache是一个内核模块,要更清晰的理解代码,还需要了解一下内核模块编写的一些基础知识。好吧,虽然对于内核编程我完全是个门外汉,这里还是需要现学现卖下。所以这一篇还是不会切入正题,已经熟悉Linux内核模块的同学请忽略并耐心等待。 […]

      • At 2011.12.23 23:53, yangdehua said:

        write backup: 先写入到cahce,然后cache中的脏块会由后台定期刷到持久存储。
        cahce笔误了,应该是cache

        • At 2013.09.02 19:02, mroff said:

          blockdev这个工具不错,但ubuntu的repo里没有。


          (Required)
          (Required, will not be published)