Go每日一库之180:fastcache(协程安全且支持大量数据存储的高性能缓存库)

2023-09-30 09:02:47 浏览数 (2)

fastcache 是一个线程安全并且支持大量数据存储的高性能缓存组件库。

这是官方 Github 主页上的项目介绍,和 fasthttp 名字一样以 fast 打头,作者对项目代码的自信程度可见一斑。此外该库的核心代码非常轻量, 笔者本着学习的目的分析下内部的代码实现。

基准测试

官方给出了 fastcache, bigcache, 标准库 map, sync.Map 的基准测试比较结果。

代码语言:javascript复制
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
  • fastcache只写 读写混合 操作比标准库的 map, sync.Map 要快,只读 操作比后者要慢

组件特性

  • 高性能
  • 线程安全
  • 设计为存储大量数据 (没有 GC 开销)
  • 自动删除比较旧数据
  • 使用简单
  • 源代码简单且非常轻量
  • 缓存数据可以保存到文件,也可以从文件中加载

示例

代码语言:javascript复制
package main

import (
 "fmt"

 "github.com/VictoriaMetrics/fastcache"
)

func main() {
    // 初始化一个大小为 32MB 的缓存
    cache := fastcache.New(32 * 1024 * 1024)
    
    key := []byte(`hello`)
    val := []byte(`world`)
    
    cache.Set(key, val)                      // 设置 K-V
    fmt.Println(cache.Has(key))              // true
    fmt.Println(cache.Has([]byte(`hello2`))) // false
    
    fmt.Printf("hello = %sn", cache.Get(nil, key)) // hello= world
    
    cache.Del(key)
    fmt.Println(cache.Has(key)) // fasle
}

从示例代码可以看到,除了初始化时需要指定缓存的大小,组件提供的 API 就是常规的 “键值对” 语义操作,例如 Get, Set, Del 等。

高性能设计细节

fastcache 采用类似 bigcache 的设计思路:

  • 缓存 由许多 组成,每个桶都持有一个锁 (分段锁),这样可以提高多核 CPU 的性能,因为多个 CPU 可以同时访问不同的桶
  • 每个桶由一个哈希索引 (数据块的位置) 和 65536 个数据块组成,每个桶的指针数量最多为 桶的存储容量 / 64KB (这里指 bucket.chunks 字段)。 例如,64GB 缓存将包含大约 1M 指针,而类似大小的 map[string][]byte 将包含大约 1B 指针,这将导致巨大的 GC 开销

从设计上来说,和每个桶持有一个大的数据块相比,fastcache 采用的 64KB 的数据块减少了内存碎片和总内存使用量。 此外当从 全局数据块空闲区 获取数据块时,会直接调用 Mmap 分配到堆外内存,减少了总内存使用量,因为 GC 会更频繁地收集未使用的内存,无需调整 GOGC。

使用约束和限制

fastcache 组件库的使用有 4 个约束条件,在技术选型的时候比较重要,不过从下面的 4 点要求来看,实际应用中可以通过设计合理的数据类型来规避这些约束条件。

  • 缓存数据的 keyvalue 数据类型必须是 []byte, 如果是其他类型,必须在存储前转换为 []byte
  • 缓存数据大小超过 64K, 必须调用 SetBig 方法存储
  • 缓存数据没有过期时间,只有当缓存数据的数量溢出时,才会删除比较旧的数据,通用的实践是将过期时间存入数据中
  • 缓存数据采用 环形缓冲区 存储,这意味着数据量过大的情况下,新的数据会重写并覆盖掉旧的数据

此外值得注意的是,Set 方法并没有返回值来表示操作的执行结果,这种设计丢失了方法语义,并且在某些极端情况下造成难以排查的 Bug。

小结

本文主要分析了 fastcache 组件的缓存设计与实现,我们可以从中学习到 3 个非常重要的设计技巧: 采用分段锁降低锁的粒度, 采用指纹 哈希索引快速定位数据位置map 使用 [uint64]uint32 作为非指针优化从而避免 GC, 在组件的基础上,也许我们可以进一步优化 (例如将桶的锁粒度细化到数据块)?感兴趣的读者可以通过修改源代码进行测试 (毕竟 fastcache 的源代码非常轻量)。 此外需要注意的是,fastcache 提供的 Get, Set 方法参数类型都是 []byte, 这意味着在调用方法前,必须现将具体的数据类型或对象转换为 []byte, 这会带来额外的 CPU 消耗。 最后,fastcache 提供的 缓存数据 <=> 文件 写入和加载功能以及 全局数据块空闲区,由于时间关系,本文不再分析其具体代码实现。

0 人点赞