Golang本地缓存利器fastcache一文学透
一、介绍
fastcache是一个用go语言实现的,很快的,线程安全的,内存缓存的,用于大量对象缓存的组件。
它的特点是:
- 快速的多核CPU的性能可扩展。
- 线程安全。并发goroutines可以读取和写入单个缓存实例。
- 快速缓存是为存储大量条目而设计的,没有GC开销。
- 当达到创建时设置的最大缓存大小时,Fastcache会自动收回旧条目。
- 简单的API
- 简单的源代码。
- 缓存可以保存到文件中,也可以从文件中加载。
- 适用于谷歌应用引擎。
二、性能
到底有多快?:
GOMAXPROCS=4 go test github.com/VictoriaMetrics/fastcache -bench='Set|Get' -benchtime=10s
goos: linux
goarch: amd64
pkg: github.com/VictoriaMetrics/fastcache
BenchmarkBigCacheSet-4 2000 10566656 ns/op 6.20 MB/s 4660369 B/op 6 allocs/op
BenchmarkBigCacheGet-4 2000 6902694 ns/op 9.49 MB/s 684169 B/op 131076 allocs/op
BenchmarkBigCacheSetGet-4 1000 17579118 ns/op 7.46 MB/s 5046744 B/op 131083 allocs/op
BenchmarkCacheSet-4 5000 3808874 ns/op 17.21 MB/s 1142 B/op 2 allocs/op
BenchmarkCacheGet-4 5000 3293849 ns/op 19.90 MB/s 1140 B/op 2 allocs/op
BenchmarkCacheSetGet-4 2000 8456061 ns/op 15.50 MB/s 2857 B/op 5 allocs/op
BenchmarkStdMapSet-4 2000 10559382 ns/op 6.21 MB/s 268413 B/op 65537 allocs/op
BenchmarkStdMapGet-4 5000 2687404 ns/op 24.39 MB/s 2558 B/op 13 allocs/op
BenchmarkStdMapSetGet-4 100 154641257 ns/op 0.85 MB/s 387405 B/op 65558 allocs/op
BenchmarkSyncMapSet-4 500 24703219 ns/op 2.65 MB/s 3426543 B/op 262411 allocs/op
BenchmarkSyncMapGet-4 5000 2265892 ns/op 28.92 MB/s 2545 B/op 79 allocs/op
BenchmarkSyncMapSetGet-4 1000 14595535 ns/op 8.98 MB/s 3417190 B/op 262277 allocs/op
将Fastcache的性能与BigCache、标准Go映射和sync.map进行比较。可以看出来FashCache的写入效率最快。换个角度说明一下就是fastcache比标准map快2.77倍,比sync.Map库快6.49倍,比BigCache快2.78倍。
三、限制
当然fastcache并不是完美的,也是存在缺陷的,可以根据下面限制决定是否要引入自己的业务环境:
- key和value都只能是[]byte类型,不是的话要自己序列化
- 大小超过64KB的大条目必须通过Cache.SetBig来存储
- 没有缓存过期。只有在缓存大小溢出时,才会从缓存中逐出条目。输入截止日期以存储在值内,以便实现缓存过期。
四、源码
malloc_mmap.go中使用了unix.Mmap()来分配内存:
- 内存映射的方式可以直接向操作系统申请内存,这块区域不归GC管。所以不管你在这块内存缓存了多少数据,都不会因为GC扫描而影响性能。
- 每次使用mmap申请内存的时候,申请了1024*64KB=64MB内存。
每64KB称为一个chunk
所有的chunk放在一个队列中
当队列中所有的chunk都用完后,再申请64MB
- chunk的管理:
var (
freeChunks []*[chunkSize]byte //相当于一个队列,保存了所有未使用的chunk
freeChunksLock sync.Mutex //chunk的锁
)
可以通过 func getChunk() []byte
函数获取一个64KB的块。如果freeChunks中没有chunk了,就再通过mmap申请64MB。
chunk的归还func putChunk(chunk []byte)
函数把有效的chunk放回freeChunks队列。
绕过GC能带来性能上的好处,但是这里分配的内存再也不会被释放,直到进程重启。
fastcache.go中是fastcache的主要代码。
type Cache struct {
buckets [bucketsCount]bucket
bigStats BigStats
}
- bucketsCount这个常量值为512 。也就是说,cache对象的内部分布了512个桶。
- bigStats 是用于内部的监控上报的
4.2.2 新建Cache对象
// func New(maxBytes int) *Cache
c := New(1024*1024*32) //cache的最小容量是32MB
New源码如下:
func New(maxBytes int) *Cache {
if maxBytes <= 0 {
panic(fmt.Errorf("maxBytes must be greater than 0; got %d", maxBytes))
}
var c Cache
maxBucketBytes := uint64((maxBytes + bucketsCount - 1) / bucketsCount)
for i := range c.buckets[:] {
c.buckets[i].Init(maxBucketBytes)
}
return &c
}
- maxBytes先按照512字节向上对齐
- 然后划分成512份,假设申请内存512MB,则每份1MB。也就是每个bucket 1MB内存。
- 分为512个桶,每个桶再单独初始化
4.2.3 SET方法
func (c *Cache) Set(k, v []byte) {
h := xxhash.Sum64(k)
idx := h % bucketsCount
c.buckets[idx].Set(k, v, h)
}
非常简单:对key计算一个hash值,然后对hash值取模,转到具体的bucket对象里面去处理。
4.3.1 bucket结构
type bucket struct {
mu sync.RWMutex
// chunks is a ring buffer with encoded (k, v) pairs.
// It consists of 64KB chunks.
chunks [][]byte
// m maps hash(k) to idx of (k, v) pair in chunks.
m map[uint64]uint64
// idx points to chunks for writing the next (k, v) pair.
idx uint64
// gen is the generation of chunks.
gen uint64
getCalls uint64 // 以下都是用于统计的变量
setCalls uint64
misses uint64
collisions uint64
corruptions uint64
}
- mu sync.RWMutex : 每个bucket有一个读写锁来处理并发。
- 和sync.Map比起来,原理上也没什么神秘的。把数据分散到512个桶,相当于竞争变为原来的1/512。
- chunks [][]byte: 这个是存储数据的chunk的数组
- 假设每个bucket 1MB, 则共有1MB/64KB=16个chunk
- 第15个chunk满了以后,又回到第0个chunk存储,同时gen字段增加,说明是新的一代
- chunk是上面提到的通过mmap分配的64KB的一个块
- key+value的数据会被顺序的放在chunk中,并记录位于数组中的下标
- 一个chunk的空间用完后,会再通过getChunk()再申请64KB的块。直到块达到用户规定的上限。
- m map[uint64]uint64: 这里存储每个hashcode对应的chunk中的偏移量。
- idx uint64: 这里记录下次插入chunk的位置,插入完成后跳转到数据的末位。
- gen uint64: 当所有的chunks都写满以后,gen的值加1,从第0块开始淘汰旧数据。
- 这里有个明显的缺点:假设hashcode都分布在较少的几个bucket中,那么就导致某几个bucket的数据频繁淘汰,而其他的bucket还剩挺多空间。不过,这只是假设,并未有数据证明会有这种现象。
4.3.2 Set过程
源码比较多,大致如下:
- 每set 16384(2的14次方)次,执行一次clean操作
- clean操作遍历整个map,移除chunk中因为回绕淘汰的数据
- key+value序列化的方式很简单,顺序存储以下内容:
- 2字节key长度
- 2字节value长度
- key的内容
- value的内容
- 写入chunk的时候加入了写锁
- 通过bucket的idx字段找到插入位置,然后按照上述序列化的方式拷贝数据
- 插入完成后得到了偏移位置,把key的hashcode作为键,把chunks中的偏移量为值,写入字段m的map中
- value这里还有个细节:value是64位的uint64, value的低40位存储偏移量,value的高24位存储generation的信息。
4.3.3 Get过程
- 首先在Cache类中,根据key的hashcode,确定选择哪个bucket
- 查询前加读锁
- 在m字段的map中,根据hashcode找到下标
- 根据下标确定key的位置
- 比较key的内容是否相等
- 最后返回value
4.3.4 Del过程
del仅删除map中的key,而chunks中对应的位置只能等到下次回绕才能清理。
THE END