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=0logs-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点を確かめます。
- 集計の元データを見直す。
shard-reports/shards.jsonの行数と、スクリプトが数えたプライマリの総数が合っているか - 統合先のディスクが足りるか。ガイドも、シャードのサイズを直すにはインデックスをコピーするため、事前にディスクの空きを確かめるよう注意しています
- 変更後の総シャード数を
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の条件まで含めて説明できます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。