ARC
在ARC原始的实现(IBM的实现)和ZFS中的扩展实现都解决了这些挑战,或者说现存问题。我将描述由Megiddo和Modha提出的Adaptive Replacement Cache的一些基本概念,ZFS的实现版本作为这个实现机制的一个扩展来介绍。这两种实现(原始的Adaptive Replacement Cache和ZFS Adjustable Replacement Cache)共享一些基本的操作原理,所以我认为这种简化是一种用来解释ZFS ARC切实可行的途径。
首先,假设我们的缓存中有一个固定的页面数量。简单起见,假设我们有一个8个页面大小的缓存。为了是ARC可以工作,在缓存中,它需要一个2倍大小的管理表。
这个管理表分成4个链表。头两个链表是显而易见的:
· 最近最多使用的页面链表 (LRU list)
· 最近最频繁使用的页面链表(LFU list)
另外两个链表在它们的角色上有些奇怪。它们被称作ghost链表。那些最近被淘汰出去的页面信息被存储在这两个链表中:
· 存储那些最近从最近最多使用链表中淘汰的页面信息 (Ghost list for LRU)
· 存储那些最近从最近最频繁使用链表中淘汰的页面信息(Ghost list for LFU)
这两个ghost链表不储存数据(仅仅储存页面信息,比如offset,dev-id),但是在它们之中的命中对ARC缓存工作的行为具有重要的影响,我将在后面介绍。那么在缓存中都发生了什么呢?
假设我们从磁盘上读取一个页面,并把它放入cache中。这个页面会放入LRU 链表中。
接接下来我们读取另外一个不同的页面。它也会被放入缓存。显然,他也会被放入LRU 链表的最近最多使用的位置
现在我们再读一次第一个页面。我们可以看到,这个页面在缓存中将会被移到LFU链表中。所有进入LRU链表中的页面都必须至少被访问两次。无论什么时候,一个已经在LFU链表中的页面被再次访问,它都会被放到LFU链表的开始位置(most frequently used)。这么做,那些真正被频繁访问的页面将永远呆在缓存中,不经常访问的页面会向链表尾部移动,最终被淘汰出去。
随着时间的推移,这两个链表不断的被填充,缓存也相应的被填充。这时,缓存已经满了,而你读进了一个没有被缓存的页面。所以,我们必须从缓存中淘汰一个页面,为这个新的数据页提供位置。这个数据页可能刚刚才被从缓存中淘汰出去,也就是说它不被缓存中任何的非ghost链表引用着。
假设LRU链表已经满了:这时在LRU链表中,最近最少使用的页面将会被淘汰出去。这个页面的信息会被放进LRU ghost链表中。现在这个被淘汰的页面不再被缓存引用,所以我们可以把这个数据页的数据释放掉。新的数据页将会被缓存表引用。
随着更多的页面被淘汰,这个在LRU ghost中的页面信息也会向ghost链表尾部移动。在随后的一个时间点,这个被淘汰页面的信息也会到达链表尾部,LRU链表的下一次的淘汰过程发生之后,这个页面信息也会从LRU ghost链表中移除,那是就再也没有任何对它的引用了。
好的,如果这个页面在被从LRU ghost链表中移除之前,被再一次访问了,将会发生什么?这样的一个读将会引起一次幽灵(phantom)命中。由于这个页面的数据已经从缓存中移除了,所以系统还是必须从后端存储媒介中再读一次,但是由于这个幽灵命中,系统知道,这是一个刚刚淘汰的页面,而不是第一次读取或者说很久之前读取的一个页面。ARC用这个信息来调整它自己,以适应当前的I/O模式(workload)。说明LRU缓存太小了。在这种情况下,LRU链表的长度将会被增加一。显然,LFU链表的长度将会被减一。同样的机制存在于LFU这边。如果一次命中发生在LFU ghost 链表中,它会减少LRU链表的长度(减一),以此在LFU 链表中加一个可用空间。
利用这种行为,ARC使它自己自适应于工作负载。如果工作负载趋向于访问最近访问过的文件,将会有更多的命中发生在LRU Ghost链表中,也就是说这样会增加LRU的缓存空间。反过来一样,如果工作负载趋向于访问最近频繁访问的文件,更多的命中将会发生在LFU Ghost链表中,这样LFU的缓存空间将会增大。