代码改变世界

Solr4.8.0源码分析(19)之缓存机制(二)

2014-11-30 23:42  追风的蓝宝  阅读(766)  评论(0编辑  收藏  举报

Solr4.8.0源码分析(19)之缓存机制(二)

     前文<Solr4.8.0源码分析(18)之缓存机制(一)>介绍了Solr缓存的生命周期,重点介绍了Solr缓存的warn过程。本节将更深入的来介绍下Solr的四种缓存类型,以及两种SolrCache接口实现类。

1、SolrCache接口实现类

     前文已经提到SolrCache有两种接口实现类:solr.search.LRUCache 和 solr.search.LRUCache。 那么两者具体有啥区别呢?

1.1 solr.search.LRUCache

     LRUCache具有以下几个参数:

  • size:cache中可保存的最大的项数,默认是1024
  • initialSize:cache初始化时的大小,默认是1024
  • autowarmCount:当切换SolrIndexSearcher时,可以对新生成的SolrIndexSearcher做autowarm(预热)处理。autowarmCount表示从旧的SolrIndexSearcher中取多少项来在新的SolrIndexSearcher中被重新生成,如何重新生成由CacheRegenerator实现。在4.0版本可以指定为已有cache项数的百分比,以便能更好的平衡autowarm的开销及效果。如果不指定该参数,则表示不做autowarm处理。

   实现上,LRUCache直接使用LinkedHashMap来缓存数据,由initialSize来限定cache的大小,淘汰策略也是使用LinkedHashMap的内置的LRU方式,读写操作都是对map的全局锁,所以并发性效果方面稍差。而对cache的get与put操作其实质上也就是对LinkedHashMap的put与get操作。

 1     map = new LinkedHashMap<K,V>(initialSize, 0.75f, true) {
 2         @Override
 3         protected boolean removeEldestEntry(Map.Entry eldest) {
 4           if (size() > limit) {
 5             // increment evictions regardless of state.
 6             // this doesn't need to be synchronized because it will
 7             // only be called in the context of a higher level synchronized block.
 8             evictions++;
 9             stats.evictions.incrementAndGet();
10             return true;
11           }
12           return false;
13         }
14       };

     对cache的get与put操作其实质上也就是对LinkedHashMap的put与get操作。

 1   @Override
 2   public V put(K key, V value) {
 3     synchronized (map) {
 4       if (getState() == State.LIVE) {
 5         stats.inserts.incrementAndGet();  //Cache累计插入数目
 6       }
 7 
 8       // increment local inserts regardless of state???
 9       // it does make it more consistent with the current size...
10       inserts++;  //当前cache的插入数目
11       return map.put(key,value);
12     }
13   }
14 
15   @Override
16   public V get(K key) {
17     synchronized (map) {
18       V val = map.get(key);
19       if (getState() == State.LIVE) {
20         // only increment lookups and hits if we are live.
21         lookups++;    //当前cache的查询数目
22         stats.lookups.incrementAndGet();  //Cache累计查询数目
23         if (val!=null) {
24           hits++;
25           stats.hits.incrementAndGet();
26         }
27       }
28       return val;
29     }
30   }

1.2 solr.search.FastLRUCache

    在配置方面,FastLRUCache除了需要LRUCache的参数,还可有选择性的指定下面的参数:

  • minSize:当cache达到它的最大数,淘汰策略使其降到minSize大小,默认是0.9*size。
  • acceptableSize:当淘汰数据时,期望能降到minSize,但可能会做不到,则可勉为其难的降到acceptableSize,默认是0.95*size。
  • cleanupThread:相比LRUCache是在put操作中同步进行淘汰工作,FastLRUCache可选择由独立的线程来做,也就是配置cleanupThread的时候。当cache大小很大时,每一次的淘汰数据就可能会花费较长时间,这对于提供查询请求的线程来说就不太合适,由独立的后台线程来做就很有必要。    

实现上,FastLRUCache内部使用了ConcurrentLRUCache来缓存数据,它是个加了LRU淘汰策略的ConcurrentHashMap,所以其并发性要好很多,这也是多数Java版Cache的极典型实现。

1 cache = new ConcurrentLRUCache<>(limit, minLimit, acceptableLimit, initialSize, newThread, false, null);

 

2. 缓存类型

2.1 filterCache

     filterCache存储了无序的lucene document id集合,该cache有3种用途:

  • filterCache存储了filter queries(“fq”参数)得到的document id集合结果。Solr中的query参数有两种,即q和fq。如果fq存在,Solr是先查询fq(因为fq可以多个,所以多个fq查询是个取结果交集的过程),之后将fq结果和q结果取并。在这一过程中,filterCache就是key为单个fq(类型为Query),value为document id集合(类型为DocSet)的cache。对于fq为range query来说,filterCache表现出其有价值的一面。
  • 另外一个最为重要的例外场景,在Solr中如果设置,useFilterForSortedQuery=true,filterCache不为空,且带有sort的排序查询,将会进入如下代码块: 
 1 if ((flags & (GET_SCORES|NO_CHECK_FILTERCACHE))==0 && useFilterForSortedQuery && cmd.getSort() != null && filterCache != null) {
 2       useFilterCache=true;
 3       SortField[] sfields = cmd.getSort().getSort();
 4       for (SortField sf : sfields) {
 5         if (sf.getType() == SortField.SCORE) {
 6           useFilterCache=false;
 7           break;
 8         }
 9       }
10     }
11 
12     // disable useFilterCache optimization temporarily
13     if (useFilterCache) {
14       // now actually use the filter cache.
15       // for large filters that match few documents, this may be
16       // slower than simply re-executing the query.
17       if (out.docSet == null) {//在DocSet方法中将会把Query的结果也Cache到filterCache中。
18         out.docSet = getDocSet(cmd.getQuery(),cmd.getFilter());
19         DocSet bigFilt = getDocSet(cmd.getFilterList());//fq不为空将Cache结果到filterCache中。
20         if (bigFilt != null) out.docSet = out.docSet.intersection(bigFilt);//返回2个结果集合的交集
21       }
22       // todo: there could be a sortDocSet that could take a list of
23       // the filters instead of anding them first...
24       // perhaps there should be a multi-docset-iterator
25       superset = sortDocSet(out.docSet,cmd.getSort(),supersetMaxDoc);//排序
26       out.docList = superset.subset(cmd.getOffset(),cmd.getLen());//返回len 大小的结果集合
  • filterCache还可用于facet查询,facet查询中各facet的计数是通过对满足query条件的document id集合(可涉及到filterCache)的处理得到的。因为统计各facet计数可能会涉及到所有的doc id,所以filterCache的大小需要能容下索引的文档数。这一部分暂未学习到。
  • 对于是否使用filterCache及如何配置filterCache大小,需要根据应用特点、统计、效果、经验等各方面来评估。对于使用fq、facet的应用,对filterCache的调优是很有必要的。

2.2 queryResultCache

      queryResultCache对Query的结果进行缓存,主要在SolrIndexSearcher类的getDocListC()方法中被使用,主要缓存具有 QueryResultKey的结果集。也就是说具有相同QueryResultKey的查询都可以命中cache,所以我们看看 QueryResultKey的equals方法如何判断怎么才算相同QueryResultKey:

 1   @Override
 2   public boolean equals(Object o) {
 3     if (o==this) return true;
 4     if (!(o instanceof QueryResultKey)) return false;
 5     QueryResultKey other = (QueryResultKey)o;
 6 
 7     // fast check of the whole hash code... most hash tables will only use
 8     // some of the bits, so if this is a hash collision, it's still likely
 9     // that the full cached hash code will be different.
10     if (this.hc != other.hc) return false;
11 
12     // check for the thing most likely to be different (and the fastest things)
13     // first.
14     if (this.sfields.length != other.sfields.length) return false;
15     if (!this.query.equals(other.query)) return false;
16     if (!unorderedCompare(this.filters, other.filters)) return false;
17 
18     for (int i=0; i<sfields.length; i++) {
19       SortField sf1 = this.sfields[i];
20       SortField sf2 = other.sfields[i];
21       if (!sf1.equals(sf2)) return false;
22     }
23 
24     return true;
25   }

由以上代码可以看出,如果要命中一个queryResultCache,需要满足query、filterquery sortFiled一致才行。

       因为查询参数是有startrows的,所以某个QueryResultKey可能命中了cache,但startrows却不在cachedocument id set范围内。当然,document id set是越大命中的概率越大,但这也会很浪费内存,这就需要个参数:queryResultWindowSize来指定document id set的大小。Solr中默认取值为50,可配置,WIKI上的解释很深简单明了:

1 <!-- An optimization for use with the queryResultCache.  When a search
2          is requested, a superset of the requested number of document ids
3          are collected.  For example, of a search for a particular query
4          requests matching documents 10 through 19, and queryWindowSize is 50,
5          then documents 0 through 50 will be collected and cached.  Any further
6          requests in that range can be satisfied via the cache.
7     -->
8     <queryResultWindowSize>50</queryResultWindowSize>
 1       // If we are going to generate the result, bump up to the
 2       // next resultWindowSize for better caching.
 3 
 4       if ((flags & NO_SET_QCACHE) == 0) {
 5         // handle 0 special case as well as avoid idiv in the common case.
 6         if (maxDocRequested < queryResultWindowSize) {
 7           supersetMaxDoc=queryResultWindowSize;
 8         } else {
 9           supersetMaxDoc = ((maxDocRequested -1)/queryResultWindowSize + 1)*queryResultWindowSize;
10           if (supersetMaxDoc < 0) supersetMaxDoc=maxDocRequested;
11         }
12       } else {
13         key = null;  // we won't be caching the result
14       }

     同样的queryResultCache在预热的时候也是根据queryResultWindowSize大小进行预热的。

     相比filterCache来说,queryResultCache内存使用上要更少一些,但它的效果如何就很难说。就索引数据来说,通常我们只是在索引上存储应用主键id,再从数据库等数据源获取其他需要的字段。这使得查询过程变成,首先通过solr得到document id set,再由Solr得到应用id集合,最后从外部数据源得到完成的查询结果。如果对查询结果正确性没有苛刻的要求,可以在Solr之外独立的缓存完整的查询结果(定时作废),这时queryResultCache就不是很有必要,否则可以考虑使用queryResultCache。当然,如果发现在queryResultCache生命周期内,query重合度很低,也不是很有必要开着它。     

 

2.3 documentCache

       Solr的查询主要分为两步,第一步根据查询条件获取ids,第二步根据id获取相应的具体id集合。相比于queryResultCache和filterCache存储的是键为query,值为ids这样的结构(第一步),documentCache存储的是建为id,值为具体的域(第二步)。但是实际上documentCache的缓存效果并不明显,相比于第二步,Solr的查询费时主要集中在第一步上,而且在进行commit的时候documentCache都会清零。所以对于一个commit比较频率的solr来说,documentCache的效果并不大。但是如果使用documentCache,就尽可能开大些,至少要大过<max_results> * <max_concurrent_queries>,否则因为cache的淘汰,一次请求期间还需要重新获取document一次。也要注意document中存储的字段的多少,避免大量的内存消耗。

1  private final SolrCache<Integer,Document> documentCache;

 

2.4 fieldvalueCache

    

       fieldvalueCache 缓存在facet组件使用情况下对multiValued=true的域相关计数进行Cache,一般那些多值域采用facet查询一定要开启该Cache,主要缓存(参考UnInvertedField 的实现):

  • maxTermCounts 最大Term数目
  • numTermsInField 该Field有多少个Term
  • bigTerms 存储那些Term docFreq 大于threshold的term
  • tnums 一个记录 term和何其Nums的二维数组
  • 每次FacetComponent执行process方法–>SimpleFacets.getFacetCounts()–>getFacetFieldCounts()–>getTermCounts(facetValue)–>UnInvertedField.getUnInvertedField(field, searcher);展开看该方法
 1 public static UnInvertedField getUnInvertedField(String field, SolrIndexSearcher searcher) throws IOException {
 2     SolrCache cache = searcher.getFieldValueCache();
 3     if (cache == null) {
 4       return new UnInvertedField(field, searcher);//直接返回
 5     }
 6 
 7     UnInvertedField uif = (UnInvertedField)cache.get(field);
 8     if (uif == null) {//第一次初始化该域对应的UnInvertedField
 9       synchronized (cache) {
10         uif = (UnInvertedField)cache.get(field);
11         if (uif == null) {
12           uif = new UnInvertedField(field, searcher);
13           cache.put(field, uif);
14         }
15       }
16     }
17 
18     return uif;
19   }

 

3. Cache的命中监控


可以在SolrAdmin的插件页面中对cache进行监控。

       其中 lookups 为当前cache 查询数, hitratio 为当前cache命中率,inserts为当前cache插入数,evictions从cache中踢出来的数据个数,size 为当前cache缓存数, warmuptime为当前cache预热所消耗时间,而已cumulative都为该类型Cache累计的查询,命中,命中率,插入、踢出的数目。

 

总结:

       本节主要介绍了Solr的几种缓存类型以及两种缓存实现接口,最后介绍了如何监控缓存的方法,由于目前学习的较浅,更深的缓存知识将在以后再深入介绍。