hashicorp/2qcache ipfs

LRU缓存的缺点

  • 嗯,在一些文件系统缓存中实现的标准的LRU淘汰算法是有一些缺点的。例如,它们对扫描读模式是没有抵抗性的。但你一次顺序读取大量的数据块时,这些数据块就会填满整个缓存空间,即使它们只是被读一次。当缓存空间满了之后,你如果想向缓存放入新的数据,那些最近最少被使用的页面将会被淘汰出去。在这种大量顺序读的情况下,我们的缓存将会只包含这些新读的数据,而不是那些真正被经常使用的数据。在这些顺序读出的数据仅仅只被使用一次的情况下,从缓存的角度来看,它将被这些无用的数据填满。

  • 另外一个挑战是:一个缓存可以根据时间进行优化(缓存那些最近使用的页面),也可以根据频率进行优化(缓存那些最频繁使用的页面)。但是这两种方法都不能适应所有的workload。而一个好的缓存设计是能自动根据workload来调整它的优化策略。

一个真实文件系统的缓存

基于 github.com/hashicorp/[email protected]/2q.go 源码分析

此缓存被用于go/ipfs这个分布式文件系统当做缓存。

你会了解到:

  • 使用两个链表,来区分第一次访问数据和多次访问数据

  • 通过只缓存key来节省空间损耗

2q queue。

frequent是最近看到的块的lru链表。

recent是刚刚看到的块的lru链表

get

  • 从frequent链表取值

  • 如果在recent链表中,则提升到frequent链表。

// Get looks up a key's value from the cache.
func (c *TwoQueueCache) Get(key interface{}) (value interface{}, ok bool) {
c.lock.Lock()
defer c.lock.Unlock()

// Check if this is a frequent value
if val, ok := c.frequent.Get(key); ok {
return val, ok
}

// If the value is contained in recent, then we
// promote it to frequent
if val, ok := c.recent.Peek(key); ok {
c.recent.Remove(key)
c.frequent.Add(key, val)
return val, ok
}

// No hit
return nil, false
}

// Add adds a value to the cache.
func (c *TwoQueueCache) Add(key, value interface{}) {
c.lock.Lock()
defer c.lock.Unlock()

// Check if the value is frequently used already,
// and just update the value
if c.frequent.Contains(key) {
c.frequent.Add(key, value)
return
}

// Check if the value is recently used, and promote
// the value into the frequent list
if c.recent.Contains(key) {
c.recent.Remove(key)
c.frequent.Add(key, value)
return
}

// If the value was recently evicted, add it to the
// frequently used list
if c.recentEvict.Contains(key) {
c.ensureSpace(true)
c.recentEvict.Remove(key)
c.frequent.Add(key, value)
return
}

// Add to the recently seen list
c.ensureSpace(false)
c.recent.Add(key, value)
return
}

// Remove removes the provided key from the cache.
func (c *TwoQueueCache) Remove(key interface{}) {
c.lock.Lock()
defer c.lock.Unlock()
if c.frequent.Remove(key) {
return
}
if c.recent.Remove(key) {
return
}
if c.recentEvict.Remove(key) {
return
}
}

// ensureSpace is used to ensure we have space in the cache
func (c *TwoQueueCache) ensureSpace(recentEvict bool) {
// If we have space, nothing to do
recentLen := c.recent.Len()
freqLen := c.frequent.Len()
if recentLen+freqLen < c.size {
return
}

// If the recent buffer is larger than
// the target, evict from there
if recentLen > 0 && (recentLen > c.recentSize || (recentLen == c.recentSize && !recentEvict)) {
k, _, _ := c.recent.RemoveOldest()
c.recentEvict.Add(k, nil)
return
}

// Remove from the frequent list otherwise
c.frequent.RemoveOldest()
}

const (
// Default2QRecentRatio is the ratio of the 2Q cache dedicated
// to recently added entries that have only been accessed once.
Default2QRecentRatio = 0.25

// Default2QGhostEntries is the default ratio of ghost
// entries kept to track entries recently evicted
Default2QGhostEntries = 0.50
)

// Determine the sub-sizes
recentSize := int(float64(size) * recentRatio)
evictSize := int(float64(size) * ghostRatio)

https://medium.com/@koushikmohan/an-analysis-of-2q-cache-replacement-algorithms-21acceae672a

如果A 1的最大大小的阈值太小,则很有可能会丢失重新引用的页面。如果阈值太高,则A m的大小将减小,并且只能存储较少的重新引用页面。这将影响整体性能。该方法的算法如下

am:frequent。a1:recent

注意recentevicted只会存储key,而没有value。

evict

O(1)时间复杂度,没有复杂参数。

这样可以避免突然接触到新 //清除经常使用的条目。

(对访问频率的计算近似。。)

  • 首次加入是加入recent链表

  • 多次访问才会加入frequent链表。

  • 避免了对一个大文件把所有lru都变成新的。

做到

  • recent是0.25倍

  • recentevicted是0.5倍。

链表大小设置

但是put的时候会尝试把这个recentevicted提升到frequent。

这个时候get不会从这个recentevicted链表之中取出值。

当recent链表太多,多余的会被送到recentevicted链表。

什么情况下

  • 如果recent的链表和frequent链表加起来大于size

  • 如果recent链表超过指定大小,则从recent链表装到recentevicted链表

  • 否则移除frequent链表

ensurespace

remove仅仅只是从对应的链表中删除。

remove

  • 如果frequent链表中有,则update值。

  • 如果在recent链表中,则promote 到frequent链表

  • 如果在recentevited链表中,则加入frequent链表

  • 加入recent链表

add

你可能感兴趣的:(hashicorp/2qcache ipfs)