Go中的缓存现状(BigCache&FreeCache&GroupCache 缓存框架对比)

Go中的缓存现状

这篇文章登上了Golang 在Reddit subreddit板块的顶部,并在Hacker News 首页排名到第二名。欢迎各位来阅读讨论,并在Github上面给我们一个小星星。

每个数据库都需要一个智能的缓存系统。缓存需要保存最近最频繁访问的内容,并且支持配置一些限制上的配置。

作为一个图形数据库,Dgraph可以在每次查询中,访问数千甚至数百万的key。这个功能主要依赖于他中间结果的数量。由于通过键值对访问数据库会导致磁盘上的查询操作,出于对性能方面的考虑(磁盘访问速度不及内存),我们希望优化这块的性能。

通常的访问模式都遵循 ZipFian分布,访问频率最高的key,比其他的key访问次数要多很多。从Dgraph中也能看到这一点(热点Key的问题)。

我们非常高兴能用Go语言来实现我们的Dgraph 组件,关于为什么Go语言适合做后端开发,这个内容太多了,在这里不赘述了。尽管Go的生态还不够健全,但不能否认Go是一个很不错的编程语言,而且我们也不会用别的语言来替代Go。

关于Go生态缺失的怨言随处可见。但是我觉得Go是成熟的,他已经实现了对机器内核的快速编译,执行和利用内核完成工作。但是作为一个致力于构建高并发的编程语言,对于性能上仍然有一些缺陷,并发库可以很好地扩展内核数量。对于并发的数组和字典,用户可以自由的使用和练习。对于串行语言来说,这样是合理的,但是对于以并行构建的编程语言,这点上似乎有一些缺陷。

特别的是,Go缺少并发的LRU/LFU 缓存,这两者可以很好地扩展到全局缓存中。在这片博客里面,我会带你一起来了解一下通常情况下的各种处理方式,包括在我的的Dgraph中进行的一些测试。Aman 同时也会展示一些目前Go生态中的设计理念,性能,命令率等的一些实践内容。

缓存框架的必备需求

  1. 并发
  2. 内存限制(限制最大的可使用空间)
  3. 在多核和多goroutines之间更好的扩展
  4. 在非随机密钥的情况下,很好地扩展(eg. Zipf)
  5. 更高的缓存命中率

Go map 与 sync.Mutex的结合使用

Go map 结合 sync.Mutex 是应对缓存的常见形式(独占所)。但这也确实会导致所有的Goroutines同时在一个地方锁住, 产生严重的锁竞争问题。而且也不能对内存的使用量做限制。所以对于有内存限制要求的场景,这个方案不适用。

不满足 上面的2,3,4条

Go maps 与 lock striping

这个方式的原理与上面的一样,但是锁的粒度更小(详见这里),很多程序员错误的认为,降低锁的粒度可以很好地避免竞争,特别是在分片数超过程序的线程数时(GOMAXPROCS)

在我们尝试编写一个简单的内存限制缓存的时候,我们也是这样做的。为了保证内存可以在释放之后还给操作系统。我们定期扫描我们的分片,然后释放掉创建的map,方便以后被再次使用。这种粗浅的方式却很有效,并且性能优于LRU(后面会解释),但是也有很多不足。

  1. Go请求内存很容易,但释放给操作系统却很难。当碎片被清空的同时,goroutines去访问key的时候,会开始分配内存空间,此时之前的内存空间并没有被完全释放,这导致内存的激增,甚至会出发OOM错误。
  2. 我们没有意识到,访问的模式还受Zipf定律的束缚。最常访问的几个key仍然存在几个锁,因此产生goroutines的竞争问题。这种方式不满足多核之间的扩展的需求。

不满足 上面的2,4条

LRU 缓存

Go 里面,groupcache 实现了一个基本的LRU 缓存,在通过lock striping实现失败之后,我们通过引入lock的方式优化了LRU的这部分内容,使它支持了并发。虽然这样解决了上面描述的内存激增的问题,但是我们意识到他同样地会引入竞争的问题。

这个缓存的大小同样也依赖于缓存的条数,而不是他们消耗的内存量。在Go的堆上面计算复杂的数据结构所消耗的内存大小是非常麻烦的,几乎不可能实现。我们尝试了很多方式,但是都无法奏效。缓存被放入之后,大小也在不停地变化(我们计划之后避免这种情况)

我们无法预估缓存会引起多少的竞争。在使用了近一年的情况下,我们意识到缓存上面的竞争有多严重,删除掉这块之后,我们的缓存效率提高了10倍。

在这块的实现上,每次读取缓存会更新链表中的相对位置。因此每个访问都在等待一个互斥锁。此外LRU的速度比Map要慢,而且在反复的进行指针的释放,维护一个map和一个双向链表。尽管我们在惰性加载上面不断地优化,但依然遭受到竞争的而影响。

不满足3,4

分片LRU 缓存

我们没有实际的去尝试,但是依据我们的经验,这只会是一个暂时的解决方法,而且并不能很好地扩展。(不过在下面的测试里面,我们依然实现了这个解决方案)

不满足4

流行的缓存实现方式

许多方法的优化点是节省GC在map碎片上花费的时间。GC的时间会随着map存数数量的增加而增大。减少的方案就是分配更少的数量,单位空间更大的区域,在每个空间上存储更多的内容。这确实是一个有效地方法,我们在Badger里面大量的使用了这个方法(Skiplist,Table builder 等)。 很多Go流行的缓存框架也是这么做的。

BigCache的缓存

BigCache会通过Hash的方式进行分片。 每个分片都包含一个map和一个ring buffer。无论如何添加元素,都会将它放置在对应的ring buffer中,并将位置保存在map中。如果多次设置相同的元素,则ring buffer中的旧值则会被标记为无效,如果ring buffer太小,则会进行扩容。

每个map的key都是一个uint32的 hash值,每个值对应一个存储着元数据的ring buffer。如果hash值碰撞了,BigCache会忽略旧key,然后把新的值存储到map中。预先分配更少,更大的ring buffer,使用map [uint32] uint32是避免支付GC扫描成本的好方法

FreeCache

FreeCache 将缓存分成了256段,每段包括256个槽和一个ring buffer存储数据。当一个新的元素被添加进来的时候,使用hash值下8位作为标识id,通过使用LSB 9-16的值作为槽ID。将数据分配到多个槽里面,有助于优化查询的时间(分治策略)。

数据被存储在ring buffer中,位置被保存在一个排序的数组里面。如果ring buffer 内存不足,则会利用LRU的策略在ring buffer逐个扫描,如果缓存的最后访问时间小于平均访问的时间,就会被删掉。要找到一个缓存内容,在槽中是通过二分查找法对一个已经排好的数据进行查询。

GroupCache

GroupCache使用链表和Map实现了一个精准的LRU删除策略的缓存。为了进行公平的比较,我们在GroupCache的基础上,实现了一个包括256个分片的切片结构。

性能对比

为了比较各种缓存的性能,我们生成了一个zipf分布式工作负载,并使用n1-highcpu-32机器运行基准测试。下表比较了三个缓存库在只读工作负载下的性能。

只读情况

我们可以看到,由于读锁是无消耗的,所以BigCache的伸缩性更好。FreeCache和GroupCache读锁是有消耗的,并且在并发数达到20的时候,伸缩性下降了。(Y轴越大越好)

只写情况

在只写的情况下,三者的性能表现比较接近,FreeCache比另两个的情况,稍微好一点。

读写情况(25% 写,75%读)

两者混合的情况下,BigCache看起来是唯一一个在伸缩性上表现完美的,正如下一节所解释的那样,命中率对于Zipf工作负载是不利的。

命中率比较

下面的表格中展示了三个框架的命中率。FreeCache非常接近GroupCache实现的LRU策略。然而,BigCache在zipf分布式工作负载下表现不佳,原因如下:

  • BigCache不能有效地利用缓冲区,并且可能会在缓冲区中为同一个键存储多个条目。
  • BigCache不更新访问(读)条目,因此会导致最近访问的键被删除。
CACHE SIZE (# OF ELEM) 10000 100000 1000000 10000000
BigCache - 37% 52% 55%
FreeCache - 38% 55% 90%
GroupCache 29% 40% 54% 90%

所以说,没有哪个框架能满足所有缓存的需求。

那还有什么没说的么?

其实也没什么了,Go中并没有一个能满足所有场景的智能缓存框架,如果你发现了有这种,请快快联系我。

与此同时,我们遇到了Caffeine,一个Java的库,被用于使用Cassandra, Finagle 和一些其他的数据库系统。他使用的是TinyLFU,一个高效的缓存接纳策略,并使用各种技术来扩展和执行,随着线程和内核数量的增长,同时提供接近最佳命中率。您可以在这篇文章中了解它是如何工作的。

Caffeine 满足了我开始提到的所有的5个需求,所以我正在考虑构建一个Go版本的Caffeine。他不仅能满足我们的需求,同事也可能填补Go语言中并发,高性能,内存限制的缓存框架的空白。如果你也想参与或者你已经有类似的成果了,请联系我。

感谢

我们想要感谢 Benjamin Manes 帮助我们对Caffeine进行一些Go版本的性能测试(Code here),我们还要感谢Damian Gryski为我们提供了基准缓存命中率的基本框架(这里),我们还修改了它,来满足我们的需要。他已经接受了我们对于他代码库(GitHub)的修改。

感谢阅读,如果方便的话,给我们Github 点个星星吧。

via:

翻译

原文链接:The State of Caching in Go
作者:Manish Rai Jain
译者:JYSDeveloper

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,511评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,495评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,595评论 0 225
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,558评论 0 190
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,715评论 3 270
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,672评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,112评论 2 291
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,837评论 0 181
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,417评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,928评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,316评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,773评论 2 234
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,253评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,827评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,440评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,523评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,583评论 2 249

推荐阅读更多精彩内容

  • 红尘中我们只是偶尔想起,纷繁的俗世中真心实意走了一回,不要说什么爱与不爱,我只是努力的活着已经很累。 ...
    荣一心_d442阅读 432评论 5 13
  • 借着充满悲呛的喉 举起用往事酿的酒 敬一个不会回头的你
    花败不开阅读 244评论 0 1
  • “南山南,北秋悲,南山有谷堆;南风喃,北海北,北海有墓碑。”初闻北海,是在《南山南》这首悲凉感人的民谣里。北海,一...
    娟娟新月阅读 3,862评论 110 269
  • 1、后海散步打卡,丁丁陪伴,和狗狗玩耍,呼吸新鲜空气,链接大自然,体验多维世界,打开自己的各种感官,不断提升身体能...
    张艾雯阅读 276评论 2 2