撇开具体场景,将cache看做一个工具类,无上下文依赖的cache方法,这时LRUCache FastLRUcache LFUcache就是分析对象了,其中get、put、remove实现就是差异和性能所在。这一块具有很强的通用性。见分析1
分析1:基本cache
分析Solr的LRUCache、FASTLRUCache、LFUCache的remove实现,调研开源cache EhCache
分析2:有上下文cache
分析Lucene的Buffer,
分析1对应源码参考Solr42的实现。
LRUcache,put 时候对LinkedHashMap做了synchronized处理,get时候也做了同步处理,put、get之前都先对统计计数做++
Warm依赖SolrIndexSearcher来注入,也就是每一个IndexSearcher对象关联一个LRUcache。
Warm过程是cache对象遍历拷贝过程。Warm项越多,warm时间越长,上一次的SolrIndexSearcher驻留时间越长,相关cache等资源内存逗留时间越长,压力上来后,会new SolrIndexSearcher,然后占用新的内存,恶性循环后,容易出现OOM。
另外一种场景,就是查询一次占用时间更长的,也会不停的new SolrIndexSearcher,出现慢了以后恶性循环下的OOM、load增加。
FastLURCache 支持异步线程cleanup
Only applicable for FastLRUCache. Default is set to false. If set to true, the cleanup will be run in a dedicated separate thread. Consider setting this to true for very large cache sizes, as the cache cleanup (triggered when the cache size reaches the upper water mark) can take some time.
Cache 是ConcurrentLRUCache,put,get没有同步外层,回到
ConcurrentLRUCache, map ConcurrentHashMap,采取重入锁(ReentrantLock)是一种递归无阻塞的同步机制。有存活性判断。
LRUCache和FastLRUCache两种Cache实现是两种很不同的思路。两者的相同点是,都使用了现成的Map来维护数据。不同点是如何来淘汰数据。LRUCache(也就是LinkedHashMap)格外维护了一个结构,在做存取操作时同时更新该结构,优点在于淘汰操作是O(1)的,缺点 是需要对存取操作加互斥锁。FastLRUCache正相反,它没有额外维护新的结构,可以由ConcurrentHashMap支持并发读,但put操 作中如果需要淘汰数据,淘汰过程是O(n)的,因为整个过程不加锁,这也只会影响该次put的性能,而FastLRUCache也可选成起独立线程异步执 行来降低影响。而另一个Cache实现Ehcache,它在淘汰数据就是同步的,不过它限定了每次淘汰数据的大小(通常都少于5个),所以同步情况下性能 不会太受影响。
LFUCache
SolrCache based on ConcurrentLFUCache implementation
* This implementation does not use a separate cleanup thread. Instead it uses the calling thread
* itself to do the cleanup when the size of the cache exceeds certain limits.
LFU替换实现,通过命中计数,LRU通过访问技术。使用TreeSet保存需要move的元素。第一阶段 遍历map,执行命中数减半,当treeSet元素小于wantoRemove的,add进入treeset,然后遍历tressset执行ConcurrentHashMap的 remove 操作。在没有达到wantuRemove数目前,Treeset会填满元素,接下来的元素,会与Treeset first元素也就最小的元素做比较,替换大的,小的进入。进入第二阶段之前,treeSet里面就是需要remove的元素了。
这一部分除了同样的属性:get、put、remove之外,就是cache这个具体媒介的分配源。也就是从heap、off heap(locale cache上 unsafe分配)、SSDCache分配来划分(不一定合理,凑合着理解了)。其中放置locale cache的很大一个特点就是GC看不到他,是说只有FGC heap对象回收,此时才会释放堆外内存,正常GC扫描过程不会对堆外内存做标记、碎片处理等工作,堆外内存主动申请、管理可以大大提升内存领用率。另外一个就是 locale cache在大块内存分配、缓存上有优势。面向字节存取,一般对象须有序列化和反序列化。不论那种形式存取本地cache的字节内容,最终都是需要相应一份堆内空间在计算过程中,也就是说本地cache一定是在堆cache之下。如果有序列化的,需要配合堆内cache执行反序列化获取的对象缓存,从而平衡解压与访问性能。参考测试性能
http://mentablog.soliveirajr.com/2012/11/which-one-is-faster-java-heap-or-native-memory/
分析2 源码参考solrlucen42 elasticsearch
从图中可以看到,cache从业务层逐渐向下,遍布各个需要性能提升的地方。在具体场景下:
warm控制、oom控制、分布式共享、以及具体场景下cache参数设置是业务环境下需要思考的。
对于上面的cache模块:
所有cache参数中配置的size是item数量,不是真实内存大小,所有大请求时候极易出现OOM。
其中queryresultcache是排序后的结果、有状态的,
FilteQueryCache、DocuemntCache无状态,
FilteQueryCache可能存在“滞后”,当新文档到来后,cache没有warm copy 老的,而倒排链已经更新,发生频率低但存在这个风险,新的版本已经做bug修复。filterquery保存的是无排序的命中的doclist,对内存消耗相对较低些。
DocuementCache 保存的是document id 对于的document store field的内容。如果删除了doc,查询的时候会过滤,从而不会访问到。依然存在风险是当document的某些text域很大,可能几个docuement就撑爆了内存。
RangeFieldCache大块分配和主动释放,这一块是针对翻页、大内存申请时候的优化。
FieldValueCache内存消耗最大、命中率最高通常。
最底层bytes OS page FScache等对用户来说要求透明的。其中索引写和merge时候,dataBuffer大小不一样的。默认是1024,而merge时候是4096.
lucene 4*以后,面向byteref,fieldvaluecache缓存的是byte 对象,命中率和内存利用率都极大提升。
在elasticsearch org.apache.lucene.store.buffer.ByteBufferDirectory中,实现了基于堆外内存初始化索引。
DocSetCache
DocSetCacheModule里面初始化SimpleDocSetCache 针对SegmentReader层
IdCache
IdCacheModule里面初始化SimpleIdCache 针对IndexReader层的缓存
Queryparsercache针对shard搜索时候,同一个结点,此时query解析共享
Indexcache\Filtecache都有相应的场景
--------------------------------------------------------------
至此,基本上认识了cache的具体工具类应该关心的get、put、remove实现,以及业务场景下,solr lucene elasticsearch中 在cache层做的实现。这些都是值得学习、借鉴的。另外,开源的Ehcache 在cache 独立设计上,有不少亮点,值得深入学习。这里推荐文档: