Solrのキャッシュについて調査したことまとめ

モニタリング方法

WEBブラウザから確認したい場合

http://<solr_host>:<solr_port>/solr/#/<Core or Collection>/plugins?type=cache

jsonxml等で取得したい場合

http://<solr_host>:<solr_port>/solr/<Core or Collection>/admin/mbeans?cat=CACHE&stats=true
項目 説明 単位
evictions キャッシュからのエントリ追放数 エントリ数
hitratio キャッシュのヒット率(hits/lookup) 割合※パーセンテージではない(0.00〜1.00)
hits キャッシュのヒット回数 回数
inserts キャッシュへのエントリ挿入回数 回数
lookups キャッシュに対するエントリ検索回数 回数
size キャッシュサイズ エントリ数
warmupTime キャッシュのauto-warming時間 ミリ秒

cumulative_xxxは前回のコアのリロード等のキャッシュクリア時からの累積値です。

これに加えて後述の showItems を属性に設定している場合はキャッシュ中身が表示されます。

パフォーマンスはデータ特性やシステムリソースによって最適値が異なるという大前提の上、特に注目すべき指標は evictionshitratio です。evictionsが増えているということはキャッシュが小さすぎるという可能性もあります。hitratioが低すぎる場合 、キャッシュオフの方がキャッシュ処理のオーバーヘッドが無くなる分早いという可能性もあります。

キャッシュの実装の種類

LRUCache と FastLRUCache

Least Recently Usedのアルゴリズムでキャッシュからevictionされるドキュメントを決定します。

FastLRUCacheはキャッシュに保存(put)するドキュメントのKey,Valueをjava.util.concurrent.ConcurrentHashMapで実装したものになります。

設定ファイル内のヘルプによると、FastLRUCacheはgetが早く、putが遅くなります。hitratioが高い環境、マルチコアCPUの環境など速度向上が見込めるとあります。

LRUCacheとFastLRUCacheの違いは上記のパフォーマンスだけでなく設定可能な値にもあります、例えば LRUCacche ではstats画面でキャッシュ内のアイテムが表示可能になる showItems 設定が使えません。この辺りは後述します。

LFUCache

Least Frequently Usedでキャッシュからevictionされるドキュメントを決定します。キャッシュアルゴリズム以外(設定値の項目など)はFastLRUCacheに近いです。

LRUとLFUのアルゴリズムの違い

両方とも基本的なロジックですが、動きどんなだっけという時には以下のテストコードがわかりやすいです。

lucene-solr/solr/core/src/test/org/apache/solr/search/TestFastLRUCache.java

  public void testOldestItems() {
    ConcurrentLRUCache<Integer, String> cache = new ConcurrentLRUCache<>(100, 90);
    for (int i = 0; i < 50; i++) {
      cache.put(i + 1, "" + (i + 1));
    }
    cache.get(1);
    cache.get(3);
    Map<Integer, String> m = cache.getOldestAccessedItems(5);
    //7 6 5 4 2
    assertNotNull(m.get(7));
    assertNotNull(m.get(6));
    assertNotNull(m.get(5));
    assertNotNull(m.get(4));
    assertNotNull(m.get(2));

    m = cache.getOldestAccessedItems(0);
    assertTrue(m.isEmpty());

    //test this too
    m = cache.getLatestAccessedItems(0);
    assertTrue(m.isEmpty());

    cache.destroy();
  }

lucene-solr/solr/core/src/test/org/apache/solr/search/TestLFUCache.java

  public void testItemOrdering() {
    ConcurrentLFUCache<Integer, String> cache = new ConcurrentLFUCache<>(100, 90);
    try {
      for (int i = 0; i < 50; i++) {
        cache.put(i + 1, "" + (i + 1));
      }
      for (int i = 0; i < 44; i++) {
        cache.get(i + 1);
        cache.get(i + 1);
      }
      cache.get(1);
      cache.get(1);
      cache.get(1);
      cache.get(3);
      cache.get(3);
      cache.get(3);
      cache.get(5);
      cache.get(5);
      cache.get(5);
      cache.get(7);
      cache.get(7);
      cache.get(7);
      cache.get(9);
      cache.get(9);
      cache.get(9);
      cache.get(48);
      cache.get(48);
      cache.get(48);
      cache.get(50);
      cache.get(50);
      cache.get(50);
      cache.get(50);
      cache.get(50);

      Map<Integer, String> m;

      m = cache.getMostUsedItems(5);
      //System.out.println(m);
      // 50 9 7 5 3 1
      assertNotNull(m.get(50));
      assertNotNull(m.get(9));
      assertNotNull(m.get(7));
      assertNotNull(m.get(5));
      assertNotNull(m.get(3));

      m = cache.getLeastUsedItems(5);
      //System.out.println(m);
      // 49 47 46 45 2
      assertNotNull(m.get(49));
      assertNotNull(m.get(47));
      assertNotNull(m.get(46));
      assertNotNull(m.get(45));
      assertNotNull(m.get(2));

      m = cache.getLeastUsedItems(0);
      assertTrue(m.isEmpty());

      //test this too
      m = cache.getMostUsedItems(0);
      assertTrue(m.isEmpty());
    } finally {
      cache.destroy();
    }
  }

キャッシュの種類

種類

  • filterCache/フィルタキャッシュ
  • queryResultCache/検索結果キャッシュ
  • documentCache/ドキュメントキャッシュ
  • fieldValueCache/フィールド値キャッシュ

キャッシュの画面上には fieldCache というのもありますが、これはLucene側のキャッシュでSolrの管理化にはありませんので、ここでは除外します。

ただ、Solr 6系使っていて、fieldのdocValuesが true であればdocValuesの方が使われるので、fieldCacheは使われないはずです。 docValuesがfalseの場合は使われていたとしても以下のようなクラスが格納されていることがキャッシュの管理画面から見られると思います。(これはDOUBLEの場合)

long,org.apache.lucene.uninverting.FieldCache.LEGACY_DOUBLE_PARSER=>org.apache.lucene.uninverting.FieldCacheImpl$LongsFromArray

これはLuceneUninvertingReader というクラスを経由しているのですが、このクラス自体も含めて Deprecated で 7.0系では消えるという話もあります。

[LUCENE-7283] Move SlowCompositeReaderWrapper and uninverting package to solr sources - ASF JIRA

lucene-solr/UninvertingReader.java at releases/lucene-solr/6.6.0 · apache/lucene-solr · GitHub

DocValuesについてはこちらに調べたこと書きました

yomon.hatenablog.com

設定項目

属性 説明 デフォルト値 LRU FastLRU LFU
class キャッシュの実装を指定 solr.LRUCache
size 上限サイズ(エントリ数指定) 1024
initialSize 初期サイズ(エントリ数指定) size で指定した値
autowarmCount 自動ウォームアップ対象のエントリ数(LFUCache以外はパーセント指定も可能 "90%") 0(disable)
maxRamMB ヒープメモリサイズの上限値(MB) Long.MAX_VALUE - -
minSize 最小サイズ(エントリ数指定) size で指定した値の90% -
acceptableSize キャッシュのクリーンアップ閾値(詳細は後述) size で指定した値の95% -
cleanupThread キャッシュのクリーンアップ処理を別スレッドで行う(true or false) false -
showItems statsで指定したエントリ数のキャッシュの中身を表示 アイテム数を指定。FastLRUCacheとLFUCacheのみ指定可 -
timeDecay クリーンアップ処理時に既存のエントリのLFUで使われるヒットカウントを下げる(入れ替えが早まる) true - -

FastLRUCacheとLFUCacheのキャッシュサイズ制御の動き

FastLRUCacheLFUCache は設定項目が多いためキャッシュ制御の動きの概要を整理しておきます。

  • initialSize でキャッシュ確保
  • 最大で size までキャッシュにエントリを保存
  • エントリをキャッシュに保存する際に size を超えてしまっていた場合クリーンアップを処理を実行
  • cleanupThreadtrue の場合クリーンアップ処理は専用の別スレッドで行われる
  • クリーンアップ処理は minSize をターゲットに進められるが十分なエントリが削除できなかった場合は acceptableSize をターゲットに更にクリーンアップ処理を進める(ソース見る限りにおいてはこれが機能してるのはFastLRUのみな気がします)

filterCache

フィルタキャッシュはフィルタクエリ(fq)の絞り込み条件と、条件に合うドキュメントID(int)の集合をキャッシュ。facet.methodが enum の場合などのファセット処理にも使われます。何がキャッシュされているかは showItems で確かめるとわかりやすいです。

  • キャッシュ内部の例
item_price:[1000.0 TO 4999.0]    :org.apache.solr.search.DocSet
  • 設定例
<filterCache class="solr.LRUCache"
             size="512"
             initialSize="512"
             autowarmCount="128"/>
useFilterForSortedQuery

Scoreを利用しないSortにはFilterCacheを適用する設定らしいです。デフォルトでコメントアウトされていますが、特種なシチュエーションでは役に立つかもです。

    <!-- Use Filter For Sorted Query
 
         A possible optimization that attempts to use a filter to
         satisfy a search.  If the requested sort does not include
         score, then the filterCache will be checked for a filter
         matching the query. If found, the filter will be used as the
         source of document ids, and then the sort will be applied to
         that.
 
         For most situations, this will not be useful unless you
         frequently get the same search repeatedly with different sort
         options, and none of them ever use "score"
      -->
    <!--
       <useFilterForSortedQuery>true</useFilterForSortedQuery>
      -->

コードにも注意書きがある。

    // OK, so now we need to generate an answer.
    // One way to do that would be to check if we have an unordered list
    // of results for the base query. If so, we can apply the filters and then
    // sort by the resulting set. This can only be used if:
    // - the sort doesn't contain score
    // - we don't want score returned.

    // check if we should try and use the filter cache
    boolean useFilterCache = false;
    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.Type.SCORE) {
          useFilterCache = false;
          break;
        }
      }
    }

https://github.com/apache/lucene-solr/blob/releases/lucene-solr/6.6.0/solr/core/src/java/org/apache/solr/search/SolrIndexSearcher.java#L1356-L1375

queryResultCache

クエリ内の q sort fq(filtered query) の3パラメータ取得したハッシュをキーとして、検索結果のドキュメントID(int)をリスト構造(DocList)でキャッシュに保持します。並び順も保持されます。

* <field count per user query

  • キャッシュ内部の例
org.apache.solr.search.QueryResultKey : org.apache.solr.search.DocList
  • 設定例
<queryResultCache class="solr.LRUCache"
                  size="512"
                  initialSize="512"
                  autowarmCount="128"
                  maxRamMB="1000"/>

他にも設定項目あります。

queryResultMaxDocsCached

solrconfig.xmlに設定します。

    <!-- Maximum number of documents to cache for any entry in the
         queryResultCache. 
      -->
    <queryResultMaxDocsCached>200</queryResultMaxDocsCached>

ここで設定した値以上の結果セットはキャッシュされないようです。

    // lastly, put the superset in the cache if the size is less than or equal
    // to queryResultMaxDocsCached
    if (key != null && superset.size() <= queryResultMaxDocsCached && !qr.isPartialResults()) {
      queryResultCache.put(key, superset);
    }

https://github.com/apache/lucene-solr/blob/releases/lucene-solr/6.6.0/solr/core/src/java/org/apache/solr/search/SolrIndexSearcher.java#L1424-L1428

documentCache

filterCachequeryResultCache がドキュメントID(int)をキャッシュするのに対して、documentCacheはドキュメントIDの先にあるLuceneのドキュメントオブジェクトをキャッシュします。このオブジェクトはインデックス更新のタイミングで変化するので、Auto Warming機能は利用できません。(他のキャッシュはautowarm時に保持してあったドキュメントIDを利用してLuceneオブジェクトをディスクからフェッチしている)

<documentCache class="solr.LRUCache"
               size="512"
               initialSize="512"
               autowarmCount="0"/>
  • キャッシュ内部の例
item_12345 : Luceneオブジェクト

fieldValueCache

Lucene管理化のstoredのfieldの値のキャッシュで、ファセット等に使われる情報がキャッシュされるようです。docValuesがTrueだと使われません。

この fieldValueCache は設定を明示的に入れなくても自動的にキャッシュの設定が有効かされstats画面にが表示されます。

このfieldValueCacheですが、SolrのWikiには記載があります。

SolrCaching - Solr Wiki

しかし、ソースによっては記載が無いところもあります。

Query Settings in SolrConfig - Apache Solr Reference Guide - Apache Software Foundation

ここにも無い。

lucene-solr/performance-statistics-reference.adoc at releases/lucene-solr/6.6.0 · apache/lucene-solr · GitHub

docValuesをfalseにしてもstatsの数値変化無く、このあたり見ても使われて無さそうだけど、どうなんでしょう。

lucene-solr/SolrIndexSearcher.java at branch_6_0 · apache/lucene-solr · GitHub

使われなくなっているという情報もあるようです。

Solr 5系はファセットが遅い? – NaviPlus Engineers' Blog

上記の通り使われてないのではと思い、今のところ気にしてません。 fieldCache との違いも良くわかってないです。Solr In Action の関連の項目見ると同じものなのでは。

その他・注意点

  • キャッシュそのものはCPU負荷の削減に繋がるものです。しかし、キャッシュのクリーンアップ処理はそれなりに負荷がかかります。 別スレッドで実行することも可能ですが、特にCPUボトルネックの場合はシステム全体のスループットに影響が出る場合があります。クエリやデータ、evictionsの数値が高くなってたら要注意です。キャッシュのサイズを増やすか、場合によってはキャッシュを切るほうが有効な時もあります。

  • キャッシュのSizeは基本的にアイテム数での設定になるますが、1アイテムあたりの容量はデータの内容により異なります。minimumで設定した値まで減らしてもHeapの空き領域を十分に確保できない場合、設定によってはGCが永遠と走り続けるような事象に陥る場合もありますので、設定値はよく検証して算出する必要があります。

参考

URL

SolrCaching - Solr Wiki

関連ソースコード

github.com

lucene-solr/solr/core/src/java/org/apache/solr/search/SolrIndexSearcher.java
lucene-solr/solr/core/src/java/org/apache/solr/search/SolrCache.java
lucene-solr/solr/core/src/java/org/apache/solr/search/SolrCacheBase.java
lucene-solr/solr/core/src/java/org/apache/solr/search/LFUCache.java
lucene-solr/solr/core/src/java/org/apache/solr/search/LRUCache.java
lucene-solr/solr/core/src/java/org/apache/solr/search/FastLRUCache.java
lucene-solr/solr/core/src/java/org/apache/solr/util/ConcurrentLFUCache.java
lucene-solr/solr/core/src/java/org/apache/solr/util/ConcurrentLRUCache.java

lucene-solr/performance-statistics-reference.adoc at releases/lucene-solr/6.6.0 · apache/lucene-solr · GitHub

参考書籍

[改訂第3版]Apache Solr入門 ―オープンソース全文検索エンジン

[改訂第3版]Apache Solr入門 ―オープンソース全文検索エンジン

Apache Solr High Performance: Boost the Performance of Solr Instances and Troubleshoot Real-time Problems

Apache Solr High Performance: Boost the Performance of Solr Instances and Troubleshoot Real-time Problems