CLOVER🍀

That was when it all began.

Infinispan as a storage for Lucene indexes

このひとつ前のエントリの続きです。Infinispanの以下のドキュメントについて、自分の理解のために英訳していきたいと思います。

Infinispan as a storage for Lucene indexes
https://docs.jboss.org/author/display/ISPN/Infinispan+as+a+storage+for+Lucene+indexes

概要

Infinispanは、高いスケーラビリティを持つApache Lucene Directoryの実装を含みます。

このDirectoryは、伝統的なファイルシステム、RAMベースのディレクトリとほぼ同じセマンティクスとなり、既存のLuceneを使っているアプリケーションを気軽に置き換えることができます。そして、信頼できるインデックス共有、ノードのオートディスカバリ、自動フェイルオーバー、リバランシング、オプションとしてトランザクション、バックエンドに伝統的なストレージソリューションとして、ファイルシステム、データベース、クラウドストレージを使用することができます。

この実装は、Luceneのorg.apache.lucene.store.Directoryを拡張したもので、分散の容易なクラスタに渡った共有メモリを使用することができます。rsyncベースのレプリケーションとこのソリューションを比較すると、インデックスが頻繁に変更があり、すべてのノードにすぐに配布する必要があるアプリケーションに適しています。これは、設定可能な一貫性レベル、同期と保証、全体の柔軟性とオートディスカバリを持ち、さらにオプションとして、インデックスの変更は任意にJTAトランザクションに参加することができます。XAトランザクションとそのリカバリをサポートしたのは、バージョン5からです。

2つの異なるLockFactoryの実装は、インデックスの変更を行うのは、ひとつのIndexWriterであることを保証します。これは、ローカルファイルシステムでインデックスをオープンすることと、同じ意味となります。

Luceneとの互換性

開発されている現在のバージョンはLucene 3.6.2、Lucene 4.3.0でコンパイルされています(ほとんどのコードを共有した、使いやすい単一のJARとして別々にアセンブルされています)。また、Lucene 3.0.xから3.5.0、バージョン2.9、古い2.4.1、そして4.0、4.1、4.2で定期的にテストされています。

使い方

Directoryのインスタンスを作成します。

import org.apache.lucene.store.Directory;
import org.infinispan.lucene.directory.DirectoryBuilder;
import org.infinispan.Cache;
 
Cache cache = // create an Infinispan cache, configured as you like
Directory indexDir = DirectoryBuilder.newDirectoryInstance(cache, cache, cache, indexName)
                                     .create();

「indexName」は、インデックスを識別するユニークなキーです。ファイルシステムベースのインデックスでのパスと、同じ役割となります。ここに異なる名前を与えることで、異なるインデックスを作成することができます。同じネットワークに参加している別のインスタンス(または、テストのための、同じマシン上のインスタンス)が同じ「indexName」を使用する時、クラスタ上で全てのコンテンツを共有することになります。異なる「indexName」を使用した場合は、同じCacheの集合上に異なるインデックスを保存することになります。

この例では、簡単なデモとして同じCacheを3回引数に渡しています。しかし、これらは異なる用途で使用されるので、それぞれチューニングするのがよい考えであることを、APIは示唆しています。

新しいノードの動的な追加・削除が可能で、サービス管理をとても簡単になり、クラウド環境にも適しています。多くのノードをするだけで、より多くのメモリとCPUを加えることができます。
*「react to load spikes」が?な感じ…ノードの追加・削除時に、瞬間的に負荷がかかる的なことを言ってるのかな…?

制限

ファイルシステムベースのDirectory上でIndexWriterを使用した時と同様、クラスタ版でさえも、IndexWriterはクラスタに渡ってひとつしかオープンすることができません。

例として、Hibernate Searchはバージョン3.3からLucene Directoryを統合していますが、インデックスの変更はJMSキューまたはJGroupsのチャネルを使って送っています。他の妥当なアプローチは、リモートIndexWriterのプロキシを使用するか、あるノードだけが書き込みを行うようにアプリケーションを設計することです。

読み込み(検索)については、各ノードの任意のスレッドから、並列に行うことができます。単一のIndexWriterが適用した変更は、とても短い時間で全ノードの全スレッドに結果が反映されます。また、トランザクションを使用した場合は、コミット後に変更内容が見れるようになることを保証します。

設定

Infinispanは、「LOCAL」クラスタリングモード、またはその他のクラスタリングのモードで設定することができます。クラスタリングのモードを「LOCAL」にした場合は、クラスタリングの機能やインデックスとしてのCacheの提供は無効になります。トランザクション管理は強制ではありませんが、インデックスの変更を有効にした時は、トランザクションに参加することができます。

以前のバージョンではBatchingが必須でしたが、現在は厳密には必須ではありません。

org.infinispan.lucene.InfinispanDirectoryのJavadocでよりよい説明がされているように、異なる目的に応じた設定をすることで、単一のCacheを使うよりもよい使い方をすることができます。
*metadataCache、chunksCache、distLocksCacheのことを言っているんだと思います
読み取りロックを使用する時は、Cacheのトランザクションを有効になっていないことを確認してください。

Cacheが閾値を越えてもエントリを削除するように設定されている限りは、どのInfinispanの設定もうまく動作するでしょう。
*要は、expireするな、と言ってるんでしょうね

Demo

Maven dependencies

飛ばします

CacheLoaderを使う

CacheLoaderを使うと、インデックスを永続ストレージにバックアップすることができます。全ノードでの共有ストア、またはノード毎のストアを使用することができます。詳しくは、「Cache Loaders and Stores」を見てください。

Cache Loaders and Stores
https://docs.jboss.org/author/display/ISPN/Cache+Loaders+and+Stores

Luceneのインデックスを保存するのにCacheLoaderを使う場合、ベストな書き込みパフォーマンスを得るために、CacheLoaderの設定を「async=true」にする必要があります。

インデックスをデータベースに保存する

リレーショナルデータベースに、Luceneのインデックスを保存することは、有用かもしれません。
これは、非常に遅くなるでしょう。しかし、この設定でクラスタ環境、非クラスタ環境の両方で役立つように、InfinispanはアプリケーションとJDBCインターフェースの間のキャッシュとして動作することができます。

JDBCデータベースにインデックスを保存する場合、JdbcStringBasedCacheStoreを使うことをお薦めします。これには、次のような属性が必要になります。

<property name="key2StringMapperClass" value="org.infinispan.lucene.LuceneKey2StringMapper" />
既存のLuceneインデックスのロード

org.infinispan.lucene.cachestore.LuceneCacheLoaderは、既存のLuceneインデックスからグリッドにダイレクトにロードすることができる、InfinispanのCacheLoaderです。現在は、読み込みのみをサポートしています。

プロパティ 説明 デフォルト値
location インデックスが保存されたパスを指定します。ロードされるインデックスが含まれる(第1階層のみの)サブディレクトリを使う場合は、マッチした各ディレクトリはInfinspanDirectoryのコンストラクタの「index name」属性となります なし(必須)
autoChunkSize バイトの閾値。これより大きいセグメントは、このサイズまでのより小さなCacheエントリとなり、薄くチャンク化されるでしょう 32MB

動作するプラットフォームに最適化された、Lucene標準のorg.apache.lucene.store.FSDirectoryにIO操作を委譲することに、注目する価値があります。

ライトスルーな実装とするのは、困難でしょう。

アーキテクチャの制限

このDirectoryの実装は、複数のノードに渡ったほぼリアルタイムの読み込みを可能にします。Luceneの設計の基本的な制限として、単一のIndexWriterだけがインデックスの変更を許されているということがあります。Writerにより、悲観的ロックが獲得されます。これは、単一のIndexWriterのインスタンスがとても高速で、マルチスレッドでの更新リクエストを受け付けるので一般的には問題ありません。Infinispanの各ノードに跨ってDirectoryを共有する時、IndexWriterの制限を上げられません。IndexWriterのインスタンスをひとつしか持てないため、全ての変更を同じノードが行うようにする必要があります。

複数のノードから、同じインデックスに対して書き込みを行う、いくつかの戦略があります。

  • ひとつのノードが書き込み、他のノードは処理を委譲するメッセージ送信を行う
  • 各ノードが回転して書き込みを行う(*書き込むノードが変わるということ??)
  • ひとつのノードのみがインデックスへの書き込みを行うように、アプリケーションを構築する

Infinispan Lucene Directoryは分散ロック戦略の実装により、コンテンツを保護します。これは、最終防衛線として設計されていますが、複数の書き込みを調整するための効率的なメカニズムと考えることはできません。

上記の提案をひとつも適用せずに、複数のノードでの高い書き込み競争を得た場合、タイムアウト例外を得ることになるでしょう。

パフォーマンス最適化のための提案

JGroupsとネットワークスタック

JGroupsは、すべてのネットワークIOを管理しています。このため、環境に合わせたチューニングをするべき重要なコンポーネントです。JGroupsのリファレンスを確認してください。

JGroupsマニュアル
http://jgroups.org/manual-3.x/html/index.html

そして、ネットワークスタックが適切にセットアップされたことを確認するために、JGroupsを含んだパフォーマンステストを行ってください。
例えばネットワークのバッファサイズを決定するような、OSレベルのパラメータをチェックすることを忘れないでください。JGroupsは間違った設定を検知した時、警告ログを出力するでしょう。そして、より多くの情報をその中に見ることができます。

CacheStoreの使用

現在、Infinispanによって提供されるすべてのCacheStoreの実装は、重大なスローダウンの原因となります。これが早く解決することを望みますが、時間がありません。Lucene Directoryでの高い書き込みパフォーマンスをが必要なのであれば、ベストなオプションはどのCacheStoreも無効にすることです。第2のベストなオプションは、CacheStoreをasyncに設定することです。読み取り専用のストレージから、単にLuceneのインデックスをロードしたいだけの場合は、org.infinispan.lucene.cachestore.LuceneCacheLoaderについての上記の説明を参照してください。

標準的なLuceneのチューニングの適用

Luceneのすべての既知のオプションは、Infinispan Lucene Directoryにも同じように当てはまります。もちろん、いくつかのケースにおいては、それほど重要ではないかもしれません。しかし、Apache Luceneのドキュメントを読むべきです。

バッチAPIトランザクションの無効化

初期のバージョンでは、バッチAPIまたはトランザクションを有効にすることを、Infinispanに要求していました。これはもはや必須ではありません。そして、これらを無効にすることで、少しのパフォーマンス改善を提供するでしょう。

正しいチャンクサイズを設定する

DirectoryBuilderに対して、オプションパラメータであるチャンクサイズを渡すことができます。それがオプションとなっている間、つまりデフォルトではテストや小さなデモにのみ適しています。より大きな値を設定することで、特に複数のノードで動作する場合にはパフォーマンス上の劇的な効果を得ることができます。正確にこの変数を設定するために、セグメントの予測サイズを見積もる必要があります。一般には、標準のFSDirectoryを使ったアプリケーションによって生成されたインデックスセグメントのファイルサイズを見ることであり、ささいなことです。

そして、次のことを考慮しなくてはいけません。

  • チャンクサイズは、内部で作成するバッファのサイズに影響します。よって、貴重なJVMメモリを浪費し、法外に大きな配列を作成してしまうことは、望む事態ではないでしょう。さらに、インデックスに書き込みを行う間、そのような配列が頻繁に確保されてしまうことを考えてください
  • セグメントがチャンクサイズに収まらない場合、フラグメントが発生するでしょう。フラグメント化されたセグメントで検索する場合、パフォーマンスがピークに達しないでしょう

org.apache.lucene.index.IndexWriterConfigを使用することで、合理的なレベルのセグメントサイズを維持し、インデックスへの書き込みをおおよそチューニングすることができます。そしてチャンクサイズをチューニングし、定義した後で、ネットワーク設定を再訪することになるでしょう。

専用Cacheの使用

Directoryのインスタンス構築の時、異なるCacheを指定するオプションがあります。「metadataCache」は、全ノードで頻繁にアクセスされるでしょう。またコンテンツも非常に小さいので、「REPL_SYNC(同期レプリケーション)」を使用するのがベストです。「chunksCache」は、ファイルシステムに保存している、いないに関わらず、インデックスセグメントを生のバイト配列として、保持しています。それゆえ、システムの大半が読み込みであると仮定すると、このCacheはレプリケーションにしたいと思うかもしれません。しかし、全ノードでレプリケーションされたすべてのデータを保存するための、十分なメモリがあるかどうかを考えなければいけません。そうでなければ、オプションとしてL1キャッシュを有効にして、「DIST_SYNC(同期分散)」を使用するのがよいかもしれません。「distLocksCache」は、「chunksCache」に似ています。インデックスの永続化が必要な場合でも、「distLocksCache」はインデックスを必要としません。


(その他、補足など)
専用Cacheの使い分けですが、だいたい以下のような感じだと思います。

  • 「metadataCache」→インデックスのメタデータ。よって、レプリケーションにすべき
  • 「chunksCache」→インデックスの実データ。可能であればレプリケーションが望ましいが、そうでない場合は分散キャッシュを使用する
  • 「distLocksCache」→「chunksCache」に似ている?

この中で、「distLocksCache」が特に分かりにくいので、少し調べてみました。

「distLocksCache」って他と共有できるの?みたいな質問も出ているみたいですし。

InfinispanDirectory: question about distLocksCache
http://lists.jboss.org/pipermail/infinispan-dev/2010-October/006476.html

DirectoryBuilderのJavadocには、あまり大した説明は書かれていません。

metadataCache - contains the metadata of stored elements
chunksCache - cache containing the bulk of the index; this is the larger part of data
distLocksCache - cache to store locks; should be replicated and not using a persistent CacheStore
indexName - identifies the index; you can store different indexes in the same set of caches using different identifiers

http://docs.jboss.org/infinispan/5.3/apidocs/org/infinispan/lucene/directory/DirectoryBuilder.html

ここだけ読むと、ロックを保存するCacheであり、CacheStoreの永続化は使用せずにレプリケーションにすべきだ、と書いてあります。

これでも、まだよく分からないですよね?

ここで、Infinispan 5.3で非推奨となってしまった、もともと使われていたクラスであるInfinispanDirectoryのクラスが概要を読んでみます。

An implementation of Lucene's Directory which uses Infinispan to store Lucene indexes. As the RAMDirectory the data is stored in memory, but provides some additional flexibility:

Passivation, LRU or LIRS Bigger indexes can be configured to passivate cleverly selected chunks of data to a cache store. This can be a local filesystem, a network filesystem, a database or custom cloud stores like S3. See Infinispan's core documentation for a full list of available implementations, or CacheStore to implement more.

Non-volatile memory The contents of the index can be stored in it's entirety in such a store, so that on shutdown or crash of the system data is not lost. A copy of the index will be copied to the store in sync or async depending on configuration; In case you enable Infinispan's clustering even in case of async the segments are always duplicated synchronously to other nodes, so you can benefit from good reliability even while choosing the asynchronous mode to write the index to the slowest store implementations.

Real-time change propagation All changes done on a node are propagated at low latency to other nodes of the cluster; this was designed especially for interactive usage of Lucene, so that after an IndexWriter commits on one node new IndexReaders opened on any node of the cluster will be able to deliver updated search results.

Distributed heap Infinispan acts as a shared heap for the purpose of total memory consumption, so you can avoid hitting the slower disks even if the total size of the index can't fit in the memory of a single node: network is faster than disks, especially if the index is bigger than the memory available to cache it.

Distributed locking As default Lucene Directory implementations a global lock needs to protect the index from having more than an IndexWriter open; in case of a replicated or distributed index you need to enable a cluster-wide LockFactory. This implementation uses by default BaseLockFactory; in case you want to apply changes during a JTA transaction see also TransactionalLockFactory.

Combined store patterns It's possible to combine different stores and passivation policies, so that each nodes shares the index changes quickly to other nodes, offloads less frequently used data to a per-node local filesystem, and the cluster also coordinates to keeps a safe copy on a shared store.

http://docs.jboss.org/infinispan/5.3/apidocs/org/infinispan/lucene/InfinispanDirectory.html

見るべきはおそらくここで、

Distributed locking As default Lucene Directory implementations a global lock needs to protect the index from having more than an IndexWriter open; in case of a replicated or distributed index you need to enable a cluster-wide LockFactory. This implementation uses by default BaseLockFactory; in case you want to apply changes during a JTA transaction see also TransactionalLockFactory.

IndexWriterのオープン時にインデックスを保護するために必要となる、グローバルなロックのようです。

そして、内部では「distLocksCache」を使用して、BaseLockFactoryクラスのインスタンスが生成されます。JTAを使う場合は、TransactionalLockFactoryクラスを使えということらしいですが。

BaseLockFactory
http://docs.jboss.org/infinispan/5.3/apidocs/org/infinispan/lucene/locking/BaseLockFactory.html

BaseLockFactoryクラスは、ロック作成の要求を受けた時に以下の項目を使って、内部のLuceneのLockクラスの実装であるBaseLuceneLockクラスのインスタンスを生成します。

  • 「distLocksCache」
  • インデックス名
  • ロック名

BaseLuceneLockクラスのコンストラクタを見てみると、こんな実装に

   BaseLuceneLock(Cache<?, ?> cache, String indexName, String lockName) {
      this.noCacheStoreCache = (Cache<Object, Object>) cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD, Flag.SKIP_INDEXING);
      this.lockName = lockName;
      this.indexName = indexName;
      this.keyOfLock = new FileCacheKey(indexName, lockName);
   }

なるほど、確かに永続化されない設定になっているようですね。

ロックとなるキーおよび値は、ここで作成されているFileCacheKeyのようです。

その他、BaseLuceneLockクラスのメソッド定義を見てみると、こんな感じになっています。

   @Override
   public boolean obtain() {
      Object previousValue = noCacheStoreCache.putIfAbsent(keyOfLock, keyOfLock);
      // 省略
   }

   @Override
   public void release() {
      clearLock();
   }

   public void clearLock() {
      Object previousValue = noCacheStoreCache.remove(keyOfLock);
      if (previousValue!=null && log.isTraceEnabled()) {
         log.tracef("Lock removed for index: %s", indexName);
      }
   }

   @Override
   public boolean isLocked() {
      boolean locked = noCacheStoreCache.containsKey(keyOfLock);
      return locked;
   }

これなら容量も小さそうですし、レプリケーションにしてもいいのかも?