読者です 読者をやめる 読者になる 読者になる

全文検索エンジン Solr のパフォーマンスチューニングについて

Java Solr

この記事は古くなりました。新しい知見は下記を参照。aoking.hatenablog.jp

概要

全文検索エンジン Solr を使用していて、パフォーマンスチューニングに四苦八苦した話。
ここでは、検索時ではなくドキュメントの追加時についてのチューニングについて記してある。


更新自体は参照に比べて頻度が少ないが、参照はレプリケーションして負荷分散しやすい。
更新は整合性を保つために一台のマスターノードに対して行われるので更新はボトルネックになりやすいのだ。


定期的に IO 負荷が高くなる

Solr を使っていると、一時的に猛烈に IO 負荷が高まる時がある。fsync になんと1分以上かかるような、猛烈な負荷だ。

これはインデクスのマージ時に起きる IO 負荷で、巨大なインデクス同士のマージだとその合計サイズ分の IO が発生することで IO 処理が専有されたままになっていた。
インデクスの最大サイズはデフォルトだと 5GB. 最悪の状態で 5GB + 5GB のファイルのマージが発生する。一時的に猛烈な負荷になるのは好ましくなく、出来ればもっと負荷を均したい。


さて、インデクスのアルゴリズムは solrconfig.xml で指定でき、デフォルトでは TieredMergePolicy で表されるアルゴリズムを使用している。この MergePolicy のパラメーターには maxMergedSegmentMB という値を渡すことができる。これはインデクスの最大サイズを表しており、デフォルト 5G から 1GB にしたところ負荷はだいぶ均された。

   <mergePolicy class="org.apache.lucene.index.TieredMergePolicy">
       <double name="maxMergedSegmentMB">1024</double>
   </mergePolicy>

1 コアを食いつぶし、ボトルネックになっている

社内の環境では 4 コアマシンで動かしているが、CPU 使用率が 100% 強で高止まりになっている(4コアなので最大400%)。このような値を指す場合、何らかの処理が1コアしか使えず、そこがボトルネックになっている可能性が高い。

top コマンドでプロセス毎ではなくスレッド毎の CPU 使用率でソートすると、やけに CPU を食っている Solr プロセスのスレッドがあった。top で取得した高負荷なスレッドIDと Solr プロセスのスレッドダンプのスレッドIDと付き合わせると、この高負荷なスレッドの正体はコミット関連の処理を行うスレッドであった。

どうやらコミットは重い処理のようなので、コミットの頻度を下げることにした。デフォルトでは 100 ドキュメント毎にコミットを行なっていたので、この値を変更する。
コミットの頻度を下げるには、solrconfig.xml の autoCommit の項目を変更すれば良い。


今回の環境では 100 倍の 10000 ドキュメント毎にコミットをするようにしたら、顕著にインデクスの速度が向上し、およそ 10 倍程度まで向上した。

  <updateHandler class="solr.DirectUpdateHandler2">
       ...
       <autoCommit> 
         <maxDocs>10000</maxDocs> <- デフォルト 100 だったのを 10000 に変更
         <maxTime>60000</maxTime> 
       </autoCommit>
  </updateHandler>

maxTime は、指定したミリ秒間コミットされなかったらコミットするという値だが、これはデフォルトの1分のままとした。


インデクス書き込み時のロックでパフォーマンスが出ない

あくせくと働いてる Solr のスレッドダンプを見ると、DirectUpdateHandler2 クラスの 210 行目で待ちが多く発生していることがわかる。ここのソースコードは下記の通り。

  @Override
  public int addDoc(AddUpdateCommand cmd) throws IOException {
    ...
    iwAccess.lock();
    try {
        ...
    } finally {
      iwAccess.unlock();
      ...
    }
    ...

見れば一目瞭然だ。ドキュメントの追加時には、書きこむインデクスのファイルに関係無く、全て直列化されている。ここがボトルネックになっていた。

残念ながらこれは Solr3.5 や 3.6 では解決することができない。しかし現在 ALPHA 版の Solr 4.0.0 はこのロジックが解消され、高速にインデクス書き込みが出来るようになっているようだ。社内で検証したところ、およそ 2〜3倍程度高速になっていた。

Solr 4.0.0 の安定版が待ち望まれる。


id 重複チェックをしている

ドキュメントの追加時、同じIDが存在しないかどうか重複チェックを行なっている。もし Solr で処理する前に ID が重複チェックしないことが保証できる場合、重複チェックは無駄でしかないので省くことができる。例えば掲示板に新規に書き込みがついた時、その書き込みは新規のものなので重複チェックは省けるだろう。この重複チェックを省く処理は、overwrite オプションで指定可能だ。

Solr 3.5 ではなぜか更新リクエストの GET パラメーターに overwrite オプションを渡しても読んでくれないようだが、Solr 4.0.0 では解消されていた。まだ検証はしていないが、大量に更新がある場合や既に巨大なインデクスが存在する場合には効果が出るのではないかと期待している。


インデクスサイズの肥大

既存のインデクスのサイズに比例して、新規ドキュメントのインデクス時間が長くなるようだ。
これを解消するのは分散構成にするしか無いが、分散構成にするほどでもなさそうな時はインデクスの最適化を試してみるべきだ。

インデクスの最適化はかなり高負荷だが、この最適化を行うと使われ無くなったインデクスデータが削除されるため、インデクスサイズが縮む。インデクスサイズが小さければインデクスにかかる時間も減らせるため、大量の更新が発生することが事前にわかっているなら最適化を行う。最適化を行うには、Solr Server に対して下記のリクエストを送れば良い。

$ curl http://solrserver:8983/solr/update?optimize=true 

ただし最適化は高負荷なので頻繁には行わないようにする。書籍「Apache Solr入門」では5コミットに1回最適化をするようにと記載されているが、そんな高頻度に最適化を行って良いのはインデクスサイズが小さい時だけだ。数十GBクラスのインデクスに対して5コミットに1回という高頻度で最適化を行うとパフォーマンスに悪影響を与えかねないので、100コミットに1回、もしくはそれ以下の頻度で良いだろう。

まとめ

Solr はじゃじゃ馬だけど、やれば出来る子。

追記

Solr 4.0 が登場したようだ。リリースノートを見る限り、かなり良い製品に仕上がってることが伺える。
強烈に素晴らしいプロダクトに変貌した Apache Solr - Solr 4.0 登場

Apache Solr入門 ―オープンソース全文検索エンジン

Apache Solr入門 ―オープンソース全文検索エンジン