IT技术博客大学习 共学习 共进步
全部 移动开发 后端 数据库 AI 算法 安全 DevOps 前端 设计 开发者

深入浅出Flashcache(一)

NinGoo.net 2012-02-05 23:27:46 累计浏览 3,264 次
本机暂存

    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

    同分类推荐文章

    1. Vibe新开源项目 - Vaala AI Gateway (2026-05-17 02:10:19)
    2. SmartPerfetto 架构文章 Q&A:8 个深度技术问答 (2026-04-10 11:00:00)
    3. 让 AI 把我的 PHP 博客重写成 Go (2026-03-27 18:33:54)

    查看更多 后端 文章 →

    建议继续学习

    1. 用Hyer来进行网站的抓取 (累计阅读 158,168)
    2. MySQL数据库在实际应用一些方面的介绍 (累计阅读 36,327)
    3. WordPress插件开发 -- 在插件使用数据库存储数据 (累计阅读 29,087)
    4. Mysql监控指南 (累计阅读 21,220)
    5. Linux如何统计进程的CPU利用率 (累计阅读 16,203)
    6. 由浅入深探究mysql索引结构原理、性能分析与优化 (累计阅读 16,195)
    7. 我的 RHCA 之路 (累计阅读 13,926)
    8. Linux内存点滴 用户进程内存空间 (累计阅读 13,052)
    9. 给程序员新手的一些建议 (累计阅读 13,024)
    10. 在Apache2.2.XX下安装Mod-myvhost模块 (累计阅读 12,983)