特点
性能
Fastcache performance is compared with BigCache, standard Go map and sync.Map.
下面是作者给出的性能指标数据
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
go map 和 string 中 GC 的时候都会扫描每个指针,那么在这个巨大的 map 中是包含了非常多的指针的,所以造成了巨大的资源消耗。
type Map struct {
// 当涉及到对dirty数据的操作的时候,需要使用这个锁
mu Mutex
// 只读,操作完的数据存放在这里
read atomic.Value // readOnly sync.Map.Store时回在里面存一份。
// dirty数据包含当前的map包含的entries,它包含最新的entries(包括read中未删除的数据,虽有冗余,但是提升dirty字段为read的时候非常快,不用一个一个的复制,而是直接将这个数据结构作为read字段的一部分),有些数据还可能没有移动到read字段中。
// 对于dirty的操作需要加锁,因为对它的操作可能会有读写竞争。
// 当dirty为空的时候, 比如初始化或者刚提升完,下一次的写操作会复制read字段中未删除的数据到这个数据中。
dirty map[interface{}]*entry //entry {unsafe.Pointer}
// 当从Map中读取entry的时候,如果read中不包含这个entry,会尝试从dirty中读取,这个时候会将misses加一,
// 当misses累积到 dirty的长度的时候, 就会将dirty提升为read,避免从dirty中miss太多次。因为操作dirty需要加锁。
misses int
}
// src/runtime/string.go
type stringStruct struct {
str unsafe.Pointer
len int
}
(1) Go 中内存分配
TCMalloc(Thread cache malloc) 将内存划分为多个级别,以减少锁的粒度。在 TCMalloc 内部,内存管理分为两部分:线程内存和页堆(page heap)。
Go 为每个逻辑处理器(P)提供了一个称为 mcache 的本地内存线程缓存,mcache 包含所有级别大小的 mspan 作为缓存。mspan 中包含 scan—包含指针的对象和 noscan —不包含指针的对象。GC 无需遍历 noscan 对象。
(2) Go GC 机制
go 程序在SWT时进行垃圾回收,会搜索所有的指针应用。举个简单的例子:申明一个map对象map[string]interface{} A, 它会在栈区存储对象A,在堆区存放数据B、C、D、E。当我释放E时进行的操作是讲map[E]直接赋值为nil。这样堆区的E数据就没有被引用到,在下次gc的时候E将不会被标记,最终被回收。
//go:notinheap
type mspan struct {
next *mspan // next span in list, or nil if none
prev *mspan // previous span in list, or nil if none
list *mSpanList // For debugging. TODO: Remove.
freeindex uintptr
...
//Go分配的内存会被染色标记。
allocBits *gcBits
gcmarkBits *gcBits
...
}
在理解上面的问题后就能很清楚的理解fastecache是如何去规避这些性能问题了。
如果所有的 string 字节都在一个单一内存片段中,通过偏移来追踪某个字符串在这段内存中的开始和结束位置。不在需要在大数组中存储指针。fastecache 原理
cahche 的实现
const bucketsCount = 512
const chunkSize = 64 * 1024
const bucketSizeBits = 40
const genSizeBits = 64 - bucketSizeBits
const maxGen = 1<<genSizeBits - 1
const maxBucketSize uint64 = 1 << bucketSizeBits
// fastecache申明的结构体
type Cache struct {
buckets [bucketsCount]bucket //大小为512的桶
bigStats BigStats //包含存取大 k/v 的方法
}
// 清空cache
func (c *Cache) Reset() {
for i := range c.buckets[:] {
c.buckets[i].Reset()
}
c.bigStats.reset()
}
// BigStats 包含GetBig/SetBig方法.
type BigStats struct {
GetBigCalls uint64
SetBigCalls uint64
TooBigKeyErrors uint64
InvalidMetavalueErrors uint64
InvalidValueLenErrors uint64
InvalidValueHashErrors uint64
}
func (bs *BigStats) reset() {
atomic.StoreUint64(&bs.GetBigCalls, 0)
atomic.StoreUint64(&bs.SetBigCalls, 0)
atomic.StoreUint64(&bs.TooBigKeyErrors, 0)
atomic.StoreUint64(&bs.InvalidMetavalueErrors, 0)
atomic.StoreUint64(&bs.InvalidValueLenErrors, 0)
atomic.StoreUint64(&bs.InvalidValueHashErrors, 0)
}
桶的实现
type bucket struct {
mu sync.RWMutex
// 存储键值对的区块s
chunks [][]byte
// 键盘值对的块索引,hash(k) idx of (k, v) pair in chunks.
m map[uint64]uint64
// 桶里的下一个块的索引。
idx uint64
// 区块重写次数, gen is the generation of chunks.
gen uint64
getCalls uint64
setCalls uint64
misses uint64
collisions uint64
corruptions uint64
}
// 初始化桶
func (b *bucket) Init(maxBytes uint64) T
// maxBucketSize uint64 = 1 << 40 1T; chunkSize = 64 * 1024
maxChunks := (maxBytes + chunkSize - 1) / chunkSize
b.chunks = make([][]byte, maxChunks)
b.m = make(map[uint64]uint64)
b.Reset()
}
使用
func New(maxBytes int) *Cache {
var c Cache
//maxBytes >= 0 ,bucketsCount=512
maxBucketBytes := uint64((maxBytes + bucketsCount - 1) / bucketsCount) //桶的大小
for i := range c.buckets[:] {
c.buckets[i].Init(maxBucketBytes)//初始化桶
}
return &c
}
func (c *Cache) Set(k, v []byte) {
h := xxhash.Sum64(k) //计算key的hash值
idx := h % bucketsCount //计算key对应的桶
c.buckets[idx].Set(k, v, h)
}
func (b *bucket) Set(k, v []byte, h uint64) {
// 限定 k v 大小不能超过 2bytes 64k
if len(k) >= (1<<16) || len(v) >= (1<<16) {
return
}
// 4个byte 设置每条数据的数据头
var kvLenBuf [4]byte
kvLenBuf[0] = byte(uint16(len(k)) >> 8)
kvLenBuf[1] = byte(len(k))
kvLenBuf[2] = byte(uint16(len(v)) >> 8)
kvLenBuf[3] = byte(len(v))
kvLen := uint64(len(kvLenBuf) + len(k) + len(v))
// 校验一下大小
if kvLen >= chunkSize {
return
}
b.mu.Lock()
// 当前索引位置
idx := b.idx
// 存放完数据后索引的位置
idxNew := idx + kvLen
// 根据索引找到在 chunks 的位置
chunkIdx := idx / chunkSize
chunkIdxNew := idxNew / chunkSize
// 新的索引是否超过当前索引
// 因为还有chunkIdx等于chunkIdxNew情况,所以需要先判断一下
if chunkIdxNew > chunkIdx {
// 校验是否新索引已到chunks数组的边界
// 已到边界,那么循环链表从头开始
if chunkIdxNew >= uint64(len(b.chunks)) {
idx = 0
idxNew = kvLen
chunkIdx = 0
b.gen++
// 当 gen 等于 1<
// 也就是用来限定 gen 的边界为1<
if b.gen&((1<<genSizeBits)-1) == 0 {
b.gen++
}
} else {
// 未到 chunks数组的边界,从下一个chunk开始
idx = chunkIdxNew * chunkSize
idxNew = idx + kvLen
chunkIdx = chunkIdxNew
}
// 重置 chunks[chunkIdx]
b.chunks[chunkIdx] = b.chunks[chunkIdx][:0]
}
chunk := b.chunks[chunkIdx]
if chunk == nil {
// 开辟块空间
chunk = getChunk()
// 初始化切片
chunk = chunk[:0]
}
// 将数据 append 到 chunk 中
chunk = append(chunk, kvLenBuf[:]...)
chunk = append(chunk, k...)
chunk = append(chunk, v...)
b.chunks[chunkIdx] = chunk
// 因为 idx 不能超过bucketSizeBits,所以用一个 uint64 同时表示gen和idx
// 所以高于bucketSizeBits位置表示gen
// 低于bucketSizeBits位置表示idx
b.m[h] = idx | (b.gen << bucketSizeBits)
b.idx = idxNew
b.mu.Unlock()
}
// 块分配
func getChunk() []byte {
freeChunksLock.Lock()
if len(freeChunks) == 0 {
// Allocate offheap memory, so GOGC won't take into account cache size.
// This should reduce free memory waste.
data, err := syscall.Mmap(-1, 0, chunkSize*chunksPerAlloc, syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_ANON|syscall.MAP_PRIVATE)
for len(data) > 0 {
p := (*[chunkSize]byte)(unsafe.Pointer(&data[0]))
freeChunks = append(freeChunks, p)
data = data[chunkSize:]
}
}
n := len(freeChunks) - 1
p := freeChunks[n]
freeChunks[n] = nil
freeChunks = freeChunks[:n]
freeChunksLock.Unlock()
return p[:]
}
示意图:
cache 结构,真正存放数据的地方是 chunks 二维数组.
chunk 的结构
fasteCache应用了一些比较hack的方式规避了go 内存分配和Gc在处理大数据量上可能引发的一些性能问题。它不支持缓存过期等因此在使用时一定要结合具体的应用场景予以取舍。