本文將介紹Solr查詢中涉及到的Cache使用及相關的實現。Solr查詢的核心類就是SolrIndexSearcher,每個core通常在同一時刻只由當前的SolrIndexSearcher供上層的handler使用(當切換SolrIndexSearcher時可能會有兩個同時提供服務),而Solr的各種Cache是依附於SolrIndexSearcher的,SolrIndexSearcher在則Cache生,SolrIndexSearcher亡則Cache被清空close掉。Solr中的應用Cache有filterCache、queryResultCache、documentCache等,這些Cache都是SolrCache的實現類,並且是SolrIndexSearcher的成員變量,各自有着不同的邏輯和使命,下面分別予以介紹和分析。
1、SolrCache接口實現類
Solr提供了兩種SolrCache接口實現類:solr.search.LRUCache和solr.search.FastLRUCache。FastLRUCache是1.4版本中引入的,其速度在普遍意義上要比LRUCache更fast些。
下面是對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, CacheRegenerator regenerator);
/** :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();
}
1.1、solr.search.LRUCache
LRUCache可配置參數如下:
1)size:cache中可保存的最大的項數,默認是1024
2)initialSize:cache初始化時的大小,默認是1024。
3)autowarmCount:當切換SolrIndexSearcher時,可以對新生成的SolrIndexSearcher做autowarm(預熱)處理。autowarmCount表示從舊的SolrIndexSearcher中取多少項來在新的SolrIndexSearcher中被重新生成,如何重新生成由CacheRegenerator實現。在當前的1.4版本的Solr中,這個autowarmCount只能取預熱的項數,將來的4.0版本可以指定為已有cache項數的百分比,以便能更好的平衡autowarm的開銷及效果。如果不指定該參數,則表示不做autowarm處理。
實現上,LRUCache直接使用LinkedHashMap來緩存數據,由initialSize來限定cache的大小,淘汰策略也是使用LinkedHashMap的內置的LRU方式,讀寫操作都是對map的全局鎖,所以並發性效果方面稍差。
1.2、solr.search.FastLRUCache
在配置方面,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的極典型實現。
2、filterCache
filterCache存儲了無序的lucene document id集合,該cache有3種用途:
1)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表現出其有價值的一面。
2)filterCache還可用於facet查詢(http://wiki.apache.org/solr/SolrFacetingOverview),facet查詢中各facet的計數是通過對滿足query條件的document id集合(可涉及到filterCache)的處理得到的。因為統計各facet計數可能會涉及到所有的doc id,所以filterCache的大小需要能容下索引的文檔數。
3)如果solfconfig.xml中配置了<useFilterForSortedQuery/>,那么如果查詢有filter(此filter是一需要過濾的DocSet,而不是fq,我未見得它有什么用),則使用filterCache。
下面是filterCache的配置示例:
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"
對於是否使用filterCache及如何配置filterCache大小,需要根據應用特點、統計、效果、經驗等各方面來評估。對於使用fq、facet的應用,對filterCache的調優是很有必要的。
3、queryResultCache
顧名思義,queryResultCache是對查詢結果的緩存(SolrIndexSearcher中的cache緩存的都是document id set),這個結果就是針對查詢條件的完全有序的結果。下面是它的配置示例:
<!-- queryResultCache caches results of searches - ordered lists of
of documents requested.
-->
< queryResultCache
class ="solr.LRUCache"
size ="16384"
initialSize ="4096"
autowarmCount ="1024" />
緩存的key是個什么結構呢?就是下面的類(key的hashcode就是QueryResultKey的成員變量hc):
this.query = query;
this.sort = sort;
this.filters = filters;
this.nc_flags = nc_flags;
int h = query.hashCode();
if (filters != null) h ^= filters.hashCode();
sfields = ( this.sort != null) ? this.sort.getSort() : defaultSort;
for (SortField sf : sfields) {
// mix the bits so that sortFields are position dependent
// so that a,b won't hash to the same value as b,a
h ^= (h << 8) | (h >>> 25); // reversible hash
if (sf.getField() != null) h += sf.getField().hashCode();
h += sf.getType();
if (sf.getReverse()) h=~h;
if (sf.getLocale()!= null) h+=sf.getLocale().hashCode();
if (sf.getFactory()!= null) h+=sf.getFactory().hashCode();
}
hc = h;
}
因為查詢參數是有start和rows的,所以某個QueryResultKey可能命中了cache,但start和rows卻不在cache的document id set范圍內。當然,document id set是越大命中的概率越大,但這也會很浪費內存,這就需要個參數:queryResultWindowSize來指定document id set的大小。Solr中默認取值為50,可配置,WIKI上的解釋很深簡單明了:
is requested, a superset of the requested number of document ids
are collected. For example, of a search for a particular query
requests matching documents 10 through 19, and queryWindowSize is 50,
then documents 0 through 50 will be collected and cached. Any further
requests in that range can be satisfied via the cache.
-->
< queryResultWindowSize >50 </ queryResultWindowSize >
相比filterCache來說,queryResultCache內存使用上要更少一些,但它的效果如何就很難說。就索引數據來說,通常我們只是在索引上存儲應用主鍵id,再從數據庫等數據源獲取其他需要的字段。這使得查詢過程變成,首先通過solr得到document id set,再由Solr得到應用id集合,最后從外部數據源得到完成的查詢結果。如果對查詢結果正確性沒有苛刻的要求,可以在Solr之外獨立的緩存完整的查詢結果(定時作廢),這時queryResultCache就不是很有必要,否則可以考慮使用queryResultCache。當然,如果發現在queryResultCache生命周期內,query重合度很低,也不是很有必要開着它。
4、documentCache
又顧名思義,documentCache用來保存<doc_id,document>對的。如果使用documentCache,就盡可能開大些,至少要大過<max_results> * <max_concurrent_queries>,否則因為cache的淘汰,一次請求期間還需要重新獲取document一次。也要注意document中存儲的字段的多少,避免大量的內存消耗。
下面是documentCache的配置示例:
-->
< documentCache
class ="solr.LRUCache"
size ="16384"
initialSize ="16384" />
5、User/Generic Caches
Solr支持自定義Cache,只需要實現自定義的regenerator即可,下面是配置示例:
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"/>
-->
6、The Lucene FieldCache
lucene中有相對低級別的FieldCache,Solr並不對它做管理,所以,lucene的FieldCache還是由lucene的IndexSearcher來搞。
7、autowarm
上面有提到autowarm,autowarm觸發的時機有兩個,一個是創建第一個Searcher時(firstSearcher),一個是創建個新Searcher(newSearcher)來代替當前的Searcher。在Searcher提供請求服務前,Searcher中的各個Cache可以做warm處理,處理的地方通常是SolrCache的init方法,而不同cache的warm策略也不一樣。
1)filterCache:filterCache注冊了下面的CacheRegenerator,就是由舊的key查詢索引得到新值put到新cache中。
new CacheRegenerator() {
public boolean regenerateItem(SolrIndexSearcher newSearcher, SolrCache newCache, SolrCache oldCache, Object oldKey, Object oldVal) throws IOException {
newSearcher.cacheDocSet((Query)oldKey, null, false);
return true;
}
}
);
2)queryResultCache:queryResultCache的autowarm不在SolrCache的init(也就是說,不是去遍歷已有的queryResultCache中的query key執行查詢),而是通過SolrEventListener接口的void newSearcher(SolrIndexSearcher newSearcher, SolrIndexSearcher currentSearcher)方法,來執行配置中特定的query查詢,達到顯示的預熱lucene FieldCache的效果。
queryResultCache的配置示例如下:
< arr name ="queries" >
<!-- seed common sort fields -->
< lst > < str name ="q" >anything </ str > < str name ="sort" >name desc price desc populartiy desc </ str > </ lst >
</ arr >
</ listener >
< listener event ="firstSearcher" class ="solr.QuerySenderListener" >
< arr name ="queries" >
<!-- seed common sort fields -->
< lst > < str name ="q" >anything </ str > < str name ="sort" >name desc, price desc, populartiy desc </ str > </ lst >
<!-- seed common facets and filter queries -->
< lst > < str name ="q" >anything </ str >
< str name ="facet.field" >category </ str >
< str name ="fq" >inStock:true </ str >
< str name ="fq" >price:[0 TO 100] </ str >
</ lst >
</ arr >
</ listener >
3)documentCache:因為新索引的document id和索引文檔的對應關系發生變化,所以documentCache沒有warm的過程,落得白茫茫一片真干凈。
盡管autowarm很好,也要注意autowarm帶來的開銷,這需要在實際中檢驗其warm的開銷,也要注意Searcher的切換頻率,避免因為warm和切換影響Searcher提供正常的查詢服務。
8、參考文章
http://wiki.apache.org/solr/SolrCaching