Claude Media
Claude CodeでElasticsearchのシャード過多を診断する手順

Claude CodeでElasticsearchのシャード過多を診断する手順

_cat/shardsの出力をファイルに落としてClaude Codeに集計させ、小さすぎるシャードの洗い出しからrolloverとshrinkの方針決めまでを進める手順をまとめます。

Elasticsearchのシャード過多(oversharding)は、検索の遅さとクラスターの不安定さとして表に出ます。原因の切り分けは、_cat/shardsの出力を眺めて小さなシャードの塊を見つけるところから始まります。この記事では、その出力をファイルにしてClaude Codeに集計させ、統合とrolloverの方針を決めるまでの流れを、権限の切り方も含めて順に説明します。

Claude Codeにクラスターへ直接つながせる必要はありません。人間が読み取り系のAPIを実行して結果を保存し、Claudeにはそのファイルだけを読ませる構成にすると、診断の範囲で書き込みの余地が残りません。

小さすぎるシャードは何が問題か

シャード過多とは、シャードが多すぎて検索性能とクラスターの安定性が落ちる状態です。Elasticsearchのシャードサイズ設計ガイドは、これを避けるための目安を2つ挙げています。

数字

シャードサイズの目安

  • 1シャードのサイズ

    10GB〜50GB

    多くの用途でうまく動く範囲

  • 1シャードの文書数

    2億件未満

    サイズと併せて見る

シャードサイズ設計ガイドの記載

小さいシャードが重い理由は3点あります。

  • 検索はシャードごとに1つのCPUスレッドで動きます。多数のシャードにまたがる検索は、ノードの検索スレッドプールを使い切ることがあります
  • インデックスとシャードにはそれぞれメモリとCPUのオーバーヘッドがあります。多くの場合、少数の大きなシャードのほうが、多数の小さなシャードより資源を使いません
  • セグメントのメタデータの一部はヒープ上に置かれます。シャードが育ってセグメントが統合されるほど、ヒープ上のメタデータは減ります

ガイドには「50MBのシャードを1000個検索するほうが、同じデータを持つ50GBのシャード1個を検索するより、はるかに高くつく」という趣旨の説明もあります。逆に大きすぎるシャードも検索を遅くし、障害後の復旧に時間がかかります。診断は「小さいものを減らす」ことが中心でも、大きすぎるものが混じっていないかは同時に見ます。

マスター適格ノードにも上限があります。管理できるインデックス数はヒープに比例し、目安は「ヒープ1GBあたり3000インデックス未満」です。ヒープ4GBの専用マスターなら12000インデックス未満という計算になります。

出力をファイルにしてClaude Codeに渡す

シャードのサイズは_cat/shardsで確認します。ガイドの例は次の形です。

GET _cat/shards?v=true&h=index,prirep,shard,store&s=prirep,store&bytes=gb

これはKibanaのDev Tools向けの書き方です。Claude Codeに渡す材料にするときは、ターミナルからJSON形式で保存します。サイズの単位は集計側で換算できるよう、バイト単位で出します。

mkdir -p shard-reports
curl -s -H "Authorization: ApiKey $ES_API_KEY" \
  "$ES_URL/_cat/shards?format=json&bytes=b&h=index,prirep,shard,store,docs,node" \
  > shard-reports/shards.json

このcurlは人間が自分のシェルで実行します。Claudeに実行させない理由は、次の節で説明するBash権限の性質にあります。保存したファイルだけを読ませる運用なら、Claude側に必要な許可はReadだけです。

作業用の権限を絞る

Claude Codeのルールは、deny、ask、allowの順に評価されます。最初に一致したルールで結果が決まり、具体性の高いルールが優先されるわけではありません。診断用のプロジェクトでは、次のように書きます。

{
  "permissions": {
    "allow": [
      "Read(./shard-reports/**)"
    ],
    "deny": [
      "Read(./.env)",
      "Bash(curl *)"
    ]
  }
}

Bash(curl *)をdenyに置くと、Claudeがcurlでクラスターを直接叩く経路を塞げます。ただしBashのdenyルールは、Claudeが通常書く形の呼び出しを止めるもので、セキュリティ境界ではありません。/usr/bin/curlのようなパス指定やsh -cの中のcurlには一致しません。確実に塞ぎたいときは、サンドボックスのネットワーク許可リストと組み合わせます。

引数でURLを絞るallowルール、たとえばcurlの宛先を特定のホストに限る書き方は、オプションの順序やリダイレクトで簡単に外れます。「読み取りだけ」を守る本命は、保存用に使うAPIキーをElasticsearch側で読み取り専用の権限にすることです。

クラスターに直接つなぐMCPサーバーの使い方はElasticsearch MCPサーバーの使い方にあります。このサーバーにはシャード情報を取るget_shardsツールもあります。

小さいシャードを系列ごとに洗い出す

_cat/shardsの出力は、日付付きのインデックスが多いクラスターでは数千行になります。1行ずつ読ませても意味がないので、まず系列(logs-app-2026.10.01のような日付違いの集まり)ごとに集計します。次のスクリプトをプロジェクトに置き、Claudeに実行させます。

import json, re, sys
from collections import defaultdict
 
GB = 1024 ** 3
rows = json.load(open(sys.argv[1]))
groups = defaultdict(lambda: {"shards": 0, "bytes": 0, "small": 0})
for r in rows:
    if r["prirep"] != "p" or r["store"] is None:
        continue
    name = r["index"].removeprefix(".ds-")
    # 日付と連番を落として、同じ系列のインデックスを1行にまとめる
    name = re.sub(r"[-.]?\d{4}[.\-]\d{2}([.\-]\d{2})?", "", name)
    name = re.sub(r"-\d{6}$", "", name)
    g = groups[name]
    size = int(r["store"])
    g["shards"] += 1
    g["bytes"] += size
    g["small"] += size < 10 * GB
for name, g in sorted(groups.items(), key=lambda x: -x[1]["shards"]):
    avg = g["bytes"] / g["shards"] / GB
    print(f'{name}\tprimaries={g["shards"]}\tavg={avg:.2f}GB\tunder10GB={g["small"]}')

プライマリシャード(prirepがp)だけを数え、未割り当てでstoreが空の行は飛ばします。10GBという閾値は、ガイドが示すサイズ範囲の下限です。レプリカを含めると系列の見た目のシャード数が倍になるので、サイズの判断にはプライマリだけを使います。

python3 -I shard_report.py shard-reports/shards.json

出力は、たとえば次のような形になります(数字は説明用の例です)。

logs-app        primaries=840   avg=0.21GB   under10GB=840
metrics-host    primaries=96    avg=38.40GB  under10GB=0

logs-appの系列は、840本のプライマリが平均0.21GBです。日次インデックスにプライマリを複数割り当てたまま、1日あたりのデータ量が少ない典型的な形です。一方のmetrics-hostはサイズの目安に収まっています。直すべき系列は、まず1行目だと分かります。

CLAUDE.mdに診断の基準を書いておく

毎回の指示で基準がぶれないよう、判断の物差しをCLAUDE.mdに置きます。数値はシャードサイズ設計ガイドの記載に合わせます。

## Elasticsearchシャード診断
 
- 入力は shard-reports/*.json のみ。クラスターには接続しない
- プライマリシャードのサイズが10GB未満の系列を「小さい」、50GBを超えるものを「大きい」と分類する
- 1シャードの文書数が2億件を超える系列は別枠で報告する
- 提案は「系列名、現状、変更案、必要な作業」の表で出す。API呼び出しは例として書くだけで実行しない

この指示のもとでClaudeに「logs-app系列を10〜50GBのシャードに寄せる案を出して」と頼みます。出てきた案は、次の節の手段と突き合わせて確認します。

統合とrolloverの方針を決める

すでにシャードが多すぎるクラスターには、シャード数を減らす方法が4つあります。診断結果の系列ごとに、どれを当てるかを決めます。

状況手段補足
ILMでmax_ageだけでrolloverしている手段max_primary_shard_sizeに切り替える補足空のインデックスや小さなシャードが増えるのを避ける
max_ageが必要手段日次から週次・月次へ間隔を広げる補足保持期間の要件が許す範囲で
空のインデックスがある手段_cat/countで見つけて削除する補足空でも資源を消費する
書き込みが止まった古いインデックス手段force merge、shrink、reindexで統合する補足force mergeは負荷が高いので負荷の低い時間帯に

rolloverの閾値で再発を防ぐ

統合は一度きりの対処で、新しく作られるインデックスが小さければ同じ状態に戻ります。ILMを使っているなら、rolloverの条件を変えます。ガイドは、50GBを超えるシャードを避けるためにmax_primary_shard_sizeを50gbに、10GB未満を避けるためにmin_primary_shard_sizeを10gbに設定するよう案内しています。

ここで押さえておきたいのは、空のインデックスの扱いです。rolloverのドキュメントによると、書き込み用インデックスが空のときは、max_ageの条件を満たしていてもrolloverがスキップされます。min_docs: 0を指定したポリシーなら上書きでき、クラスター全体ではindices.lifecycle.rollover.only_if_has_documentsをfalseにして無効化できます。つまり空のインデックスが大量に見つかった系列は、この設定が変わっているか、rollover以外の経路でインデックスが作られている可能性があります。

データストリームでシャード数を変えたいときは、一致するインデックステンプレートのindex.number_of_shardsを変更します。新しい裏付けインデックスから反映されます。

既存インデックスの統合と、シャードの縮小

書き込みが止まった古いインデックスには、次の手段があります。

# 小さなセグメントを大きなセグメントに統合する(負荷の低い時間帯に)
POST my-index-000001/_forcemerge
 
# 日次インデックスを月次インデックスにまとめる
POST _reindex
{
  "source": { "index": "my-index-2099.10.*" },
  "dest":   { "index": "my-index-2099.10" }
}

reindexのあとは、元の小さなインデックスを削除します。シャード数だけを減らしたいなら、shrink APIがあります。ILMのwarmフェーズにはshrinkアクションがあり、max_primary_shard_sizeを指定すると、各シャードがその値を超えない最適なシャード数を自動で計算します。

shrinkには制約があります。number_of_shardsは元のインデックスのシャード数の約数でなければなりません。number_of_shardsとmax_primary_shard_sizeは同時に指定できません。プライマリが1つのインデックスはこれ以上縮められず、書き込み用インデックスにも使えません。hotフェーズで使うには、rolloverアクションがポリシーに含まれている必要があります。

Claudeには、系列ごとに「どの手段を使い、どの順で、何を確認するか」を作業計画として書かせます。実行するのは人間です。参考として、MongoDBのexplain()を読んでインデックスを設計する手順も、Claudeに診断を任せて変更は人が行う構成になっています。

出力を鵜呑みにしないための確認

Claudeが出した案は、実行前に3点を確かめます。

  1. 集計の元データを見直す。shard-reports/shards.jsonの行数と、スクリプトが数えたプライマリの総数が合っているか
  2. 統合先のディスクが足りるか。ガイドも、シャードのサイズを直すにはインデックスをコピーするため、事前にディスクの空きを確かめるよう注意しています
  3. 変更後の総シャード数をGET _cluster/stats?filter_path=indices.shards.totalで測り、変更前と比べる

シャードのサイズ自体を作り直す場合、スナップショットの復元やインデックスのcloneでは解決しません。ガイドが挙げているのは、Split Indexでプライマリ数を増やす方法と、設定を直した宛先インデックスを作ってreindexする方法の2つです。コピーが終わったあとは、元のインデックス名を宛先へのエイリアスにすると、参照する側を変えずに済みます。

シャード数の上限エラーが出たとき

過多の状態が進むと、インデックスやシャードの作成時に次のエラーが出ます。

this action would add [x] shards, but this cluster currently has [y]/[z] maximum normal shards open;

cluster.max_shards_per_nodeが、クラスター全体で開けるシャードの数を制限しています。ガイドの説明では、変更が安定性を損なわないと確信できるなら、この設定を一時的に上げて処理を進められます。ただし長期の解決策は、データ層のノードを増やすか、シャード数を減らすことです。恒久策が入ったら、設定をnullに戻します。

PUT _cluster/settings
{ "persistent": { "cluster.max_shards_per_node": null } }

Claudeにこのエラー文を渡すときも、shards.jsonと同じ要領でファイルにして読ませます。上限を上げるコマンドを提案させる場合は、「一時的な措置であり、戻す手順も併記する」ことを指示に入れておくと、上げたまま放置される事態を避けられます。

もう一つ、まれなエラーとしてNumber of documents in the shard cannot exceed [2147483519]があります。1シャードの文書数にはLuceneの上限があり、docs.countとdocs.deletedの合計に適用されます。推奨の2億件よりはるかに大きい値ですが、削除済み文書が多いシャードでは、_forcemerge?only_expunge_deletes=trueで削除済み文書を取り除ける場合があります。上限に近いなら、分割かreindexを計画します。

診断の依頼文をテンプレートにする

最後に、Claudeへの依頼を次の形に固定すると、毎回の診断の質がそろいます。

shard-reports/shards.json を shard_report.py で集計して、
小さい系列を平均サイズの小さい順に5つ挙げてください。
各系列について、現在のrollover設定(max_age / max_primary_shard_size)が
分かる場合は、それを前提に変更案を出してください。分からない場合は
「ILM設定の確認が必要」と書いてください。

ILMの設定自体は_cat/shardsには出ません。ポリシーを確認する出力も同じ要領でファイルに保存して渡すと、Claudeは小さい系列の原因を、rolloverの条件まで含めて説明できます。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. elastic.co/docs/deploy-manage/production-guidance/optimize-performanc…
  2. elastic.co/docs/manage-data/lifecycle/index-lifecycle-management/roll…
  3. elastic.co/docs/reference/elasticsearch/index-lifecycle-actions/ilm-s…
  4. code.claude.com/docs/en/permissions
この記事を共有:XはてブLinkedIn