介绍一个golang库:fastcache
学习VictoriaMetrics源码的时候发现,VictoriaMetrics的缓存部分,使用了同一产品下的fastcache。下面分享阅读fastcache源码的的结论:
1.官方介绍
fastcache是一个用go语言实现的,很快的,线程安全的,内存缓存的,用于大量对象缓存的组件。
它的特点是:
- 快!CPU核越多越快,不信你看我下面的benchmark。
- 线程安全。多个协程可以同时读写单个cache实例。
- fastcache用于存储大量的cache实体,而且不会被GC扫描。
- 当设定的cache空间满了以后,fastcache会自动淘汰老数据。
- API贼简单。
- 源码也贼简单。
- cache还可以保存在文件中,需要的时候能加载。
- 在Google的云服务上也能跑得起来。(说明未使用很特殊的操作系统API)
作者valyala是fasthttp和VictoriaMetrics等作品的主要开发者。valyala大神有极其强悍的工程能力,很多看来已经很简单的成熟组件被他又一次妙手生花,YYDS!
2. 性能
究竟有多快呢?作者做了一个对比:(这里主要看set操作)
- golang的标准map: 6.21 M次/s
- sync.Map库:2.65 M次/s
- BigCache库:6.20 M次/s
- fastcache库:17.21 M次/s
换个角度看:
- 比golang的标准map快2.77倍
- 比sync.Map库快6.49倍
- 比BigCache快2.78倍
快得我都不知道说啥好了……
3. 限制
当然也不是快就完美了,也是有些限制的。要根据这些限制来确定fastcache是否适合引入你的业务环境中:
- key和value都只能是[]byte类型,不是的话要自己序列化
- key长度+value长度+4不能超过64KB,否则就要使用额外的SetBig()方法
- 没有缓存过期机制。只有在cache满了以后才能淘汰旧数据。
- 可以自己把过期时间存储在value中,读出来的时候判断一下。如果过期了,手动调用Del()方法来删除。
- cache的总容量是预先设置好的,超过这个容量就要淘汰最早插入的值。
- 当然了,cache嘛,仅适合cache场景,不能用于无损的数据存储。
- 最后:hash冲突的处理上,整个cache分为512个桶。如果两个key的hashcode完全相同的话,新插入的值会替换掉旧的值,导致前一个值丢失……
- 发生hash冲突时仅仅只是原子累加到监控变量,让你知道曾经发生过……
- 我认为这一点很不合理,给作者提了个issue
4. 源码解读
4.1 使用mmap分配内存
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能带来性能上的好处,但是这里分配的内存再也不会被释放,直到进程重启。
4.2 Cache类的实现
fastcache.go中是fastcache的主要代码。
4.2.1 cache对象的结构
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对象里面去处理。
xxhash库用汇编实现,是目前最快的hashcode计算的库
4.3 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的数组- chunk是上面提到的通过mmap分配的64KB的一个块
- key+value的数据会被顺序的放在chunk中,并记录位于数组中的下标
- 一个chunk的空间用完后,会再通过
getChunk()
再申请64KB的块。直到块达到用户规定的上限。- 假设每个bucket 1MB, 则共有1MB/64KB=16个chunk
- 第15个chunk满了以后,又回到第0个chunk存储,同时gen字段增加,说明是新的一代
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过程
搞清楚了Set,Get就更简单了:
- 首先在Cache类中,根据key的hashcode,确定选择哪个bucket
- 查询前加读锁
- 在m字段的map中,根据hashcode找到下标
- 根据下标确定key的位置
- 比较key的内容是否相等
- 最后返回value
4.3.4 Del过程
del仅删除map中的key,而chunks中对应的位置只能等到下次回绕才能清理。
删除的动作是滞后的,因此fastcache不适合删除很多的业务场景。
5.总结
fastcache为什么快,因为用了这些手段:
使用mmap来成块的分配内存。
- 每次直接向操作系统要64MB,这些内存都绕开了GC。
- 每次以64KB为单位请求一个块
- 在64KB的块内顺序存储,相当于更简单的自己实现的分配算法
整个cache分成512个bucket
- 相当于有了512个map+512个读写锁,通过这样减少了竞争
- map类型的key和value都是整形,容量小,且对GC友好
- 淘汰用轮换的方法+固定次数的set后再清理,解决了(或者说绕开了)碎片的问题
希望对你有用,have fun
来自我的公众号:原文链接
介绍一个golang库:fastcache的更多相关文章
- 介绍一个新库: Norns.Urd.HttpClient
Norns.Urd.HttpClient Norns.Urd.HttpClient 基于AOP框架 Norns.Urd实现, 是对 System.Net.Http下的 HttpClient封装,让大家 ...
- 介绍一个python视频处理库:moviepy
由于博客园的插件和我自己博客的插件不一致,代码以及视频插入转换很麻烦,所以还是我原来博客的地址查看吧. 介绍一个python视频处理库:moviepy
- 一个让业务开发效率提高10倍的golang库
一个让业务开发效率提高10倍的golang库 此文除了是标题党,没有什么其他问题. 这篇文章推荐一个库,https://github.com/jianfengye/collection. 这个库是我在 ...
- 介绍一个非常好用的跨平台C++开源框架:openFrameworks
介绍一个非常好用的跨平台C++开源框架:openFrameworks 简介 首先需要说明的一点是: openFrameworks 设计的初衷不是为计算机专业人士准备的, 而是为艺术专业人士准备的, 就 ...
- 【Hades】ades是一个开源库,基于JPA和Spring构建,通过减少开发工作量显著的改进了数据访问层的实现
几乎每个应用系统都需要通过访问数据来完成工作.要想使用领域设计方法,你就需要为实体类定义和构建资源库来实现领域对象的持久化.目前开发人员经常使用JPA来实现持久化库.JPA让持久化变得非常容易,但是仍 ...
- NDK中android.mk文件的简单介绍和第三方库的调用
先贴一个样例,然后解释一下: LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := mydjvuapi SRC_FILE_ ...
- 教你一步步发布一个开源库到 JCenter
今天想来分享下,如何一步步自己发布一个开源库到 JCenter 这方面的博客网上已经特别多了,所以本篇并不打算仅仅只是记录流程步骤而已,而是尽可能讲清楚,为什么需要有这个步骤,让大伙知其然的同时还知其 ...
- 你不需要 jQuery,但你需要一个 DOM 库
写这篇文章的目的,一方面是介绍一下自己编写的模块化 DOM 库 domq.js,另一方面是希望大家对 jQuery 有一个正确的认识,即使 jQuery 已经逐渐退出历史舞台,但是它的 API 将会以 ...
- 介绍一个axios调试好用的工具:axios-mock-adapter
上一篇文章中写到用promise时应注意的问题,这一篇文章继续介绍一个可以和axios库配合的好工具: axios-mock-adapter.axios-mock-adapter可以用来拦截http请 ...
随机推荐
- C/C++ 基本类型 占字节
下面给出不同位数编译器下的基本数据类型所占的字节数: 16位编译器 char :1个字节char*: 2个字节(即指针变量)short: 2个字节int: 2个字节unsigned int : 2个 ...
- LeetCode 1482. 制作 m 束花所需的最少天数
LeetCode 1482. 制作 m 束花所需的最少天数 题目 给你一个整数数组 bloomDay,以及两个整数 m 和 k . 现需要制作 m 束花.制作花束时,需要使用花园中 相邻的 k 朵花 ...
- 1084 - Winter
1084 - Winter PDF (English) Statistics Forum Time Limit: 2 second(s) Memory Limit: 32 MB Winter is ...
- 阿克曼函数推导过程(m<=3)
阿克曼函数(Ackermann)是非原始递归函数的例子.它需要两个自然数作为输入值,输出一个自然数.它的输出值增长速度非常快,仅是对于(4,3)的输出已大得不能准确计算. \[A(m, n)=\lef ...
- iGPT and ViT
目录 概 主要内容 iGPT ViT 代码 Chen M., Radford A., Child R., Wu J., Jun H., Dhariwal P., Luan D., Sutskever ...
- AUGMIX : A SIMPLE DATA PROCESSING METHOD TO IMPROVE ROBUSTNESS AND UNCERTAINTY
目录 概 主要内容 实验的指标 Dan Hendrycks, Norman Mu,, et. al, AUGMIX : A SIMPLE DATA PROCESSING METHOD TO IMPRO ...
- Understanding Black-box Predictions via Influence Functions
目录 概 主要内容 样本重要性分析 样本摄动对损失的影响 高效计算 共轭梯度 随机估计 一些应用 附录 (1)的证明 Koh P W, Liang P. Understanding black-box ...
- Loss Landscape Sightseeing with Multi-Point Optimization
目录 概 主要内容 代码 Skorokhodov I, Burtsev M. Loss Landscape Sightseeing with Multi-Point Optimization.[J]. ...
- [Guide]Google Python Style Guide
扉页 项目主页 Google Style Guide Google 开源项目风格指南 - 中文版 背景 Python 是Google主要的脚本语言.这本风格指南主要包含的是针对python的编程准则. ...
- DP转LVDS方案 瑞奇达CS5211替代PS8625方案 CS5211芯片
PS8625将作为DP或eDP接收器设备出现在视频源中,并将作为LVDS显示面板的LVDS源设备.该设备是一个完全集成的解决方案,不需要外部CPU.内存.时钟基准或电压调节器.PS8625可配置为从显 ...