Bering Note – formerly 流沙河鎮

情報技術系のこと書きます。

OpenSearchCon 2022「並行処理とマージポリシーで検索を高速化しよう - Andriy Redko, Aiven」

OpenSearchCon 2022のセッション「Concurrency and Merge Policies to the Rescue」を日本語でまとめます。 可能な限り正確に内容を拾えるようにリスニングに努めたつもりですが、もし誤りがあればご指摘ください。

OpenSearchCon とは?

イベントページ

opensearch.org

各セッションはYouTubeで視聴可能

www.youtube.com

Concurrency and Merge Policies to the Rescue

セッションリンクは以下.

www.youtube.com

スピーカー

  • Andriy Redko
    • Staff Software Developer, Aiven
    • OpenSearch Comitter


セッションまとめ

本セッションでは、OpenSearch 2.1.0で利用可能になった二つの技術的なアプローチが紹介された。

OpenSearchにおける検索パフォーマンスの課題

OpenSearchで大量のデータを扱う際の検索クエリ時間の改善は非常に困難な課題である。残念ながら、すべてのユースケースに適用できる単一の解決策は存在しない。この問題に対処する方法として、密接に関連する二つのコンセプトを紹介する。
一つ目は「リフレッシュ時のマージ」、二つ目は「検索フェーズでの複数スレッドの使用」である。これらの技術にはそれぞれCPU使用率とIO操作の観点でトレードオフがあり、ベンチマークの結果を通じて各戦略の効果が明らかになった。これらについて解説していく。

Apache Luceneとセグメント管理の基礎



OpenSearchは23年の歴史を持つApache Luceneに依存しており、Luceneはその歴史にもかかわらず今でも革新と改善を続けている。OpenSearchは分散システムとして通常ノードのクラスターとして展開され、インデックスをシャードと呼ばれる小さなチャンクに分割してデータノード間で分散する。

各シャードは本質的に小さなApache Luceneインデックスであり、これがOpenSearchで扱う基本単位となる。各Apache Luceneインデックスはセグメントで構成され、セグメントは異なるタイプのファイルの集合体である。 インデックスは複数のセグメントで構成され、これらのセグメントにはインデックスドキュメントのサブセットのみが含まれる。新しいドキュメントがインデックスに取り込まれても、すぐには読み取り可能にならない。インデックスライターがコミットして初めて、新しいドキュメントが検索可能になる。
セグメントが永続化されると、それらはイミュータブルとなり、変更されることはない。従って、時間の経過とともにセグメントの数が増加していく。検索効率を維持するため、小さなセグメントを大きなものにマージする必要がある。そうしなければ、検索はどんどん遅くなっていく。
この操作は通常、スケジュールされて実行されるか、force mergeを使用して行われる。しかし、この操作はIO観点で非常に集約的であり、通常はクラスターがアイドル状態のときに実行する必要がある。

検索時のセグメント処理


クエリが送信されると、コーディネーティングノードがクラスター内のデータノード間でクエリを分散し、特に関連するインデックスシャード間で分散する。ここで重要なのは、各シャードレベルでの処理方法である。各セグメントは検索クエリにマッチするドキュメントを求めて順次参照される。つまり、シャード内に10個のセグメントがある場合、1番目のセグメントを完全に処理してから2番目のセグメントに移る、という具合に一つずつ順番に処理していく。この処理を行うのは単一のスレッドのみで、並列処理は行われない。
この仕組みを理解すると、セグメント数の増加が検索パフォーマンスに与える影響が明確になる。セグメントが10個から100個に増えれば、それだけ順次処理しなければならないセグメントが増加し、検索処理全体の時間も比例して長くなる。結果として、検索はどんどん遅くなっていく。

解決策1:マージポリシーの改善

Merge on Refresh

最初に思い浮かぶのは、何らかの方法でセグメント数を減らすことである。従来は強制マージ(force merge)という手法があったが、これは非常に重い処理である。強制マージでは、複数のセグメントを一つに統合するため、すべてのセグメントからデータを読み取り、新しい統合されたセグメントに書き直す必要がある。この処理は大量のディスクI/Oを伴い、CPU資源も消費するため、クラスターがアイドル状態のときにしか実行できない。
そこで登場したのが「merge on refresh」である。OpenSearchでは、新しく追加されたドキュメントを検索可能にするため、定期的に「refresh」と呼ばれる処理を実行している。OpenSearchでは、ドキュメントがインデックスに書き込まれても即座には検索結果に反映されない。これは、書き込みパフォーマンスを高めるために、一度メモリ上のバッファに蓄積し、一定間隔ごとに「refresh」処理を行うことで、検索可能な状態(セグメントへの反映)にしているためである。
最近のリリースで、Apache Luceneはこのrefreshのタイミングでセグメントマージを行う「merge on refresh」機能を導入した。従来はドキュメントの追加やインデックスの更新時に小さなセグメントがどんどん蓄積され、後で大規模なマージ処理が必要になっていた。この新機能は、refreshのタイミングで小さなセグメントを積極的にマージして、セグメント数の増加を事前に抑制することを目的としている。
ただし、マージ処理自体にはIO負荷がかかるため、無制限に実行すると全体のパフォーマンスに悪影響を与える可能性がある。そこで、マージ処理に費やす時間に上限を設け、その制限を超えそうな場合は処理を中断して通常のフローに戻る仕組みが実装されている。これにより、パフォーマンスへの悪影響を最小限に抑えながら、セグメント数の削減効果を得ることができる。

Merge on Flush

さらに、Apache Luceneは「merge on flush」と呼ばれる新しいマージポリシーを導入した。これは少し異なる動作をし、フラッシュフェーズで実行される。マージする内容の決定は、ファイルやセグメントの閾値に基づいて行われる。「flush」とは、インデックスのメモリ上にある全ての操作(書き込みや更新など)をディスク上のセグメントに永続化する処理を指す。flushが行われることで、これまでトランザクションログ(translog)にのみ記録されていたデータがLuceneインデックスのセグメントとしてディスクに保存され、これらの操作はトランザクションログから削除できるようになる。

これらの機能は低コストでセグメントの量を減らす単一の目標を目指している。

ベンチマーク


新しいマージポリシーの効果を検証するため、OpenSearchの独自ベンチマークツールを使用した検証を実施した。テスト環境では、従来の強制マージタスクを意図的に無効化し、純粋にマージポリシーの効果を測定できるようにした。
比較対象として、標準的なOpenSearch 2.2をベースライン(基準値)として設定した。一方、新機能を検証する環境では、merge on flushポリシーの有効化と、より多くのセグメントをマージできるようマージワーク時間をわずかに拡張するという二つの設定変更のみを行った。
この結果、新しいマージポリシーを適用した環境では、セグメント数が従来の3分の1まで減少した。これに伴い、すべての種類の検索クエリにおいて検索パフォーマンスの指標(パーセンタイル)が改善された。
ただし、トレードオフも存在する。セグメントマージにかかる時間は従来の約2倍に増加した。しかし、これは予想される結果であり、より積極的なマージ処理によってセグメント数を削減する代償として、マージ処理自体に多くの時間を費やすことになる。

解決策2:並行セグメント処理


merge on refreshとmerge on flushだけでは限界がある。ある時点で、大きなセグメントをマージする必要がある問題に直面し、結局は性能が悪くなってしまう。
そこで、セグメントを順次処理する代わりに並行してすべてを処理するアプローチを考える。

実は、Apache Luceneには4年以上前から並行セグメント処理機能が実装されていた。しかし、この機能をOpenSearchで利用するには、コア部分での大幅な実装変更が必要だった。
並行セグメント処理の仕組みは、従来の単一スレッドによる順次処理を根本的に変更するものである。インデックスサーチャーにスレッドプールを組み込み、複数のスレッドが同時に異なるセグメントを処理できるようにする。各スレッドが担当するセグメントで検索を実行し、最終的にすべての結果を統合して返す。この処理パターンは、分散コンピューティングでよく使われるscatter-gather(分散・収集)やMapReduceのアプローチと非常に似ている。

Concurrent Search Plugin

並行セグメント処理によるパフォーマンス向上の可能性は明らかだったため、OpenSearchコミュニティでこの機能の導入を求める声が高まり、専用のプラグインが開発されることになった。 現在、Concurrent Search Pluginはサンドボックス(実験的機能)として提供されている。プラグインの動作は非常にシンプルで、インストールするだけでインデックスサーチャーにスレッドプールが自動的に組み込まれ、Apache Lucene側の並行セグメント処理機能が有効になる。 ただし、すべての状況で並行処理が実行されるわけではない。インデックスに含まれるセグメントが一つだけの場合、並行処理を行う意味がないため、Apache Luceneは自動的に従来の単一スレッド処理を選択する。このような最適化により、不要なオーバーヘッドを避けることができる。

注: Concurrent Search(Concurrent Segment Search)は、現在では正式機能として組み込まれ、OpenSearch 3.0ではデフォルトで有効になっている

docs.opensearch.org

プラグインの確認方法


並行検索パスが選択されているかどうかを確認する最も簡単な方法は、デバッグログを有効にすることである。デバッグログが利用可能な場合、「concurrent search over segments」メッセージとともに並行クエリサーチャーがログに表示される。

しかし、一部の環境では、クラスター全体のロギング設定を変更することが不可能な場合がある。より良い方法として、検索クエリプロファイルを使用できる。検索クエリでprofileプロパティを指定すると、クエリに関連する情報が返される。注目すべき変更は、コレクターセクションにある。従来の順次処理では「SimpleTopScoreDocCollector」と表示されるが、並行セグメント処理では「SimpleTopDocsCollectorManager」と表示される。

ベンチマークと制限事項


並行検索プラグインベンチマークでは、マージポリシーほど良好な結果は得られなかった。フレーズクエリとスクロールクエリではいくつかの改善が見られたが、他のタイプのクエリでは結果は印象的ではなく、ほぼ同じ数値となった。これは基本的に、まったく機能していないことを意味する。

ただし、これは並行セグメント処理自体が機能していないという意味ではない。実際にシステムの内部動作を確認すると、スレッドプールは活発に動作しており、複数のスレッドが並行してセグメントを処理している。
インデックスが大きなセグメントと小さなセグメントを混在させる場合、大きなセグメントが依然として検索時間を支配する可能性がある。残念ながら、現在のApache Luceneは単一セグメント内を並列化または並行検索しない。セグメント単位でのみ並行処理するため、混合インデックスでは依然として大きなセグメントがすべての時間を占有する問題が発生する可能性がある。

また、クラスターが多数の検索クエリを同時に処理している高負荷状況では、別の問題が発生する。通常、検索クエリは専用のスレッドプールで処理されるが、並行セグメント処理が有効になると、各検索クエリがより多くのスレッドを必要とするようになる。 例えば、従来は1つの検索クエリに1つのスレッドで十分だったが、並行処理では1つのクエリが4つのスレッドを同時に使用する場合がある。多数のクエリが同時に実行されると、利用可能なスレッド数を超えてしまい、新しいクエリは処理を開始できずに待機状態(キュー)に入ってしまう。 結果として、並行処理による高速化の恩恵を受けるどころか、スレッドの奪い合いによって全体的なレスポンス時間が悪化する可能性がある。この問題は現在も解決策が模索されている課題である。

今後の改善点と制限事項


現在のConcurrent Search Pluginにはいくつかの制限があり、これらはすべて将来の改善と作業のポイントである。

Early Termination

Early terminationは、何らかの条件で検索クエリを終了できる優れた機能である。例えば、10件の結果のみが必要な場合などである。検索が順次の場合は非常に簡単で、条件が満たされたら検索を停止すればよい。しかし、複数のスレッドが関与すると、それらを調整することは非常に困難になる。同期プリミティブを導入することもできるが、それは競合点となり、基本的に並行セグメント処理で得られたすべてのメリットを消去してしまう。

Partial Results

Partial resultsは、クエリがタイムアウトした場合などに非常に有用な機能である。クエリがタイムアウトすると、これまでに収集されたものを返すことができる。しかし複数のスレッドがセグメントを処理している場合、現時点では結果が得られない。

良いニュースは、最近のリリースで、Apache Luceneがインデックスサーチャーでタイムアウトをネイティブサポートする新機能を導入したことである。これはすでにOpenSearchのイシューとして提起されており、非常に近いうちに取り上げられ、自然に統合されることを期待している。そうすれば、この問題は自然に解決される。

Aggregations

集約は現在、検索処理の順次フローの一部として動作し、セグメントを横断する際に構築される。この部分はまだ並行検索に移行されていない。必要な作業量が膨大なためである。
現在、検索クエリがペイロードに集約を含む場合、自動的に順次フローにフォールバックする。

Thread Pool共有

現在、データノード上のすべてのインデックスとすべてのシャード間で共有される単一のスレッドプールのみが存在する。これは公平性を大幅に損なう可能性があり、特定のインデックスやシャードに対してオン・オフを切り替えられるよう、より選択的で詳細な設定を提供することに取り組んでいる。

まとめ


Merge on refreshとmerge on flushは、インデックスの検索パフォーマンスを大幅に改善できる。これに加えて、制限はあるものの非常に有用で、すべてのコアをビジー状態に保つことで検索クエリのパフォーマンスを大幅かつ著しく改善できるConcurrent Search Pluginがある。
これらの技術は、OpenSearchにおける大規模データ処理の検索パフォーマンス向上への重要なステップであり、今後のさらなる発展が期待される。