Elasticsearch検索遅延:1TB/日ログ基盤でのシャード最適化とILM導入実録

「Elasticsearchの検索が、特定の時間帯になるとタイムアウトする」。SREチームとして最も聞きたくない報告の一つです。私が担当していた、1日あたり約1TBのログデータを処理する大規模なELKスタック基盤において、まさにこの現象が発生しました。 当初は平均200msで応答していた検索クエリが、データ量の増加に伴い、ピーク時には3秒〜5秒、最悪の場合はタイムアウトエラーを返すようになりました。ヒープメモリの使用率は常に85%を超え、Full GCが頻発するという典型的な「クラスター疲弊」の症状です。 本記事では、単純なノード増設(スケールアウト)に頼らず、根本的な原因であった不適切なシャード配置を見直し、ElasticsearchチューニングとILM(Index Lifecycle Management)によってパフォーマンスをV字回復させたプロセスを共有します。

ボトルネック分析:なぜヒープは枯渇するのか

問題が発生していた環境は、Elasticsearch 7.17系、データノード12台(AWS r5.2xlarge、メモリ64GB)で構成されていました。 Kibanaからのダッシュボード表示や、アプリケーションからのログ検索において、特に「過去30日間のデータ」を対象としたクエリが著しく遅延していました。

_cat/shards APIと_nodes/statsを用いて詳細を分析したところ、以下の重大な設計ミスが浮き彫りになりました。

  • 過剰なシャード数(Over-sharding): 1インデックスあたりのサイズが小さすぎる(数百MB〜数GB)にもかかわらず、日次で新しいインデックスを作成しており、クラスター全体でシャード数が40,000を超えていました。
  • シャードサイズの不均衡: ログ流量が多い日は1シャードあたり80GBに達する一方、少ない日は1GB未満というバラつきがあり、リソース使用効率が極端に悪い状態でした。
Critical Error:
Elasticsearchの各シャードはLuceneインデックスそのものであり、オープンしているだけでヒープメモリ上のリソース(Segment Memory)を消費します。経験則として、1GBのヒープあたり20シャード以下に抑えるのが推奨されますが、この環境ではその閾値を大幅に超えていました。

失敗したアプローチ:日次インデックスの維持

最初に取り組んだのは、「古いインデックスをクローズする」という対症療法的なアプローチでした。スクリプトを組み、30日以上前のインデックスをclose状態にしましたが、これは根本解決にはなりませんでした。 なぜなら、検索対象となる「直近7日間」のホットなデータだけでも、トラフィックのスパイクによってシャード数が爆発的に増えていたからです。また、日次でインデックスを切る戦略(logs-2025-01-01のような命名)では、データ量が少ない日のシャードが無駄になり、逆にデータ量が多い日は1シャードが肥大化しすぎてリカバリが終わらないというジレンマに陥りました。

解決策:ILMによるRolloverとHot-Warmアーキテクチャ

この問題を解決するために採用したのが、適切なシャーディング戦略に基づくRollover(ロールオーバー)設定と、ILMを用いたHot-Warm-Coldアーキテクチャの導入です。 目標とするシャードサイズを「30GB〜50GB」というスイートスポットに定め、時間ベースではなく「サイズベース」でインデックスを切り替える方針に転換しました。

以下は、実際に本番環境に適用したインデックスライフサイクル管理(ILM)ポリシーの定義です。

// PUT _ilm/policy/logs_optimized_policy
{
  "policy": {
    "phases": {
      "hot": {
        "min_age": "0ms",
        "actions": {
          "rollover": {
            // シャードサイズを均一化するための最重要設定
            "max_primary_shard_size": "50gb",
            "max_age": "2d"
          },
          "set_priority": {
            "priority": 100
          }
        }
      },
      "warm": {
        // HotからWarmへの移行:書き込みが終了したデータ
        "min_age": "2d",
        "actions": {
          "shrink": {
            // 検索効率向上のためシャード数を1に縮小
            "number_of_shards": 1
          },
          "forcemerge": {
            // 削除済みドキュメントをパージし、セグメントを集約
            "max_num_segments": 1
          },
          "allocate": {
            "require": {
              "data": "warm"
            }
          },
          "set_priority": {
            "priority": 50
          }
        }
      },
      "delete": {
        "min_age": "90d",
        "actions": {
          "delete": {}
        }
      }
    }
  }
}

この設定の肝は、max_primary_shard_size: "50gb"です。これにより、ログの流量に関わらず、シャードサイズが常に50GB付近になるように新しいインデックスが自動生成(Rollover)されます。 また、warmフェーズでのforcemergeは、読み取り専用となったインデックスの検索パフォーマンスを劇的に向上させます。セグメント数が1つに集約されることで、検索時のディスクI/OとCPUオーバーヘッドが最小化されるためです。これは検索エンジン最適化の観点からも非常に重要なステップです。

インデックステンプレートの適用

ILMポリシーを作成しただけでは機能しません。これをインデックステンプレートに紐付け、書き込み用エイリアスを設定する必要があります。

// PUT _index_template/logs_template
{
  "index_patterns": ["logs-prod-*"],
  "template": {
    "settings": {
      "index.lifecycle.name": "logs_optimized_policy",
      "index.lifecycle.rollover_alias": "logs-prod-write",
      "number_of_shards": 2,
      "number_of_replicas": 1,
      "refresh_interval": "30s" 
      // インジェスト負荷を下げるためデフォルト(1s)から変更
    }
  }
}

ここでrefresh_intervalを30秒に設定している点にも注目してください。リアルタイム性が厳密に求められないログ分析において、この値を増やすことはインデキシングスループットを向上させるための定石です。

導入後のパフォーマンス検証

この構成を適用し、既存のデータをReindex(再インデックス)してから1週間後のメトリクスを比較しました。結果は劇的でした。

指標 (Metric) 最適化前 (Legacy) 最適化後 (Optimized)
シャード総数 42,000+ 1,800
平均検索レイテンシ (P99) 3,200ms 180ms
ヒープメモリ使用率 Avg 85% (Spike 98%) Avg 45%
Full GC発生頻度 1回 / 10分 ほぼ発生せず

数値の変化について詳細に分析します。まず、シャード総数が激減したことで、各データノードが保持するクラスター状態(Cluster State)の管理コストが下がりました。これがマスターノードの安定化に寄与しています。 次に、forcemergeされたWarm層のインデックスに対する検索が高速化したことで、重い集計クエリ(Aggregations)のレスポンスが改善しました。 何より、ヒープメモリに余裕ができたことで、OSのページキャッシュ(FileSystem Cache)に利用できるメモリが増え、ディスクI/O自体が減少したことがレイテンシ改善の最大の要因です。

Elasticsearch公式:検索速度のチューニングガイド

注意点とエッジケース

このILM戦略は強力ですが、万能ではありません。導入時に遭遇したいくつかの副作用と注意点を共有します。

Shrink操作のリソース消費:
Warmフェーズへ移行する際のshrinkアクション(シャード縮小)は、内部的に新しいインデックスを作成してセグメントをコピーするため、一時的にディスクI/Oとネットワーク帯域を大量に消費します。ILMの実行タイミングがピークタイムと重ならないように調整するか、深夜帯に実行されるようmin_ageを調整する必要があります。

また、Rolloverを使用すると、インデックス名に連番が付与されるため(例:logs-prod-000001)、アプリケーション側で物理インデックス名を指定している場合は修正が必要です。必ずエイリアス(logs-prod-writeや読み取り用エイリアス)を経由してアクセスするように変更してください。

さらに、データノードにnode.attr.data: hotwarmといった属性を付与し、elasticsearch.ymlで正しく設定していないと、allocateアクションでシャードが移動できず、ILMがエラー状態でスタックする可能性があります。GET _ilm/explainコマンドは、このような構成ミスをデバッグする際の強力な味方となります。

成果:
最終的に、ハードウェアを追加することなく、検索パフォーマンスを約17倍高速化し、インフラコストの増大を防ぐことに成功しました。

結論

Elasticsearchのパフォーマンス問題の多くは、リソース不足ではなく、データモデリングとシャーディング戦略の不備に起因します。 特に時系列データを扱うELKスタック環境においては、漫然と日次インデックスを作成するのではなく、ILMを活用して「データの鮮度とサイズ」に基づいたライフサイクル管理を行うことが不可欠です。 今回紹介したHot-Warm構成とForce Mergeの組み合わせは、検索速度とコスト効率を両立させるための最良のプラクティスの一つと言えるでしょう。

Post a Comment