【转】缓存设计的一些思考

***{转自:缓存设计的一些思考}***算法

  互联网架构中缓存无处不在,某厂牛人曾经说过:”缓存就像清凉油,哪里不舒服,抹一下就行了”。高品质的存储容量小,价格高;低品质存储容量大,价格低,缓存的目的就在于”扩充”高品质存储的容量。本文探讨缓存相关的一些问题。sql

LRU替换算法缓存

  缓存的技术点包括内存管理和替换算法。LRU是使用最多的替换算法,每次淘汰最久没有使用的元素。LRU缓存实现分为两个部分:Hash表和LRU链表,Hash表用于查找缓存中的元素,LRU链表用于淘汰。内存常以Slab的方式管理。数据结构

  上图是Memcache的内存管理示意图,Memcache以Slab方式管理内存块,从系统申请1MB大小的大块内存并划分为不一样大小的Chunk,不一样Slab的Chunk大小依次为80字节,80 * 1.25,80 * 1.25^2, …。向Memcache中添加item时,Memcache会根据item的大小选择合适的Chunk。多线程

  Oceanbase最初也采用LRU算法,只是内存管理有些不一样。Oceanbase向系统申请2MB大小的大块内存,插入item时直接追加到最后一个2MB内存块的尾部,当缓存的内存量太大须要回收时根据必定的策略整块回收2MB的内存,好比回收最近最少使用的item所在的2MB内存块。这样的作法虽然不是特别精确,可是内存管理简单,对于系统初期颇有好处。架构

缓存锁nosql

  缓存须要操做两个数据结构:Hash表和LRU链表。多线程操做cache时须要加锁,比较直接的作法是总体加一把大锁后再操做Hash表和LRU链表。有以下的优化思路:优化

  1. Hash表和LRU链表使用两把不一样的锁,且Hash表锁的粒度能够下降到每一个Hash桶一把锁。这种作法的难点是须要处理两种数据结构不一致致使的问题,假设操做顺序为read hash -> del hash item -> del lru item -> read lru item,最后一次read lru item时item所在的内存块可能已经被回收或者重用,通常须要引入引用计数并考虑复杂的时序问题。
  2. 采用多个LRU链表以减小LRU表锁粒度。Hash表的锁冲突能够经过增长Hash桶的个数来解决,而LRU链表是一个总体,难以分解。能够将缓存的数据分红多个工做集,每一个item属于某个工做集,每一个工做集一个LRU链表。这样作的主要问题是可能不均衡,好比某个工做集很热,某些从总体上看比较热的数据也可能被淘汰。
  3. 牺牲LRU的精确性以减小锁。好比Mysql中的LRU算法变形,大体以下:将LRU链表分红两部分,前半部分和后半部分,若是访问的item在前半部分,什么也不作,而不是像传统的LRU算法那样将item移动到链表头部;又如Linux Page Cache中的CLOCK算法。Oceanbase目前的缓存算法也是经过牺牲精确性来减小锁。前面提到,Oceanbase缓存以2MB的内存块为单位进行淘汰,最开始采用LRU策略,每次淘汰最近最少使用的item所在的2MB内存块,然而,这样作的问题是须要维护最近最少使用的item,即每次读写缓存都须要加锁。后续咱们将淘汰策略修改成:每一个2MB的内存块记录一个访问次数和一个最近访问时间,每次读取item时,若是访问次数大于全部2MB内存块访问次数的平均值,更新最近访问时间;不然,将访问次数加1。根据记录的最近访问时间淘汰2MB内存块。虽然,这个算法的缓存命中率不容易评估,可是缓存读取只须要一些原子操做,不须要加锁,大大减小了锁粒度。
  4. 批量操做。缓存命中时不须要当即更新LRU链表,而是能够将命中的item保存在线程Buffer中,积累了必定数量后一次性更新LRU链表。

LIRS思想spa

  Cache有两个问题:一个是前面提到的下降锁粒度,另外一个是提升精准度,或者称为提升命中率。LRU在大多数状况下表现是不错的,可是有以下的问题:.net

  • 顺序扫描。顺序扫描的状况下LRU没有命中状况,并且会淘汰其它将要被访问的item从而污染cache。
  • 循环的数据集大于缓存大小。若是循环访问且数据集大于缓存大小,那么没有命中状况。

  之因此会出现上述一些比较极端的问题,是由于LRU只考虑访问时间而没有考虑访问频率,而LIRS在这方面作得比较好。LIRS将数据分为两部分:LIR(Low Inner-reference Recency)和HIR(High Inner-reference Recency),其中,LIR中的数据是热点,在较短的时间内被访问了至少两次。LIRS能够当作是一种分级思想:第一级是HIR,第二级是LIR,数据先进入到第一级,当数据在较短的时间内被访问两次时成为热点数据则进入LIR,HIR和LIR内部都采用LRU策略。这样,LIR中的数据比较稳定,解决了LRU的上述两个问题。LIRS论文中提出了一种实现方式,不过咱们能够作一些变化,如能够实现两级cache,cache元素先进入第一级cache,当访问频率达到必定值(好比2)时升级到第二级,第一级和第二级均内部采用LRU进行替换。Oracle Buffer Cache中的Touch Count算法也是采用了相似的思想。

SSD与缓存

  SSD发展很快,大有取代传统磁盘之势。SSD的发展是否会使得单机缓存变得毫无必要咱们无从得知,目前,Memory + SSD + 磁盘的混合存储方案仍是比较靠谱的。SSD使用能够有以下不一样的模式:

  1. write-back:数据读写都走SSD,内存中的数据写入到SSD便可,另外有单独的线程按期将SSD中的数据刷到磁盘。典型的表明如Facebook Flashcache。
  2. write-through:数据写操做须要先写到磁盘,内存和SSD合在一块儿当作两级缓存,即cache中相对较冷的数据在SSD,相对较热的数据在内存。

  固然,随着SSD的应用,我想减小缓存锁粒度的重要性会愈来愈突出。

总结&推荐资料

  到目前为止,咱们在SSD,缓存相关优化的工做仍是比较少的。从此的一年左右时间,咱们将会投入必定的精力在系统优化上,相信到时候再来总结的时候认识会更加深入。我想,缓存相关的优化工做首先要作的是根据需求制定一个大体的评价标准,接着使用实际数据作一些实验,最终可能会同时保留两到三种实现方式或者配置略微有所不一样的缓存实现。缓存相关的推荐资料以下:

[1] Touch Count Algorithm. http://youyus.com/wp-content/uploads/resource/Shallahamer%20TC4a.pdf

[2] LIRS. http://portal.acm.org/citation.cfm?id=511340

相关文章
相关标签/搜索