[翻訳] 並列セグメント検索
OpenSearch は、スケーラブルで柔軟かつ拡張可能なオープンソースの検索・分析エンジンです。Apache Lucene をベースに構築されており、その強力な機能を活用してユーザーデータを取り込み、ミリ秒単位のレイテンシで検索リクエストに応答します。
ユーザーは常に検索パフォーマンスを向上させる方法を求めています。レイテンシに敏感なユースケースでは、リソースを追加するコストを払ってでもクラスターをスケールアップしてレイテンシを改善することを厭わないユーザーもいます。一方で、クラスターに十分なリソースがあるにもかかわらず、検索リクエストのレイテンシを改善できないユーザーもいます。Apache Lucene は検索アルゴリズムとヒューリスティクスを継続的に改善しており、OpenSearch はそれらの改善をエンジンに取り込んでいます。その中には単純な最適化もあれば、大幅なシステム再設計を必要とするものもあります。並列セグメント検索は、非自明な変更を必要としますが、さまざまなワークロードタイプで検索レイテンシを改善する機能です。
背景
並列検索について詳しく説明する前に、OpenSearch がインデックスをどのように保存し、検索リクエストをどのように処理するかについて簡単に説明します。OpenSearch は分散システムであり、通常はノードのクラスターとしてデプロイされます。各クラスターには 1 つ以上のデータノードがあり、そこにインデックスが保存されます。各インデックスは「シャード」と呼ばれる小さなチャンクに分割され、データノード全体に分散されます。さらに、各シャードは Apache Lucene インデックスであり、インデックスされたドキュメントのサブセットを含むセグメントで構成されています。以下の図にこの構造を示します。

シャーディング技術により、OpenSearch は大量のデータにスケールできます。しかし、検索クエリを効率的に処理することが課題となります。
Scatter/Gather メカニズム
OpenSearch は、検索リクエストを実行してクライアントにレスポンスを返す際に Scatter/Gather メカニズムに従います。検索リクエストがクラスターノードに到着すると、そのノードはリクエストのコーディネーターとして機能します。コーディネーターは、検索リクエストを処理するために使用するインデックスとシャードを決定し、can_match、dfs、query、fetch などの検索リクエストの各フェーズを調整します。検索リクエストのタイプに応じて、1 つ以上のフェーズが実行されます。例えば、size が 0 のリクエストでは、fetch フェーズは省略され、query フェーズのみが実行されます。
Query フェーズ
ここでは、実際の検索リクエスト処理とドキュメント収集が行われる query フェーズに焦点を当てます。シャードを特定した後、コーディネーターノードは各シャードに内部リクエスト (「シャード検索リクエスト」と呼びます) をバッチで送信します (各ノードに並列で送信されるシャード検索リクエストの数を制限します)。各シャードがドキュメントのセットを返すと、コーディネーターはレスポンスをマージし、最終的なレスポンスをクライアントに返します。以下の図に、query フェーズ中のコーディネーターノードとデータノード間の通信を示します。

Query フェーズのワークフロー
シャードが検索リクエストを受信すると、(必要に応じて) クエリを書き換え、Lucene を使用してリクエストを処理するために必要なクエリ、ウェイト、コレクターツリーなどの内部オブジェクトを作成します。これらは、検索リクエストのボディで定義されたフィルター、terminate_after、アグリゲーションなどのさまざまな句や操作を表します。セットアップが完了すると、Lucene の検索 API が呼び出され、シャードのすべてのセグメントに対して順次検索が実行されます。
各セグメントに対して、Lucene はクエリで提供されたフィルターとブール句を適用し、一致するドキュメントを特定してスコアリングします。その後、一致したドキュメントはコレクターツリーを通過し、一致したドキュメント ID が収集されます。例えば、TopDocsCollector は上位の一致ドキュメントを収集し、さまざまな BucketCollector オブジェクトはアグリゲーションタイプに応じて一致するドキュメントの値を集計します。
すべての一致ドキュメントが収集されると、OpenSearch は後処理を実行して最終的なシャードレベルの結果を作成します。例えば、アグリゲーションでは各アグリゲーション結果の内部表現 (InternalAggregation) が作成され、コーディネーターノードに送り返される後処理が実行されます。コーディネーターノードはシャード全体のすべてのアグリゲーション結果を蓄積し、reduce 操作を実行して検索レスポンスで返される最終的なアグリゲーション出力を作成します。以下の図に、非並列の query フェーズワークフローを示します。

並列セグメント検索
Apache Lucene の初期の頃、インデックスサーチャーはセグメントを並列に走査する新しい実験的機能で強化されました。理論的には、並列セグメント検索は複数のスレッドを使用してセグメントを検索することで、検索レイテンシの顕著な改善を提供します。
並列セグメント走査の最初の実装は、OpenSearch 2.0.0 でサンドボックスプラグインとして含まれました (この PR を参照)。その後、アグリゲーションのサポート、プロファイル API の強化、統計情報などのギャップをすべて埋め、この機能を GA 対応に向けて進めることを決定しました (対応する GitHub issue を参照)。では、なぜ並列セグメント検索は特定のワークロードにとってゲームチェンジャーなのでしょうか?
並列検索の仕組み
並列セグメント検索は、前述の Lucene の機能を使用して、query フェーズ中にシャード検索リクエストを処理します。コーディネーターノードとシャード間のフローに変更はありません。シャード検索リクエストでは、一度に 1 つのセグメントを検索して一致ドキュメントを収集する代わりに、並列セグメント検索はセグメント全体で並列に収集を実行します。並列検索モデルでは、シャードのすべてのセグメントが「スライス」と呼ばれる複数の作業単位に分割されます。各リクエストのスライス数を計算するために、デフォルトでは OpenSearch は Lucene のヒューリスティクスを使用し、最大 250,000 ドキュメントまたは 5 セグメントのいずれか先に達した方をスライスあたりの上限とします。各スライスには 1 つ以上のセグメントが割り当てられ、各シャード検索リクエストには 1 つ以上のスライスが作成されます。各スライスは、すべてのシャード検索リクエストで共有される index_searcher スレッドプール内の別々のスレッドで実行されます。すべてのスライスの実行が完了すると、各スライスは収集された結果に対して reduce 操作を実行し、コーディネーターノードに送信される最終的なシャードレベルの結果を作成します。
CollectorManager
この並列実行モデルを実現するために、Apache Lucene は Scatter/Gather パターンを適用し、newCollector と reduce の 2 つのメソッドを提供する CollectorManager インターフェースを導入しました。各シャード検索リクエストに対して、OpenSearch は Collector ツリーの代わりに CollectorManager ツリー (並列検索フローでセットアップ) を作成し、この CollectorManager ツリーを Apache Lucene Search API に渡します。
計算されたスライス数に応じて、Apache Lucene は各スライスに対して CollectorManager::newCollector を使用して Collector ツリーを作成します。すべてのスライスの実行が完了すると、Lucene は CollectorManager::reduce を呼び出して、各スライスの Collector インスタンス全体で収集された結果を reduce します。これは、Apache Lucene または OpenSearch でサポートされている各操作が並列モデルをサポートするには、対応するスレッドセーフな Collector ツリーを作成できる CollectorManager インターフェースの実装が必要であることを意味します。ネイティブの Lucene コレクターについては、CollectorManager の実装がすぐに使用できます。OpenSearch で実装されたコレクターは CollectorManager を実装する必要があります (例えば、MinimumCollectorManager や、すべてのアグリゲーションを処理するための AggregationCollectorManager を実装しました)。以下の図に、並列 query フェーズのワークフローを示します。

アグリゲーションを伴う並列検索
アグリゲーションサポートの課題の 1 つは、OpenSearch が約 50 種類の異なるアグリゲーションコレクターをサポートしていることです。並列検索をサポートするために、さまざまなオプションを検討しました (この GitHub issue を参照)。各アグリゲーションコレクターを個別に処理するのではなく、異なるアグリゲーションタイプに対して汎用的な方法で reduce 操作をサポートできるメカニズムを探していました。その方針に従い、アグリゲーションの reduce は現在コーディネーターノードでシャード全体の結果に対して実行されているため、シャード上のスライスレベルの reduce にも同様のメカニズムを適用できることに気づきました。したがって、並列検索が有効な場合、アグリゲーション操作を含む検索リクエストでは、各シャードでスライス全体の結果に対して reduce 操作が発生します。その後、シャード全体の結果を収集するためにコーディネーターノードで別の reduce 操作が発生します。
パフォーマンス結果
現在、この機能は実験的としてリリースされており、本番環境での使用は推奨されていません。本番環境で安定した機能となるよう、発見されたすべての問題の解決に取り組んでいます。機能のステータスは プロジェクトボード の並列検索セクションで追跡できます。現在の機能を使用して、OpenSearch Benchmark の nyc_taxis ワークロード を使用した予備的なベンチマーク数値を収集しました。以下のセクションでは、これらのベンチマーク結果をまとめています。
クラスター構成 1
- インスタンスタイプ: r5.2xlarge
- ノード数: 1
- シャード数: 1
- 検索クライアント数: 1
レイテンシ比較 (p90)
| ワークロード操作 | CS 無効 (ms) | CS 有効 (Lucene デフォルトスライス) (ms) | 改善率 (正の値が良い) | CS 有効 (固定スライス数=4) (ms) | 改善率 (正の値が良い) |
|---|---|---|---|---|---|
| default | 4.4 | 4.6 | −5% | 5 | −14% |
| range | 234 | 81 | +65% | 91 | +61% |
| date_histogram_agg | 582 | 267 | +54% | 278 | +52% |
| distance_amount_agg | 15348 | 4022 | +73% | 4293 | +72% |
| autohisto_agg | 589 | 260 | +55% | 273 | +53% |
平均 CPU 使用率
| CS 無効 | CS 有効 (Lucene デフォルトスライス) | CS 有効 (固定スライス数=4) |
|---|---|---|
| 12% | 65% | 43% |
クラスター構成 2
- インスタンスタイプ: r5.8xlarge
- ノード数: 1
- シャード数: 1
- 検索クライアント数: 1
レイテンシ比較 (p90)
| ワークロード操作 | CS 無効 (ms) | CS 有効 (Lucene デフォルトスライス) (ms) | 改善率 (正の値が良い) | CS 有効 (固定スライス数=4) (ms) | 改善率 (正の値が良い) |
|---|---|---|---|---|---|
| default | 4.25 | 4.7 | −11% | 4.1 | +3% |
| range | 227 | 91 | +59% | 74 | +67% |
| date_histogram_agg | 558 | 271 | +51% | 255 | +54% |
| distance_amount_agg | 12894 | 3334 | +74% | 3790 | +70% |
| autohisto_agg | 537 | 286 | +46% | 278 | +48% |
平均 CPU 使用率
| CS 無効 | CS 有効 (Lucene デフォルトスライス) | CS 有効 (固定スライス数=4) |
|---|---|---|
| 3% | 14% | 11% |
これらは初期の結果であり、並列セグメント検索がパフォーマンスに与える影響を測定するために他のシナリオも実行する予定です。複数のクライアントやノードあたり複数のシャードなど、追加の実際のシナリオからのベンチマーク結果を提供するフォローアップ記事を公開し、観察結果を共有します。このメタ issue では、さまざまな構成とシナリオを含むベンチマーク計画を追跡しています。追求する価値のある他のシナリオがあると思われる場合は、この issue にフィードバックを残してください。これらのシナリオや他のカスタムワークロードを実行し、結果を共有することで貢献することもできます。
並列検索の有効化
クラスターで並列セグメント検索を有効にして使用するには、並列検索のドキュメントの手順に従ってください。
制限事項
並列検索をまだサポートしていない領域がいくつかあります (例えば、ParentJoinAggregation)。詳細については、ドキュメントの制限事項セクションを参照してください。
まとめ
OpenSearch で並列セグメント検索をサポートできることを嬉しく思います。非本番環境でこの機能を試し、機能をより堅牢でユーザーフレンドリーにするために修正できるギャップや問題についてフィードバックを提供することを強くお勧めします。この機能の進捗状況はこのプロジェクトボードで追跡できます。
OpenSearch Project(OSS) の Publicationです。 OpenSearch Tokyo User Group : meetup.com/opensearch-project-tokyo/
Discussion