Solr之缓存篇
Solr在Lucene之上开发了很多Cache功能,从目前提供的Cache类型有:
(1)filterCache
(2)documentCache
(3)fieldvalueCache
(4)queryresultCache
而每种Cache针对具体的查询请求进行对应的Cache。本文将从几个方面来阐述上述几种Cache在Solr的运用,具体如下:
(1)Cache的生命周期
(2)Cache的使用场景
(3)Cache的配置介绍
(4)Cache的命中监控
1 Cache生命周期
所有的Cache的生命周期由SolrIndexSearcher来管理,如果Cache对应的SolrIndexSearcher被重新构建都代表正在运行的Cache对象失效,而SolrIndexSearcher是否重新打开主要有几个方面影响。
(1)增量数据更新后提交DirectUpdateHandler2.commit(CommitUpdateCommand cmd),该方法代码如下:
if (cmd.optimize) { optimizeCommands.incrementAndGet(); } else { commitCommands.incrementAndGet(); if (cmd.expungeDeletes) expungeDeleteCommands.incrementAndGet(); } Future[] waitSearcher = null; if (cmd.waitSearcher) {//是否等待打开SolrIndexSearcher,一般新的Searcher会做一些预备工作,比如预热Cache waitSearcher = new Future[1]; } boolean error=true; iwCommit.lock(); try { log.info("start "+cmd); if (cmd.optimize) {//是否优化索引,一般增量数据不优化 openWriter(); writer.optimize(cmd.maxOptimizeSegments); } else if (cmd.expungeDeletes) { openWriter(); writer.expungeDeletes();//一般对于标记删除的文档进行物理删除,当然优化也能将标记删除的doc删除, //但是该方法会比优化快很多 } closeWriter();//关闭增量打开的Writer对象 callPostCommitCallbacks(); if (cmd.optimize) {//如果有listener的话会执行这部分代码 callPostOptimizeCallbacks(); } // open a new searcher in the sync block to avoid opening it // after a deleteByQuery changed the index, or in between deletes // and adds of another commit being done. core.getSearcher(true,false,waitSearcher);//该方法是重新打开Searcher的关键方法, //其中有重要参数来限定是否new open 或者reopen IndexReader. // reset commit tracking tracker.didCommit();//提供Mbean的一些状态监控 log.info("end_commit_flush"); error=false; } finally {//commlit后将一些监控置0 iwCommit.unlock(); addCommands.set(0); deleteByIdCommands.set(0); deleteByQueryCommands.set(0); numErrors.set(error ? 1 : 0); } // if we are supposed to wait for the searcher to be registered, then we should do it // outside of the synchronized block so that other update operations can proceed. if (waitSearcher!=null && waitSearcher[0] != null) { try { waitSearcher[0].get();//等待Searcher经过一系列操作,例如Cache的预热。 } catch (InterruptedException e) { SolrException.log(log,e); } catch (ExecutionException e) { SolrException.log(log,e); } } }
其中最重要的方法
core.getSearcher(true,false,waitSearcher);
再展开来看参数含义,
参数1 boolean forceNew,是否打开新的searcher对象
参数2 boolean returnSearcher,是否返回最新的searcher对象
参数3 final Future[] waitSearcher 是否等待searcher的预加工动作,也就是调用该方法的线程将会等待这个searcher对象的预加工动作,如果该searcher对象管理很多的 Cache并设置较大的预热数目,该线程将会等待较长时间才能返回。(预热,也许会很多人不了解预热的含义,我在这里稍微解释下,例如一个Cache已经 缓存了比较多的值,如果因为新的IndexSearcher被重新构建,那么新的Cache又会需要重新累积数据,那么会发现搜索突然会在一段时间性能急 剧下降,要等到Cache重新累计了一定数据,命中率才会慢慢恢复。所以这样的情形其实是不可接受的,那么我们可以做的事情就是将老Cache对应的 key,在重新构建SolrIndexSearcher返回之前将这些已经在老Cache中Key预先从磁盘重新load Value到Cache中,这样暴露出去的SolrIndexSearcher对应的Cache就不是一个内容为空的Cache。而是已经“背地”准备好 内容的Cache)
getSearcher()关于Cache有2个最重要的代码段,其一,重新构造新的SolrIndexSearcher:
newestSearcher = getNewestSearcher(false); String newIndexDir = getNewIndexDir(); File indexDirFile = new File(getIndexDir()).getCanonicalFile(); File newIndexDirFile = new File(newIndexDir).getCanonicalFile(); // reopenReaders在solrconfig.xml配置,如果为false,每次都是重新打开新的IndexReader if (newestSearcher != null && solrConfig.reopenReaders && indexDirFile.equals(newIndexDirFile)) { IndexReader currentReader = newestSearcher.get().getReader(); IndexReader newReader = currentReader.reopen();//如果索引目录没变则是reopen indexReader if (newReader == currentReader) { currentReader.incRef(); } tmp = new SolrIndexSearcher(this, schema, "main", newReader, true, true);//构建新的SolrIndexSearcher } else {//根据配置的IndexReaderFactory来返回对应的IndexReader IndexReader reader = getIndexReaderFactory().newReader(getDirectoryFactory().open(newIndexDir), true); tmp = new SolrIndexSearcher(this, schema, "main", reader, true, true);//返回构建新的SolrIndexSearcher }
在看看创建SolrIndexSearcher构造函数关于Cache的关键代码:
if (cachingEnabled) {//如果最后的参数为true代表可以进行Cache ArrayList<SolrCache> clist = new ArrayList<SolrCache>(); fieldValueCache = solrConfig.fieldValueCacheConfig==null ? null : solrConfig.fieldValueCacheConfig.newInstance(); if (fieldValueCache!=null) clist.add(fieldValueCache);//如果solrconfig配置 <fieldValueCache....,构建新的Cache filterCache= solrConfig.filterCacheConfig==null ? null : solrConfig.filterCacheConfig.newInstance(); if (filterCache!=null) clist.add(filterCache);//如果solrconfig配置 <filterCache ...,构建新的Cache queryResultCache = solrConfig.queryResultCacheConfig==null ? null : solrConfig.queryResultCacheConfig.newInstance(); if (queryResultCache!=null) clist.add(queryResultCache);//如果solrconfig配置 <queryResultCache...,构建新的Cache documentCache = solrConfig.documentCacheConfig==null ? null : solrConfig.documentCacheConfig.newInstance(); if (documentCache!=null) clist.add(documentCache);//如果solrconfig配置 <documentCache...,构建新的Cache if (solrConfig.userCacheConfigs == null) { cacheMap = noGenericCaches; } else {//自定义的Cache cacheMap = new HashMap<String,SolrCache>(solrConfig.userCacheConfigs.length); for (CacheConfig userCacheConfig : solrConfig.userCacheConfigs) { SolrCache cache = null; if (userCacheConfig != null) cache = userCacheConfig.newInstance(); if (cache != null) { cacheMap.put(cache.name(), cache); clist.add(cache); } } } cacheList = clist.toArray(new SolrCache[clist.size()]); }
其二,将老searcher对应的Cache进行预热:
future = searcherExecutor.submit( new Callable() { public Object call() throws Exception { try { newSearcher.warm(currSearcher); } catch (Throwable e) { SolrException.logOnce(log,null,e); } return null; } } );
展开看warm(SolrIndexSearcher old)方法(具体如何预热Cache将在其他文章进行详述):
public void warm(SolrIndexSearcher old) throws IOException { // Make sure this is first! filters can help queryResults execute! boolean logme = log.isInfoEnabled(); long warmingStartTime = System.currentTimeMillis(); // warm the caches in order... for (int i=0; i<cacheList.length; i++) {//遍历所有配置的Cache,将进行old-->new 的Cache预热。 if (logme) log.info("autowarming " + this + " from " + old + "\n\t" + old.cacheList[i]); this.cacheList[i].warm(this, old.cacheList[i]); if (logme) log.info("autowarming result for " + this + "\n\t" + this.cacheList[i]); } warmupTime = System.currentTimeMillis() - warmingStartTime;//整个预热所耗时间 }
到这里为止,SolrIndexSearcher进行Cache创建就介绍完毕,而Cache的销毁也是通过SolrIndexSearcher的关闭一并进行,见solrIndexSearcher.close()方法:
public void close() throws IOException { if (cachingEnabled) { StringBuilder sb = new StringBuilder(); sb.append("Closing ").append(name); for (SolrCache cache : cacheList) { sb.append("\n\t"); sb.append(cache); } log.info(sb.toString());//打印Cache状态信息,例如当前Cache命中率。累积命中率,大小等。 } else { log.debug("Closing " + name); } core.getInfoRegistry().remove(name); // super.close(); // can't use super.close() since it just calls reader.close() and that may only be called once // per reader (even if incRef() was previously called). if (closeReader) reader.decRef();//Reader对象计数减1 for (SolrCache cache : cacheList) { cache.close();//关闭Cache } // do this at the end so it only gets done if there are no exceptions numCloses.incrementAndGet(); }
OK,到这里,Cache经由SolrIndexSearcher管理的逻辑就完整介绍完毕。
2 Cache的使用场景
(1)filterCache
该Cache主要是针对用户Query中使用fq的情况,会将fq对应的查询结果放入Cache,如果业务上有很多比较固定的查询Query,例如固定状 态值,比如固定查询某个区间的Query都可以使用fq将结果缓存到Cache中。查询query中可以设置多个fq进行Cache,但是值得注意的是多 个fq都是以交集的结果返回。
另外一个最为重要的例外场景,在Solr中如果设置,useFilterForSortedQuery=true,filterCache不为空,且带有sort的排序查询,将会进入如下代码块:
if ((flags & (GET_SCORES|NO_CHECK_FILTERCACHE))==0 && useFilterForSortedQuery && cmd.getSort() != null && filterCache != null) { useFilterCache=true; SortField[] sfields = cmd.getSort().getSort(); for (SortField sf : sfields) { if (sf.getType() == SortField.SCORE) { useFilterCache=false; break; } } } // disable useFilterCache optimization temporarily if (useFilterCache) { // now actually use the filter cache. // for large filters that match few documents, this may be // slower than simply re-executing the query. if (out.docSet == null) {//在DocSet方法中将会把Query的结果也Cache到filterCache中。 out.docSet = getDocSet(cmd.getQuery(),cmd.getFilter()); DocSet bigFilt = getDocSet(cmd.getFilterList());//fq不为空将Cache结果到filterCache中。 if (bigFilt != null) out.docSet = out.docSet.intersection(bigFilt);//返回2个结果集合的交集 } // todo: there could be a sortDocSet that could take a list of // the filters instead of anding them first... // perhaps there should be a multi-docset-iterator superset = sortDocSet(out.docSet,cmd.getSort(),supersetMaxDoc);//排序 out.docList = superset.subset(cmd.getOffset(),cmd.getLen());//返回len 大小的结果集合
(2)documentCache主要是对document结果的Cache,一般而言如果查询不是特别固定,命中率将不会很高。
(3)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);展开看该方法
public static UnInvertedField getUnInvertedField(String field, SolrIndexSearcher searcher) throws IOException { SolrCache cache = searcher.getFieldValueCache(); if (cache == null) { return new UnInvertedField(field, searcher);//直接返回 } UnInvertedField uif = (UnInvertedField)cache.get(field); if (uif == null) {//第一次初始化该域对应的UnInvertedField synchronized (cache) { uif = (UnInvertedField)cache.get(field); if (uif == null) { uif = new UnInvertedField(field, searcher); cache.put(field, uif); } } } return uif; }
(4)queryresultCache 对Query的结果进行缓存,主要在SolrIndexSearcher类的getDocListC()方法中被使用,主要缓存具有 QueryResultKey的结果集。也就是说具有相同QueryResultKey的查询都可以命中cache,所以我们看看 QueryResultKey的equals方法如何判断怎么才算相同QueryResultKey:
public boolean equals(Object o) { if (o==this) return true; if (!(o instanceof QueryResultKey)) return false; QueryResultKey other = (QueryResultKey)o; // fast check of the whole hash code... most hash tables will only use // some of the bits, so if this is a hash collision, it's still likely // that the full cached hash code will be different. if (this.hc != other.hc) return false; // check for the thing most likely to be different (and the fastest things) // first. if (this.sfields.length != other.sfields.length) return false;//比较排序域长度 if (!this.query.equals(other.query)) return false;//比较query if (!isEqual(this.filters, other.filters)) return false;//比较fq for (int i=0; i<sfields.length; i++) { SortField sf1 = this.sfields[i]; SortField sf2 = other.sfields[i]; if (!sf1.equals(sf2)) return false;//比较排序域 } return true; }
从上面的代码看出,如果要命中一个queryResultCache,需要满足query、filterquery sortFiled一致才行。
3 Cache的配置介绍
要使用Solr的四种Cache,只需要在SolrConfig中配置如下内容即可:
<query> <filterCache size="300" initialSize="10" autowarmCount="300"/> <queryResultCache size="300" initialSize="10" autowarmCount="300"/> <fieldValueCache size="300" initialSize="10" autowarmCount="300" /> <documentCache size="5000" initialSize="512" autowarmCount="300"/> <useFilterForSortedQuery>true</useFilterForSortedQuery>//是否能使用到filtercache关键配置 <queryResultWindowSize>50</queryResultWindowSize>//queryresult的结果集控制 <enableLazyFieldLoading>false</enableLazyFieldLoading>//是否启用懒加载field </query>
其中size为缓存设置大小,initalSize初始化大小,autowarmCount 是最为关键的参数代表每次构建新的SolrIndexSearcher的时候需要后台线程预热加载到新Cache中多少个结果集。
那是不是这个预热数目越大就越好呢,其实还是要根据实际情况而定。如果你的应用为实时应用,很多实时应用的实现都会在很短的时间内去得到重新打开的 内存索引indexReader,而Solr默认实现就会重新打开一个新的SolrIndexSearcher,那么如果Cache需要预热的数目越多, 那么打开新的SolrIndexSearcher就会越慢,这样对实时性就会大打折扣。
但是如果设置很小。每次都打开新的SolrIndexSearcher都是空Cache,基本上那些fq和facet的查询就基本不会命中缓存。所以对实时应用需要特别注意。
4 Cache的命中监控
页面查询:
http://localhost:8080/XXXX/XXXX/admin/stats.jsp 进行查询即可:
其中 lookups 为当前cache 查询数, hitratio 为当前cache命中率,inserts为当前cache插入数,evictions从cache中踢出来的数据个数,size 为当前cache缓存数, warmuptime为当前cache预热所消耗时间,而已cumulative都为该类型Cache累计的查询,命中,命中率,插入、踢出的数目。