Chinaunix首页 | 论坛 | 博客
  • 博客访问: 160761
  • 博文数量: 17
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 342
  • 用 户 组: 普通用户
  • 注册时间: 2014-03-19 11:38
个人简介

A ZFS fan

文章分类
文章存档

2014年(17)

分类: 服务器与存储

2014-04-24 21:33:20

     在计算机系统中,处理快设备与慢设备协调工作时,往往会在他们之间添加一个缓存设备。缓存设备的速度与快设备相当,但是空间较慢设备要小很多。其实缓存算法主要考虑如何管理缓存以及淘汰策略,因为缓存空间远比硬盘小,那么当缓存空间被填满之后,还需要读取数据该怎么办?只能把已经缓存的部分数据淘汰掉,将新的数据缓存进来。这就存在一个问题:把什么样的数据淘汰出去?淘汰策略设计得当,可以让经常访问的数据一直留在缓存中,大大提高系统的运行效率;设计的不好,可能需要不停地在缓存中替换数据,甚至降低系统的效率。


1. ARC缓存算法简介

在读缓存方面,ZFS采用了ARC算法,这里首先简单描述一下ARC算法,详细说明可以参考这篇文章:

ARC缓存算法使用四个链表,分别为:


  • 最近使用链表(MRU)
  • 最常使用链表(MFU)
  • 从最近使用链表中淘汰出来的页构成的链表(ghost MRU)
  • 从最常使用链表中淘汰出来的页构成的链表(ghost MFU)

其中,ghost链表并不用来缓存数据,只用来存储索引,但是如果命中ghost链表中的数据时,将要采取特殊的处理。
当第一次读取一个数据页时,将它放到MRU链表中,第二次读取新的缓存页的时候,要放到MRU链表的最前面(它是最近访问的),当命中MRU链表中数据页的时候,将该页的索引调整到MFU中。如果第二次命中MFU中的缓存时,将该缓存页调整到MFU链表的开头。

根据这样的规则,随着时间流逝,缓存空间最终会被填满。两个链表(MRU,MFU)都将会被填满。这时如果再读近一个没有被缓存的页面,如果这个页面没有被ghost链表缓存,那么就直接将链表末尾的缓存淘汰掉,将新的数据缓存。被淘汰的数据缓存放到对应的ghost链表中。那么如果读入的数据在ghost链表中被命中了呢?

如果命中了ghost MRU中的页,那么说明此时的MRU链表有点小了,需要将MRU链表增加一个,同时减少MFU链表空间;反之亦然,如果命中了ghost MFU链表中的页,那么说明此时的MFU链表小了,需要扩大,同时减小MRU链表空间。

通过这样的设计,系统可以自动地适应当前环境的变化,自动调节MRU和MFU的大小,获得更好的缓存效果。比如,我们查看一个很大的日志,需要连续读入不同的数据。普通的缓存算法可能要将数据都加载进缓存,从而会淘汰有用的缓存数据,而ARC则不同,它只会影响到MRU,而MFU缓存空间不变,这样就使得大部分有用的数据保留下来,保证缓存空间不被污染。

2. ZFS缓存与原始ARC算法的不同点

ZFS 的ARC实现与原始的ARC在以下三点有所不同:

  1. 原始ARC算法设计上,所有的页都是可以被淘汰的,缓存中的页不能被锁定在内存中。这样就使得淘汰算法变得简单:只需要淘汰列表中最后一页即可。但是ZFS的ARC实现就不这么简单了。在任意一个时刻,缓存中的一个子集是不可被淘汰的,因为ZFS对他们有引用,只有那些没有额外活动的引用的块才可以被淘汰。
  2. 这也导致在某些时刻,无法从缓存中淘汰所需大小的。在这种情况下,我们不能调整缓存的大小。为了防止缓存无限制地增长,ZFS设计了缓存“瓶颈”,用来在缓存中有足够空间之前减缓新的数据流入缓存。
  3. 原始的ARC算法的缓存大小是固定的,当缓存空间满了之后,就会有缓存页被替换出去,这样就常发生缓存页缺失。ZFS的采用动态大小的缓存模型。缓存大小随着使用率提高而增加,当然这种增加是根据系统对内存的使用来衡量的。当操作系统使用内存紧张的情况下,ZFS会减少缓存空间的大小。
  4. 原始ARC算法缓存页的大小也是固定的。所有的缓存都以同样大小存在缓存中。当缓存数据没有命中时,只能简单地将一个页替换出去。ZFS的缓存模型中,使用可变大小的缓存块(512字节到128K)。当没有命中的情况发生时,ZFS选择一组缓存块,将他们淘汰。被淘汰的空间大小尽可能与新的缓存大小相同。

3. ZFS ARC缓存的设计与实现


OK,下面我们来详细说明一下ZFS的ARC缓存实现

3.1 Buffer状态定义


首先,ZFS在实现是通过为每个缓存块定义状态来实现:

点击(此处)折叠或打开

  1. typedef enum arc_buf_contents {
  2.     ARC_BUFC_DATA,                /* buffer contains data */
  3.     ARC_BUFC_METADATA,            /* buffer contains metadata */
  4.     ARC_BUFC_NUMTYPES
  5. } arc_buf_contents_t;


  6. typedef struct arc_state {
  7.     list_t    arcs_list[ARC_BUFC_NUMTYPES];    /* list of evictable buffers */
  8.     uint64_t arcs_lsize[ARC_BUFC_NUMTYPES];    /* amount of evictable data */
  9.     uint64_t arcs_size;    /* total amount of data in this state */
  10.     kmutex_t arcs_mtx;
  11. } arc_state_t;

  12. /* The 6 states: */
  13. static arc_state_t ARC_anon;
  14. static arc_state_t ARC_mru;
  15. static arc_state_t ARC_mru_ghost;
  16. static arc_state_t ARC_mfu;
  17. static arc_state_t ARC_mfu_ghost;
  18. static arc_state_t ARC_l2c_only

我们可以发现以下两点:

1)这里定义了六种状态;
中间四个很好理解,那么ARC_anon和ARC_l2c_only代表什么?

  • ARC_anon:这种类型的buffer表示它们没有与DVA相关联,这些buffer中的数据是修改后的数据块,但是还没有被写入磁盘。它们首先是被放到MRU链表中,当被分配了DVA,写入磁盘之后就会被移到MFU链表中
  • ARC_l2c_only:这种类型的buffer并不存在MFU或是MRU链表中,而是存在于二级缓存中(二级缓存主要通过hash表缓存数据头信息)。

2)每种状态有两个链表。
ARC_BUFC_NUMTYPES=2,从定义也很容易看出,两种链表分别缓存元数据以及普通数据。


3.2 ARC Buffer定义


ARC在缓存过程中,数据部分存储使用arc_buf结构体表示,而头部信息存储arc_buf_hdr结构体中。详细定义如下:

点击(此处)折叠或打开

  1. struct arc_buf {
  2.     arc_buf_hdr_t        *b_hdr;
  3.     arc_buf_t        *b_next;
  4.     kmutex_t        b_evict_lock;
  5.     void            *b_data;
  6.     arc_evict_func_t    *b_efunc;
  7.     void            *b_private;
  8. };

  9. struct arc_buf_hdr {
  10.     /* protected by hash lock */
  11.     dva_t            b_dva;     // DVA信息
  12.     uint64_t        b_birth;
  13.     uint64_t        b_cksum0;

  14.     kmutex_t        b_freeze_lock;
  15.     zio_cksum_t        *b_freeze_cksum;
  16.     void            *b_thawed;

  17.     arc_buf_hdr_t        *b_hash_next;
  18.     arc_buf_t        *b_buf;    // header对应的buf指针
  19.     uint32_t        b_flags;
  20.     uint32_t        b_datacnt;

  21.     arc_callback_t        *b_acb;
  22.     kcondvar_t        b_cv;

  23.     /* immutable */
  24.     arc_buf_contents_t    b_type;  // 数据部分缓存的类型(元数据/普通数据)
  25.     uint64_t        b_size;        // 缓存数据大小
  26.     uint64_t        b_spa;

  27.     /* protected by arc state mutex */
  28.     arc_state_t        *b_state;
  29.     list_node_t        b_arc_node;

  30.     /* updated atomically */
  31.     clock_t            b_arc_access;  // 当前ARC缓存最近访问时间

  32.     /* self protecting */
  33.     refcount_t        b_refcnt;

  34.     l2arc_buf_hdr_t        *b_l2hdr;
  35.     list_node_t        b_l2node;
  36. };

从定义中很明显可以看出,arc_buf中仅仅存储了数据缓存的数据,以及该部分缓存对应的arc_buf_hdr指针,至于缓存数据的详细信息都是存储在arc_buf_hdr中的,比如数据对应的DVA信息,缓存数据的大小信息,回调函数等等。

3.3 关键函数说明


淘汰:static void arc_evict(arc_state_t *state, uint64_t spa, int64_t bytes, boolean_t recycle, arc_buf_contents_t type)
从缓存中淘汰指定大小的空间(在第2节中已经说明了,ZFS在ARC缓存实现过程中采用的是不同大小的块,要获取指定大小的缓存空间需要在缓存空间中多次查找,淘汰出所需的缓存空间),淘汰策的主要流程如下:



Arc 读操作
对应函数如下:
int arc_read(zio_t *pio, spa_t *spa, const blkptr_t *bp, arc_done_func_t *done, void *private, zio_priority_t priority, int zio_flags, uint32_t *arc_flags, const zbookmark_t *zb)
ARC读操作主要是通过缓存来读取指定DVA的block中的数据。如果要读取的block在缓存中存在(命中),则调用给定的回调函数,之后立刻返回,此时回调函数中的I/O指针位NULL,因为数据在缓存中,并不需要额外的I/O操作。如果要读取的block不在缓存中(Miss),则需要通过特定的回调函数来传给spa一个请求,将要读取的block添加到缓存中。

如果要读取的block由于个读进程正在处理中,有以下三种处理方式:
  1. 等待该读进程处理完毕,之后将数据读取的结果;
  2. 如果给定了“done”对应的函数,当读取完毕之后调用该函数;
  3. 直接返回

arc_read对应的概要流程图如下

arc_read流程图


ZFS采用的普遍认为效果最好的ARC算法。使得ZFS文件系统的性能有了很好的保证。这里只是总体上介绍了一下ZFS中ARC的实现。有兴趣可以去读源码中的arc.h、arc.c两个文件,这样可以对ZFS中ARC的实现有更好的认识。

本文乃nnusun原创,请勿转载,如需转载请详细标明出处
阅读(6570) | 评论(0) | 转发(0) |
0

上一篇:ZFS - 数据完整性

下一篇:ZFS - 存储池管理

给主人留下些什么吧!~~