遅いElasticsearchクエリのトラブルシューティング:特定と解決手順

ヘルスチェック、スローログ、プロファイルAPI、マッピング、シャード、安全なクエリパターンを使用して、遅いElasticsearchクエリを診断する方法。

遅いElasticsearchクエリのトラブルシューティング:特定と解決手順

遅いElasticsearchクエリは、単一の普遍的な設定で修正されることはほとんどありません。クエリが遅くなる原因は、データのスキャン量が多すぎる、ヒットするシャードが多すぎる、高コストな集計を要求している、間違ったフィールドでソートしている、他の処理の背後で待機している、またはヒープやディスク帯域幅が不足しているノードに到達しているなど、さまざまです。

可能であれば、実際のリクエストから始めてください。「検索が遅い」という漠然とした報告は対処が困難です。コピーされたリクエスト本文、対象インデックスパターン、時間範囲、ユーザーが感じるレイテンシ、タイムスタンプがあれば、遅いクエリを同じ瞬間のクラスタメトリクスと比較できます。

Elasticsearchクエリレイテンシの理解

トラブルシューティングに入る前に、Elasticsearchのクエリパフォーマンスに影響を与える主要な要因を理解することが重要です:

  • データ量と複雑さ: データの総量、フィールド数、ドキュメントの複雑さは、検索時間に直接影響します。
  • クエリの複雑さ: 単純なtermクエリは高速ですが、多くの句、集計、scriptクエリを含む複雑なboolクエリはリソースを大量に消費する可能性があります。
  • マッピングとインデックス戦略: データのインデックス方法(例:text vs keywordフィールド、fielddataの使用)は、クエリの効率に大きく影響します。
  • クラスタの健全性とリソース: クラスタノードのCPU、メモリ、ディスクI/O、ネットワークレイテンシは重要です。不健全なクラスタやリソースが制約されたノードは、必然的にパフォーマンス低下を引き起こします。
  • シャーディングとレプリケーション: シャードの数とサイズ、およびノード間の分散方法は、並列処理とデータ取得に影響します。

遅いクエリの初期チェック

高度なプロファイリングツールを使用する前に、常にこれらの基本的なチェックから始めてください:

1. クラスタの健全性を監視する

_cluster/health APIを使用して、Elasticsearchクラスタの全体的な健全性を確認します。redステータスはプライマリシャードの欠落を示し、yellowは一部のレプリカシャードが未割り当てであることを示します。どちらもクエリパフォーマンスに深刻な影響を与える可能性があります。

GET /_cluster/health

status: greenを確認してください。しかし、そこで止まらないでください。グリーンのクラスタでも、過負荷、シャードの不適切な分散、非効率的なクエリの実行が発生する可能性があります。

2. ノードリソースを確認する

個々のノードのリソース使用状況を調査します。高いCPU使用率、低い利用可能メモリ(特にヒープ)、または飽和したディスクI/Oは、ボトルネックの強力な指標です。

GET /_cat/nodes?v
GET /_cat/thread_pool?v

cpuload_1mheap.percentdisk.used_percentに注意してください。searchスレッドプールのキューサイズが大きい場合も、過負荷を示しています。

3. スローログを分析する

Elasticsearchは、定義されたしきい値を超えるクエリをログに記録できます。これは、個々のリクエストを詳細に調査することなく、特定の遅いクエリを特定するための優れた最初のステップです。

スローログのしきい値はインデックス設定です。影響を受けるインデックスまたはインデックスパターンに適用して、すべてのノードログを氾濫させることなく、有用な例を取得します:

PUT /my-index/_settings
{
  "index.search.slowlog.threshold.query.warn": "10s",
  "index.search.slowlog.threshold.fetch.warn": "1s"
}

その後、Elasticsearchログで[WARN][index.search.slowlog]のようなエントリを監視します。

深堀り:プロファイルAPIを使用したボトルネックの特定

初期チェックで問題が特定できない場合、または特定のクエリが遅い理由を理解する必要がある場合、ElasticsearchプロファイルAPIが最も強力なツールです。これは、各コンポーネントが費やした時間を含め、クエリが低レベルでどのように実行されるかを詳細に示します。

プロファイルAPIとは?

プロファイルAPIは、検索リクエストの完全な実行計画を返し、各クエリコンポーネント(例:TermQueryBooleanQueryWildcardQuery)と収集フェーズにかかった時間を詳細に示します。これにより、クエリのどの部分が最も時間を消費しているかを正確に特定できます。

プロファイルAPIの使用方法

既存の検索リクエスト本文に"profile": trueを追加するだけです:

GET /your_index/_search?profile=true
{
  "query": {
    "bool": {
      "must": [
        { "match": { "title": "elasticsearch" } }
      ],
      "filter": [
        { "range": { "date": { "gte": "now-1y/y" } } }
      ]
    }
  },
  "size": 0, 
  "aggs": {
    "daily_sales": {
      "date_histogram": {
        "field": "timestamp",
        "fixed_interval": "1d"
      }
    }
  }
}

注意: プロファイルAPIはオーバーヘッドを追加するため、特定のクエリのデバッグに使用し、すべてのリクエストで本番環境で使用しないでください。

プロファイルAPI出力の解釈

出力は冗長ですが構造化されています。profileセクション内で注目すべき主要なフィールドは次のとおりです:

  • type: 実行されているLuceneクエリまたはコレクターのタイプ(例:BooleanQueryTermQueryWildcardQueryMinScoreCollector)。
  • description: コンポーネントの人間が読める説明。多くの場合、操作対象のフィールドと値が含まれます。
  • time_in_nanos: このコンポーネントとその子コンポーネントが費やした合計時間(ナノ秒)。
  • breakdown: 異なるフェーズ(例:rewritebuild_scorernext_docadvancescore)で費やされた時間の詳細な内訳。

解釈例: WildcardQueryまたはRegexpQuerytime_in_nanosが高く、rewriteにかなりの時間が費やされている場合、クエリの書き換え(ワイルドカードパターンの展開)が非常に高コストであることを示しています。特に、カーディナリティの高いフィールドや大規模なインデックスで顕著です。

...
"profile": {
  "shards": [
    {
      "id": "_na_",
      "searches": [
        {
          "query": [
            {
              "type": "BooleanQuery",
              "description": "title:elasticsearch +date:[1577836800000 TO 1609459200000}",
              "time_in_nanos": 12345678,
              "breakdown": { ... },
              "children": [
                {
                  "type": "TermQuery",
                  "description": "title:elasticsearch",
                  "time_in_nanos": 123456,
                  "breakdown": { ... }
                },
                {
                  "type": "PointRangeQuery",
                  "description": "date:[1577836800000 TO 1609459200000}",
                  "time_in_nanos": 789012,
                  "breakdown": { ... }
                }
              ]
            }
          ],
          "aggregations": [
            {
              "type": "DateHistogramAggregator",
              "description": "date_histogram(field=timestamp,interval=1d)",
              "time_in_nanos": 9876543,
              "breakdown": { ... }
            }
          ]
        }
      ]
    }
  ]
}
...

この簡略化された例では、DateHistogramAggregatorが不釣り合いに高いtime_in_nanosを示している場合、集計がボトルネックです。

遅いクエリの一般的な原因と解決戦略

プロファイルAPIの結果と一般的なクラスタ状態に基づいて、一般的な問題とその解決策を以下に示します:

1. 非効率的なクエリ設計

問題: 特定のクエリタイプは、特に大規模なデータセットでは本質的にリソースを大量に消費します。

  • wildcardprefixregexp クエリ:多くの用語を反復処理する必要があるため、非常に遅くなる可能性があります。
  • script クエリ:フィルタリングやスコアリングのためにすべてのドキュメントでスクリプトを実行することは非常に高コストです。
  • 深いページネーション:非常に大きなオフセットでfromsizeを使用すること。
  • 多すぎるshould:数百または数千のshould句を持つブールクエリは、非常に遅くなる可能性があります。

解決手順:

  • 大きなフィールドでの広範なwildcard / prefix / regexpクエリを避ける:
    • 入力中検索には、インデックス時にcompletion suggesterまたはn-gramを使用します。
    • 正確なプレフィックスには、目的に応じたプレフィックスフィールド、index_prefixes、またはデータに一致するkeyword戦略を検討します。
  • scriptクエリを最小限に抑える: ロジックをインジェスト時(専用フィールドの追加など)に移動できるか、標準のクエリ/集計で処理できるかを再評価します。
  • ページネーションを最適化する: ユーザー向けの深いページネーションには、安定したソートでsearch_afterを使用します。バッチ抽出ジョブにはスクロールAPIを使用し、インタラクティブな検索ページには使用しないでください。
  • shouldクエリをリファクタリングする: 類似の句を結合するか、適切な場合はクライアント側のフィルタリングを検討します。

2. マッピングの欠落または非効率性

問題: 不適切なフィールドマッピングにより、Elasticsearchが高コストな操作を強制される可能性があります。

  • 正確なマッチング/ソート/集計に使用されるテキストフィールド: textフィールドは分析されトークン化されるため、正確なマッチングが非効率になります。ソートや集計にはfielddataが必要であり、これはヒープを大量に消費します。
  • 過剰なインデックス作成: 検索されない、または不必要に分析されるフィールドのインデックス作成。

解決手順:

  • 正確なマッチング、ソート、集計にはkeywordを使用する: 正確なマッチング、フィルタリング、ソート、集計が必要なフィールドには、keywordフィールドタイプを使用します。
  • multi-fieldsを活用する: 同じデータを異なる方法でインデックス化します(例:全文検索用のtitle.textと正確なマッチング・集計用のtitle.keyword)。
  • 未使用の検索可能フィールドのindexを無効にする: フィールドが表示のみで検索されない場合は、"index": falseを検討します。_sourceの無効化には注意が必要です。更新、再インデックス、デバッグ、リカバリワークフローに影響します。

3. シャーディングの問題

問題: 不適切なシャードの数やサイズは、負荷分散の不均一や過剰なオーバーヘッドを引き起こす可能性があります。

  • 多すぎる小さなシャード: 各シャードにはオーバーヘッドがあります。小さなシャードが多すぎると、マスターノードに負荷がかかり、ヒープ使用量が増加し、リクエスト数が増加して検索が遅くなる可能性があります。
  • 少なすぎる大きなシャード: 検索中の並列処理が制限され、ノードに「ホットスポット」が発生する可能性があります。

解決手順:

  • 最適なシャードサイズ: シャードサイズは10GBから50GBを目指します。時間ベースのインデックス(例:logs-YYYY.MM.DD)とロールオーバーインデックスを使用して、シャードの成長を管理します。
  • 再インデックスと縮小/分割: _reindex_split_shrink APIを使用して、既存のインデックスのシャードを統合またはサイズ変更します。
  • シャードの分散を監視する: シャードがデータノード間で均等に分散されていることを確認します。

4. ヒープとJVM設定

問題: JVMヒープメモリの不足または最適でないガベージコレクションは、頻繁な一時停止とパフォーマンス低下を引き起こす可能性があります。

解決手順:

  • 十分なヒープを割り当てる: XmsXmxを同じ値に設定します。一般的な開始点は、物理RAMの半分以下で、圧縮通常オブジェクトポインタのしきい値(多くの場合30GB弱)を下回るようにします。
  • JVMガベージコレクションを監視する: GET _nodes/stats/jvm?prettyまたは専用の監視ツールを使用してGC時間を確認します。頻繁または長時間のGCポーズはヒーププレッシャーを示しています。

5. ディスクI/Oとネットワークレイテンシ

問題: ストレージの低速またはネットワークのボトルネックは、クエリレイテンシの根本的な原因となる可能性があります。

解決手順:

  • 高速ストレージを使用する: ElasticsearchデータノードにはSSDを強く推奨します。高性能ユースケースにはNVMe SSDがさらに適しています。
  • 十分なネットワーク帯域幅を確保する: 大規模クラスタやインデックス作成/クエリが頻繁な環境では、ネットワークスループットが重要です。

6. Fielddataの使用

問題: ソートや集計のためにtextフィールドでfielddataを使用すると、大量のヒープを消費し、OutOfMemoryError例外を引き起こす可能性があります。

解決手順:

  • textフィールドでfielddata: trueを避ける: この設定は、理由があってtextフィールドではデフォルトで無効になっています。代わりに、multi-fieldsを使用して、ソート/集計用のkeywordサブフィールドを作成します。

クエリ最適化のベストプラクティス

遅いクエリを予防的に防ぐには:

  • スコアリング不要の条件にはfilterコンテキストを優先する: rangetermexists条件にレリバンススコアリングが必要ない場合は、boolクエリのfilter句に配置します。フィルターはスコアリングをスキップし、Elasticsearchが最適化しやすいことがよくあります。
  • フィルタリングにはconstant_scoreクエリを使用する: これは、キャッシュの利点のためにフィルターコンテキストで実行したいqueryfilterではない)がある場合に便利です。
  • キャッシュの再利用に適した設計を行う: Elasticsearchは自動的にキャッシュするものを決定します。安定したデータに対する繰り返しのフィルターは、常に変化する値を持つ一意の単発フィルターよりも恩恵を受けます。
  • indices.query.bool.max_clause_countを調整する: 多くのshould句でデフォルトの制限(1024)に達した場合は、クエリの再設計を検討するか、この設定を増やします(注意して)。
  • 定期的な監視: クラスタの健全性、ノードリソース、スローログ、クエリパフォーマンスを継続的に監視して、問題を早期に発見します。
  • テスト、テスト、テスト: 本番環境にデプロイする前に、ステージング環境で現実的なデータ量とワークロードに対して常にクエリパフォーマンスをテストします。

最善のクエリ修正は、通常、証拠に現れます。スローログはリクエストの形状を示します。プロファイルAPIはクエリのどの部分が時間を消費しているかを示します。ノード統計は、クエリ実行時にクラスタに十分なCPU、ヒープ、ディスクI/Oがあったかどうかを示します。設定を変更する前にこれらを組み合わせることで、実際の問題が続いている間に症状を調整することを避けられます。