一、概述
Solr查询的核心类就是SolrIndexSearcher,每个core通常在同一时刻只由当前的SolrIndexSearcher供上层的handler使用(当切换SolrIndexSearcher时可能会有两个同时提供服务),而Solr的各种Cache是依附于SolrIndexSearcher的,SolrIndexSearcher存在则Cache生,SolrIndexSearcher亡则Cache被清空close掉。Solr在Lucene之上开发了很多Cache功能,从目前提供的Cache类型有:
(1)filterCache
(2)documentCache
(3)fieldvalueCache
(4)queryresultCache
(5)User/GenericCaches
其中1、2、3、4都是SolrCache的实现类,并且是SolrIndexSearcher的成员变量,各自有着不同的逻辑和使命。
二、SolrCache接口实现类
Solr提供了两种SolrCache接口实现类:solr.search.LRUCache和solr.search.FastLRUCache。FastLRUCache是1.4版本中引入的,其速度在普遍意义上要比LRUCache更快些。
下面是对SolrCache接口主要方法的注释:
public interface SolrCache {
/**
*Solr在解析配置文件构造SolrConfig实例时会初始化配置中的各种CacheConfig,
* 在构造SolrIndexSearcher时通过SolrConfig实例来newInstance SolrCache,
* 这会调用init方法。参数args就是和具体实现(LRUCache和FastLRUCache)相关的
* 参数Map,参数persistence是个全局的东西,LRUCache和FastLRUCache用其来统计
*cache访问情况(因为cache是和SolrIndexSearcher绑定的,所以这种统计就需要一个
* 全局的注入参数),参数regenerator是autowarm时如何重新加载cache,
*CacheRegenerator接口只有一个被SolrCache warm方法回调的方法:
*boolean regenerateItem(SolrIndexSearcher newSearcher,
*SolrCache newCache, SolrCache oldCache, Object oldKey, Object oldVal)
*/
public Object init(Map args, Object persistence, CacheRegeneratorregenerator);
/**:TODO: copy from Map */
public int size();
/**:TODO: copy from Map */
public Object put(Object key, Object value);
/**:TODO: copy from Map */
public Object get(Object key);
/**:TODO: copy from Map */
public void clear();
/**
* 新创建的SolrIndexSearcher autowarm方法,该方法的实现就是遍历已有cache中合适的
* 范围(因为通常不会把旧cache中的所有项都重新加载一遍),对每一项调用regenerator的
*regenerateItem方法来对searcher加载新cache项。
*/
void warm(SolrIndexSearcher searcher, SolrCache old) throws IOException;
/**Frees any non-memory resources */
public void close();
2.1、solr.search.LRUCache
LRU又称最近最少使用。假如缓存的容量为10,那么把缓存中的对象按访问(插入)的时间先后排序,当容量不足时,删除时间最早的。Solr中LRUCache是通过LinkedHashMap来实现的。
LRUCache可配置参数如下:
1)size:cache中可保存的最大项数,默认是1024
2)initialSize:cache初始化时容量大小,默认是1024。
3)autowarmCount:当切换SolrIndexSearcher时,可以对新生成的SolrIndexSearcher做autowarm(预热)处理。autowarmCount表示从旧的SolrIndexSearcher中取多少项来在新的SolrIndexSearcher中被重新生成,如何重新生成由CacheRegenerator实现。1.4版本的Solr中,这个autowarmCount只能取预热的项数,如果不指定该参数,则表示不做autowarm处理。
实现上,LRUCache直接使用LinkedHashMap来缓存数据,由initialSize来限定cache的大小,淘汰策略也是使用LinkedHashMap内置的LRU方式,读写操作都是对map的全局锁,所以在高并发条件下,其性能会有所影响。因此Solr用另外一种方式实现了LRUCache,即FastLRUCache。
2.2、solr.search.FastLRUCache
FastLRUCache内部采用了ConcurrentLRUCache实现,而ConcurrentLRUCache内部又采用ConcurrentHashMap实现,所以是线程安全的。缓存通过CacheEntry中的访问标记lastAccessed来维护CacheEntry被访问的先后顺序。 即每当Cache有get或者put操作,则当前CacheEntry的lastAccessed都会变成最大的(state.accessCounter)。当FastLRUCache容量已满时,通过markAndSweep方式来剔除缓存中lastAccessed最小的N个项以保证缓存的大小达到一个acceptable的值。
在配置方面,FastLRUCache除了需要LRUCache的参数,还可有选择性的指定下面的参数:
1)minSize:当cache达到它的最大数,淘汰策略使其降到minSize大小,默认是0.9*size。
2)acceptableSize:当淘汰数据时,期望能降到minSize,但可能会做不到,则可勉为其难的降到acceptableSize,默认是0.95*size。
3)cleanupThread:相比LRUCache是在put操作中同步进行淘汰工作,FastLRUCache可选择由独立的线程来做,即通过配置cleanupThread来实现。当cache大小很大时,每一次的淘汰数据就可能会花费较长时间,这对于提供查询请求的线程来说就不太合适,由独立的后台线程来做就很有必要。
实现上,FastLRUCache内部使用了ConcurrentLRUCache来缓存数据,它是个加了LRU淘汰策略的ConcurrentHashMap,所以其并发性要好很多,这也是多数Java版Cache的极典型实现。
三 、 Cache原理及配置
3.1 filterCache
该Cache主要是针对用户Query中使用fq(filter query)的情况,会将fq对应的查询结果放入Cache,如果业务上有很多比较固定的查询Query,例如固定状态值,比如固定查询某个区间的Query都可以使用fq将结果缓存到Cache中。查询query中可以设置多个fq进行Cache,但是值得注意的是多个fq都是以交集的结果返回。
filterCache存储了无序的lucenedocument id集合,该cache有3种用途:
1)filterCache存储了filterqueries(“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表现出其有价值的一面。
2)filterCache还可用于facet查询(http://wiki.apache.org/solr/SolrFacetingOverview),facet查询中各facet的计数是通过对满足query条件的documentid集合(可涉及到filterCache)的处理得到的。因为统计各facet计数可能会涉及到所有的doc id,所以filterCache的大小需要能容下索引的文档数。
3)如果solfconfig.xml中配置了<useFilterForSortedQuery/>,那么如果查询有filter(此filter是一需要过滤的DocSet,而不是fq,我未见得它有什么用),则使用filterCache。
配置示例:
<!-- Internal cache used bySolrIndexSearcher for filters (DocSets),
unordered sets of *all* documents that match a query.
When a new searcher is opened, its caches may be prepopulated
or "autowarmed" using data from caches in the old searcher.
autowarmCount is the number of items to prepopulate. For LRUCache,
the prepopulated items will be the most recently accessed items.
-->
<filterCache
class="solr.LRUCache"
size="16384"
initialSize="4096"
autowarmCount="4096"/>
对于是否使用filterCache及如何配置filterCache大小,需要根据应用特点、统计、效果、经验等各方面来评估。对于使用fq、facet的应用,对filterCache的调优是很有必要的。
3.2、documentCache
顾名思义,documentCache是用来保存<doc_id,document>对的。如果使用documentCache,就尽可能开大些,至少要大过<max_results> * <max_concurrent_queries>,否则因为cache的淘汰,一次请求期间还需要重新获取document一次。也要注意document中存储的字段的多少,避免大量的内存消耗。documentCache主要是对document结果的Cache,一般而言如果查询不是特别固定,命中率将不会很高。
配置示例:
<!-- documentCache caches Lucene Document objects (the stored fields for each document).
-->
<documentCache
class="solr.LRUCache"
size="16384"
initialSize="16384"/>
3.3、queryResultCache
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 fastestthings)
// 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、filterquerysortFiled一致才行。因为查询参数是有start和rows的,所以某个QueryResultKey可能命中了cache,但start和rows却不在cache的document id set范围内。当然,document id set是越大命中的概率越大,但这也会很浪费内存,这就需要个参数:queryResultWindowSize来指定document id set的大小。
相比filterCache来说,queryResultCache内存使用上要更少一些,但它的效果如何就很难说。就索引数据来说,通常我们只是在索引上存储应用主键id,再从数据库等数据源获取其他需要的字段。这使得查询过程变成,首先通过solr得到document id set,再由Solr得到应用id集合,最后从外部数据源得到完成的查询结果。如果对查询结果正确性没有苛刻的要求,可以在Solr之外独立的缓存完整的查询结果(定时作废),这时queryResultCache就不是很有必要,否则可以考虑使用queryResultCache。当然,如果发现在queryResultCache生命周期内,query重合度很低,也不是很有必要开着它。
配置示例:
<!-- queryResultCache caches results of searches - ordered lists of
document ids (DocList) based on a query, a sort, and the range
of documents requested.
-->
<queryResultCache
class="solr.LRUCache"
size="16384"
initialSize="4096"
autowarmCount="1024"/>
3.4 filterValueCache
fieldValueCache在SolrIdexSearcher的定义如下:
SolrCache<String,UnInvertedField> fieldValueCache; 其中key代表FieldName,value是一种数据结构UnInvertedField。 fieldValueCache在solr中只用于multivalued Field。一般用到它的就是facet操作。关于这个缓存需要注意的是,如果没有在solrconfig.xml中配置,那么它是默认存在的(初始大小10,最大10000,不会autowarm)会有内存溢出的隐患。
由于该cache的key为FieldName,而一般一个solrCore中的字段最多也不过几百。在这么多字段中,multivalued 字段会更少,会用到facet操作的则少之又少。所以在solrconfig.xml中的配置不必过大,大了也是浪费。
该缓存存储排序好的docIds,一般是topN。这个缓存占用内存会比filterCache小。因为它存储的是topN。但是如果QueryCommand中带有filter(DocSet类型),那么该缓存不会起作用。主要因为DocSet在执行hashcode和equals方法时比较耗时。
3.5 User/Generic Caches
Solr支持自定义Cache,只需要实现自定义的regenerator即可,下面是配置示例:
<!-- Example of a generic cache. These caches may be accessed by name
through SolrIndexSearcher.getCache(),cacheLookup(), and cacheInsert().
The purpose is to enable easy caching of user/application level data.
The regenerator argument should be specified as an implementation
of solr.search.CacheRegenerator if autowarming is desired.
-->
<!--
<cache name="yourCacheNameHere"
class="solr.LRUCache"
size="4096"
initialSize="2048"
autowarmCount="4096"
regenerator="org.foo.bar.YourRegenerator"/>
-->
四、solr基于Cache 的优化
solr应用中为了提高查询速度有可以利用几种cache来优化查询速度,在日常使用中最为立竿见影,在这章节中基于之前做过的一个职位搜索的例子来介绍两个基于cache的优化,职位搜索截取集群中一个collection节点作为示例,数据量78万。
4.1查询结果缓存应用
将solrconfig.xml中所有的缓存配置都去掉,此时搜索条件为:jobsName:软件工程师 and educateBackground:本科 and workPlace:济南
查询结果如下图:
在solrconfig.xml中开启查询结果缓存配置
<queryResultCache class="solr.LRUCache" size="512" initialSize="512"autowarmCount="0"/>
<documentCache class="solr.LRUCache" size="512" initialSize="512" autowarmCount="0"/>
<queryResultMaxDocsCached>1024</queryResultMaxDocsCached>
从上面结果可以很明显看出查询走缓存的效果还是非常明显的,并且不用担心数据更新然后再次打开SolrIndexSearcher后缓存不一致的情况。当切换SolrIndexSearcher时,可以对新生成的SolrIndexSearcher做autowarm(预热)处理,会从旧的SolrIndexSearcher中取出原来缓存的项在新的SolrIndexSearcher中重新生成。
4.2基于filterCache查询优化
在常规的搜索查询中,查询经常会带一些基础查询条件,比如在职位搜索中经常会基于学历为本科,职位类型是全职等为前提条件的查询,这些前提条件变动小并且每次查询都要带上,这个时候filtercache就能很好的发挥出它的优势了。
为了验证filtercache,首先去除所有cache配置,重启搜索服务,在不使用任何cache的情况下不同搜索条件的耗时如下:
查询条件 |
查询耗时(ms) |
jobDescrip: 文案编辑 and employeeSex:不限 and jobsType:全职and educateBackground:本科 and workPlace:深圳 and salary:面议 |
114 |
jobDescrip: 工程师 and employeeSex:不限 and jobsType:全职and educateBackground:本科 and workPlace:深圳 and salary:面议 |
147 |
jobDescrip: 高级软件开发工程师 and employeeSex:不限 and jobsType:全职and educateBackground:本科 and workPlace:深圳 and salary:面议 |
210 |
在去除其他cache配置条件下只添加filtercache配置:
<filterCacheclass="solr.FastLRUCache" size="512"
initialSize="512"
autowarmCount="0"/>
当查询条件为:jobDescrip: 软件工程师,其中filterquery为: employeeSex:不限 andjobsType:全职 and educateBackground:本科 and workPlace:深圳 and salary:面议。此时查询如下(重启后首次查询的耗时包含了初次搜索时基础数据的初始化):
接着进行二次搜索,搜索效果如下:
通过如下缓存命中监控图可以看出第二次搜索filtercache直接命中,命中率为50%。
此时在filterquery条件不变的情况下,只改变基础搜索词:jobDescrip:文案编辑
通过上图可以看出查询时间大大降低,此时再查看filtercache缓存命中率,总共命中两次,并且第二、三查询速度明显加快
但是在使用filterquery的时候需要注意query跟filterquery中不能有重复的字段,
例如:
Query=jobDescrip: 软件工程师and employeeSex:不限
Filterquery=employeeSex:不限 andjobsType:全职 and educateBackground:本科 and workPlace:深圳 and salary:面议
如上,条件中(employeeSex:不限)在query和filterquery中都会出现,这样的写法非但起不到查询优化的目的,而且还会增加查询的性能开销。
参考资料
Grainger T, Potter T, Seeley Y. Solr inaction[M]. Cherry Hill: Manning, 2014.
Kuć R. Apache Solr 4 Cookbook[M]. PacktPublishing Ltd, 2013.
https://wiki.apache.org/solr/SolrCaching#User.2FGeneric_Caches