在计算机系统中,处理快设备与慢设备协调工作时,往往会在他们之间添加一个缓存设备。缓存设备的速度与快设备相当,但是空间较慢设备要小很多。其实缓存算法主要考虑如何管理缓存以及淘汰策略,因为缓存空间远比硬盘小,那么当缓存空间被填满之后,还需要读取数据该怎么办?只能把已经缓存的部分数据淘汰掉,将新的数据缓存进来。这就存在一个问题:把什么样的数据淘汰出去?淘汰策略设计得当,可以让经常访问的数据一直留在缓存中,大大提高系统的运行效率;设计的不好,可能需要不停地在缓存中替换数据,甚至降低系统的效率。
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在以下三点有所不同:
-
原始ARC算法设计上,所有的页都是可以被淘汰的,缓存中的页不能被锁定在内存中。这样就使得淘汰算法变得简单:只需要淘汰列表中最后一页即可。但是ZFS的ARC实现就不这么简单了。在任意一个时刻,缓存中的一个子集是不可被淘汰的,因为ZFS对他们有引用,只有那些没有额外活动的引用的块才可以被淘汰。
-
这也导致在某些时刻,无法从缓存中淘汰所需大小的。在这种情况下,我们不能调整缓存的大小。为了防止缓存无限制地增长,ZFS设计了缓存“瓶颈”,用来在缓存中有足够空间之前减缓新的数据流入缓存。
-
原始的ARC算法的缓存大小是固定的,当缓存空间满了之后,就会有缓存页被替换出去,这样就常发生缓存页缺失。ZFS的采用动态大小的缓存模型。缓存大小随着使用率提高而增加,当然这种增加是根据系统对内存的使用来衡量的。当操作系统使用内存紧张的情况下,ZFS会减少缓存空间的大小。
-
原始ARC算法缓存页的大小也是固定的。所有的缓存都以同样大小存在缓存中。当缓存数据没有命中时,只能简单地将一个页替换出去。ZFS的缓存模型中,使用可变大小的缓存块(512字节到128K)。当没有命中的情况发生时,ZFS选择一组缓存块,将他们淘汰。被淘汰的空间大小尽可能与新的缓存大小相同。
3. ZFS ARC缓存的设计与实现
OK,下面我们来详细说明一下ZFS的ARC缓存实现
3.1 Buffer状态定义
首先,ZFS在实现是通过为每个缓存块定义状态来实现:
-
typedef enum arc_buf_contents {
-
ARC_BUFC_DATA, /* buffer contains data */
-
ARC_BUFC_METADATA, /* buffer contains metadata */
-
ARC_BUFC_NUMTYPES
-
} arc_buf_contents_t;
-
-
-
typedef struct arc_state {
-
list_t arcs_list[ARC_BUFC_NUMTYPES]; /* list of evictable buffers */
-
uint64_t arcs_lsize[ARC_BUFC_NUMTYPES]; /* amount of evictable data */
-
uint64_t arcs_size; /* total amount of data in this state */
-
kmutex_t arcs_mtx;
-
} arc_state_t;
-
-
/* The 6 states: */
-
static arc_state_t ARC_anon;
-
static arc_state_t ARC_mru;
-
static arc_state_t ARC_mru_ghost;
-
static arc_state_t ARC_mfu;
-
static arc_state_t ARC_mfu_ghost;
-
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结构体中。详细定义如下:
-
struct arc_buf {
-
arc_buf_hdr_t *b_hdr;
-
arc_buf_t *b_next;
-
kmutex_t b_evict_lock;
-
void *b_data;
-
arc_evict_func_t *b_efunc;
-
void *b_private;
-
};
-
-
struct arc_buf_hdr {
-
/* protected by hash lock */
-
dva_t b_dva; // DVA信息
-
uint64_t b_birth;
-
uint64_t b_cksum0;
-
-
kmutex_t b_freeze_lock;
-
zio_cksum_t *b_freeze_cksum;
-
void *b_thawed;
-
-
arc_buf_hdr_t *b_hash_next;
-
arc_buf_t *b_buf; // header对应的buf指针
-
uint32_t b_flags;
-
uint32_t b_datacnt;
-
-
arc_callback_t *b_acb;
-
kcondvar_t b_cv;
-
-
/* immutable */
-
arc_buf_contents_t b_type; // 数据部分缓存的类型(元数据/普通数据)
-
uint64_t b_size; // 缓存数据大小
-
uint64_t b_spa;
-
-
/* protected by arc state mutex */
-
arc_state_t *b_state;
-
list_node_t b_arc_node;
-
-
/* updated atomically */
-
clock_t b_arc_access; // 当前ARC缓存最近访问时间
-
-
/* self protecting */
-
refcount_t b_refcnt;
-
-
l2arc_buf_hdr_t *b_l2hdr;
-
list_node_t b_l2node;
-
};
从定义中很明显可以看出,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由于个读进程正在处理中,有以下三种处理方式:
-
等待该读进程处理完毕,之后将数据读取的结果;
-
如果给定了“done”对应的函数,当读取完毕之后调用该函数;
-
直接返回
arc_read对应的概要流程图如下
arc_read流程图
ZFS采用的普遍认为效果最好的ARC算法。使得ZFS文件系统的性能有了很好的保证。这里只是总体上介绍了一下ZFS中ARC的实现。有兴趣可以去读源码中的arc.h、arc.c两个文件,这样可以对ZFS中ARC的实现有更好的认识。
本文乃nnusun原创,请勿转载,如需转载请详细标明出处
阅读(6723) | 评论(0) | 转发(0) |