Lucene/Solrのパフォーマンスチューニングした内容をまとめてみた

Solrのパフォーマンスチューニングを一通り行ったので、忘れないうちにまとめてみたいと思います。Lucene/Solrのバージョン6系統になら当てはまると思います。

インデクシングのパフォーマンスにも関係するところはありますが、主に検索側のパフォーマンスについて書いています。

Solrのパフォーマンス変化要因

前提として以下のような内容でチューニング内容は変わってきます。

  • 用意できるシステムリソース(CPU、メモリ、SSD or HDD)
  • 同居しているミドルウェア等あれば
  • 格納するドキュメントのサイズ、種類や特性
  • ドキュメントの更新頻度
  • 発行するクエリ

などなど

まずはOSレベルでボトルネック見極め

その上でSolrが CPU Bound なのか I/O Bound なのかを見極めるところから始まります。基本的なことなので、ここ読んでる人には不要かもしれませんが。

負荷テストクエリ

負荷テストのクエリはよく考慮します。基本は実際に本番で受けるリクエストに可能な限り近づける方針で。既に動いている環境ならそのログから拾ったりできますし、新規の環境だとしてもできるだけ本番想定に近いクエリ作るように気をつけます。

取得件数多かったり、複雑なスコア計算したり、facet多用したりパフォーマンス的に厳しいクエリ作ろうと思えばいくらでも作れます、一つのクエリでCPU食い潰したりもできます。逆に緩いクエリでもTPS上がって糠喜びするはめになりかねないです。

クエリの種類が本番より少ないと少ないリソースでも、ドキュメントがキャッシュに乗って良いパフォーマンスが出たりもしますし。

テストクエリの精度は大切です。

パフォーマンス計測ツール

この時点での計測ツールとしてはまずはvmstatやdstat、iostatのような基本的なものでも十分かと思います。その上で、jmeter等で負荷をかけて、まずはvmstatて言うとこの rb が溜まるほどの負荷をかけられる環境を準備します。

キューが溜まらないようならSolrの外に問題がある可能性も高いので。

後はCPUの項目見ればCPU BoundかI/O Boundかを大ざっぱに掴むことができます。

I/O Boundへの対応

ある程度の大きさのインデックスになると、何もケアしない状態ではメモリに乗らないことが多くなります。SSDが出てディスクも早くなってきたとは言えディスクアクセスはパフォーマンス悪化の大きな要因になります。

インデックスはOSキャッシュに乗っているか

Indexがキャッシュに乗るか乗らないかはSolrノパフォーマンスに大きく影響します。まずは全てのIndexをキャッシュに乗せることができないかを考えます。

そのためにもIndexがキャッシュに乗っているのか乗っていないのかは正確に把握しておきます。

キャッシュ落としてみたり。

# echo 1 > /proc/sys/vm/drop_caches

キャッシュしてみたり。

$ cat /path/to/index/* > /dev/null

詳しく確認したければvmtouchが便利です。

yomon.hatenablog.com

インデックスのディレクトリには様々なファイルがありますが、どのインデックスのどの種類のファイルがディスク領域を大きく食っているかは把握しておきます。

LuceneのIndexファイルに関するメモ書き | mwSoft

ここを正確に把握しているかどうかで、後々落とし穴にハマるかどうかの違いになる場合もあります。自戒もこめて。

少し古いですが、この資料がインデックスサイズに対して詳しいベンチーマーク結果が載っていて面白いです。最後の方にBoolean QueryとPhrase Queryの処理の流れも記載あるので動きをイメージするためにも読んでみるといいかもしれません。

https://www.hathitrust.org/technical_reports/Large-Scale-Search.pdf

このHathiTrustのように、インデックスサイズが2TBとかならメモリに乗せるのは予算的に難しいところが多いと思うので、分散したり、ディスク設計考えたりのI/O周りのチューニングがメインになってきそうですね。

Scaling up Large Scale Search from 500,000 volumes to 5 Million ... | HathiTrust Digital Library

OSキャッシュに乗らない場合

OSキャッシュに乗っているのなら第一関門はクリアです。しかしそうも簡単にはいかないことも多いはず。(そもそも絶対に乗らない場合もありますし)以下はその場合の対応です。

Schemaのチューニング

何としてもOSキャッシュに乗せたい。インデックスが大きいからキャッシュに乗り切らないのなら、インデックスを小さくできないかを考えます。

schemaに無駄が無いか、もう一度、Fieldの設定を一つ一つ確認します。本当にそのフィールドは indexed="true" である必要があるのか。

storeddocValued を理解して設定しているか。データによってはstoredにした方がインデックス縮小できる場合もありますし、逆にdocValuesの方が圧縮が効く場合もあります。docValuesとstoredはデータの持ち方がまるで違うので、クエリの特性も考えてチューニングします。docValues についてはこちらに書きました。

yomon.hatenablog.com

Indexing、Replication、Merge、OptimizeなどSolrのパフォーマンスに影響するものはインデックスサイズに影響を受けるものは多くあります。機能を担保できている前提でならインデックスサイズに関しては小さいは正義です。

本番(またはそれに近い)データを少量用意してインデックスさせては、チューニングして動作確認、再インデックスみたいな根気のいる作業が必要ですが、改善点が見つかった時の成果は大きいです。

Solrはインデックスを複数のコアに分散することで複数ノードに分けた分散検索が可能です。(SolrCloud含む)

この方式は多くの場面で有効でありパフォーマンスも上がり、1サーバ辺りのインデックスサイズも分散縮小されるので、小さなサーバ並べてもOSキャッシュに乗せやすくなります。

ただ、これも銀の弾丸というわけではありません。

分散検索は1クエリを2クエリに分けて実行し内容をマージして返します。

  1. q=query&fl=id,score でクエリに合致するドキュメントのリストを取得
  2. q=query&ids=2,3,5,6....&fl=f1,f2,f3 でフィールドを取得

これを分散したコア数に投げるので5コアに分散したらユーザ側の1クエリに対して、Solrは分散先に10クエリ投げることになります。これは10倍時間やリソースがかかるというわけではありません。1クエリ自体の時間や計算量は短くなるため分散はパフォーマンスに良い効果があるということです。しかし、問題はこの10クエリのうち1つでもクエリが遅くなるとそれに引っ張られれて全体のパフォーマンスが遅くなってしまうことです。

それぞれの分散先のサーバに余裕がある状態なら良いのですが、1サーバでも詰まってしまうと大きなパフォーマンス遅延を起こしてしまう可能性があります。

他にもランキング部分の考え方に誤差があったりするのですが、その辺りはパフォーマンスからはずれるのでこの辺りとか参考にしてください。

http://events.linuxfoundation.org/sites/events/files/slides/ApacheCon_IntroSolrCloud.pdf

分散がパフォーマンス出しやすいのは確かですが、クラウド全盛の昨今では、ある一定の規模までは、スケールアウトとスケールアップでインフラコストが変わらない場合があります(特にCPUコア数とRAM)。スケールアウトの分散は必ずしも銀の弾丸ではなく、場合によってはスケールアップの方が良い結果がでる場合もあるかと思います。

このマピオンさんの事例のようにLBを間に置く方法もシステムの柔軟性があがってスケールしやすくなります。AWSならInternal ALBとかですね。ALBが出たことで1台のALBでも複数のコアへルーティングしてくれるようになり、分散検索にも対応しやすくなりました。

Replication

Master-Slave構成を取っている場合はレプリケーションもパフォーマンス悪化要因です。ここもOSキャッシュを最大限に活用するとパフォーマンス影響を軽減できます。

Solrのレプリケーション帯域幅を制御してI/Oパフォーマンスを制御する - YOMON8.NET

Linuxページキャッシュの設定を変更してWrite I/Oをチューニングしたメモ - YOMON8.NET

または上記のマピオンさんの事例のように中間にLB置くなら、レプリケーション実行時にアクセスを止める手もあります。

CPU Boundへの対策

I/O Boundを切り抜けることができれば次はCPU Boundとの戦いになります。

Solrキャッシュ

CPUリソースとの戦いは厳しいのですが、基本に立ち戻って、「可能な限りCPUに計算させないようにする」ことが大切かと思います。ということでキャッシュです。

Solrは下位のミドルウェアLuceneに頼らずに自身でもキャッシュを持っています。このキャッシュを適切に設定してやることは直接CPUリソースの節約に繋がります。

Solrのキャッシュについてはこちらに纏めています。

yomon.hatenablog.com

更新が少ないインデックスだとすると、Proxyやアプリケーションにキャッシュさせて、そもそもSolrに計算させないようにする方法も考えられます。その場合はインデックス更新に合わせたキャッシュクリアの運用を考える必要もあります。

qiita.com

JavaVM(とSolr/Lucene内部)

上記に書いてあることが全て終わると、後はSolr/Lucene内部に踏み込むことになります。

JavaVM関連ツール

どこでCPU使われているか確認するのに jvisualvm 使っています。使い方はググれば出てくるというか触ればわかるレベルなので書きません。副次的な効果としてコールスタックまで確認できるのでSolr/Luceneソースコード読むにも役立ちます。Search Facet QueryなどそれぞれのComponentでどれくらい時間がかかっているかなども割り出せます。

JVMのメモリ割り当て少なすぎたり、調子に乗ってキャッシュ増やしすぎたりすると、突然CPU負荷かからなくなって遅いと思ったら、実はFull GCが走り続けているなんてこともあるので jstat も手放せません。

CPUだけではないですが、ダンプ解析するならJava Memory Analyzer (MAT)(http://www.eclipse.org/mat/)も便利。

Solrのデバッグログ

Solrの中身を追う場合は以下のlog4jのログレベルをDEBUGにしてみると、実際のソースコードと並べて処理を追いやすくなります。

https://github.com/apache/lucene-solr/blob/releases/lucene-solr/6.6.0/solr/server/resources/log4j.properties#L4

クエリのチューニング

上記のような情報を使いながら、クエリを変化させ、仮説検証作業を繰り返すことになります。

パフォーマンス影響の多いパラメータを特定して要件と相談しながら、削れるものは削ります。

こうやって書いてみると基本的なことがほとんどですが、基本が大事ということで。