为什么要学习开源项目
个人认为学习开源项目的收益:
- 跟进社区,不做井底之蛙 看到一个开源项目,可以思考下:大佬们最近都在解决哪些问题?他们用到了哪些开源工具?我能拿到项目里用吗?这玩意有 bug 吗?要不要提个 issue 或者提个 PR 呢?
- 面向原理编程 我们在实际项目中会用上很多开源库/框架,你是否好奇过它们的实现机制呢?理解用到的库的实现机制,能帮我们避开很多坑,堪称降维打击
- 学习优秀的设计 优秀的开源项目经过了成千上万开发者的 review,质量一般会比公司赶进度赶出来的质量高得多得多,从中学习优秀的设计,再在实际项目中多用用,同事会感叹:
3. bigcache 简介
3.1 本地缓存与分布式缓存
sync.Map
3.2 bigcache 诞生背景
bigcache 的开发者是 allegro,是波兰的一个电商网站,参考资料中给出了他们的技术博客的原文,文中详细描述了他们问题的背景以及思考,值得研究。他们的需求主要是:
- 用 HTTP 协议处理 GET POST 请求,body 不大
- 10k rps(requests per second) 5k 读 5k 写
- 缓存至少 10 分钟
- 低延时:平均 5ms ,P99 < 10ms,P999 < 400ms
总结一下,他们需要一个快速、支持过期淘汰、支持 RESTful api 的字典服务
开发团队经过了一番对比,选择了 go 语言(高并发度、带内存管理安全性比 C/C++ 好),抛弃了分布式缓存组件(redis/memcached/couchbase),主要理由是多一跳网络开销。这里我表示怀疑,P999 400ms 的时延其实不至于担心到 redis 网络那点时间,分布式环境下 local cache 不同机器间的数据不一致带来的 cache miss 可能更蛋疼。 最终开发团队选择了实现一个支持以下特性的内存缓存库:
- 百万级缓存项时响应速度也很快
- 并发安全
- 支持设置过期时间
4. 关键设计
4.1 并发与 sharding
sync.RWMutexshard(分片)
bigcache.goBigCache[]*cacheShardcacheShard
那么在写入一个 key value 缓存时,是如何做分片的呢?
uint64
这里把取余的操作用位运算来实现了,这也解释了为什么在使用 bigcache 的时候需要使用 2 的幂来初始化 shard num 了
1024 - 1num & masknum % mask
需要注意,这里的 hash 可能是会冲突的,虽然概率极小,当出现 hash 冲突时,bigcache 将直接返回结果不存在:
4.2 cacheShard 与 bytes queue 设计
ringbufferBytesQueue
cacheShard
在处理完 sharding 后,bigcache 会将整个 value 与 key、hashedKey 等信息序列化后存进一个 byte array,这里的设计是不是有点类似网络协议里的 header 呢?
这里存原始的 string key,我理解单纯是为了处理 hash 冲突用的。
cacheShardbytes queueFIFO
注意到这点,在初始化时使用正确的配置,就能减少重新分配内存的次数了。
4.3 GC 优化
bigcache 本质上就是一个大的哈希表,在 go 里,由于 GC STW(Stop the World) 的存在大的哈希表是非常要命的,看看 bigcache 开发团队的博客的测试数据:
With an empty cache, this endpoint had maximum responsiveness latency of 10ms for 10k rps. When the cache was filled, it had more than a second latency for 99th percentile. Metrics indicated that there were over 40 mln objects in the heap and GC mark and scan phase took over four seconds.
缓存塞满后,堆上有 4 千万个对象,GC 的扫描过程就超过了 4 秒钟,这就不能忍了。
主要的优化思路有:
ringbuffer
当 map 中的 key 和 value 都是基础类型时,GC 就不会扫到 map 里的 key 和 value
map[uint64]uint32cacheShardFIFO
经过优化,bigcache 在 2000w 条记录下 GC 的表现
go version go version go1.13 linux/arm64
go run caches_gc_overhead_comparison.go Number of entries: 20000000
GC pause for bigcache: 22.382827ms
GC pause for freecache: 41.264651ms
GC pause for map: 72.236853ms
效果挺明显,但是对于低延时的服务来说,22ms 的 GC 时间还是很致命的,对象数还是尽量能控制住比较好。
5. 小结
认真学完 bigcache 的代码,我们至少有以下几点收获:
map