Elasticsearchのインデックスとシャードの設定

## はじめに
Elasticsearchにおいて、インデックスとシャードの設定はシステムのパフォーマンスとスケーラビリティを左右する重要なポイントです。細切れのシャードが大量にある状態は検索のオーバーヘッドが大きく、逆にシャードが大き過ぎても検索速度が低下します。

![インデックスとシャード.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3901159/87f29ee8-027e-8959-3590-5f84729eca61.png)

この記事では、シャードのサイズと使用数が適切になるように、インデックスの作成単位とインデックスあたりのシャード数を設定する方法を考えます。

## シャードのサイズ

この記事における設定のポイントはシャードのサイズです。シャードのサイズについては、公式ドキュメントにベストプラクティスが示されているので、それを参考に以下の通りとします。

- 基準: 30GB
- 上限: 40GB

基準とするサイズは30GBですが、インデックスをシャードに分割する際に、ちょうど30GBずつに割り切れることはまず無いと思われます。割り切れない場合の余りは、40GBを超えない範囲でシャードを大きくする方向で調整し、それが出来なければ、30GB未満のシャードに細分化することにします。

参照したベストプラクティスは以下の通りです。

> **Aim for shards of up to 200M documents, or with sizes between 10GB and 50GB**
> ... Searching a thousand 50MB shards will be substantially more expensive than searching a single 50GB shard containing the same data. However, very large shards can also cause slower searches and will take longer to recover after a failure.
>
> 出典: https://www.elastic.co/guide/en/elasticsearch/reference/current/size-your-shards.html

シャードあたりのサイズが10GB～50GB程度になっているのが良いとの経験則があるようです。

> **TIP:** Avoid having very large shards as this can negatively affect the cluster's ability to recover from failure. There is no fixed limit on how large shards can be, but a shard size of 50GB is often quoted as a limit that has been seen to work for a variety of use-cases.
> ...
> **TIP:** Small shards result in small segments, which increases overhead. Aim to keep the average shard size between at least a few GB and a few tens of GB. For use-cases with time-based data, it is common to see shards between 20GB and 40GB in size.
>
> 出典: https://www.elastic.co/blog/how-many-shards-should-i-have-in-my-elasticsearch-cluster

こちらも同様の主旨ですが、時系列データであれば、20GB～40GBというより狭い範囲が示されています。

## インデックスの作成単位

次に、シャードの元であるインデックスについて考えます。

インデックスに登録するデータは通信パケットやシステムログなどの時系列データで、各データのサイズは以下の通りとします。説明のための架空の値ですが、実態に即したものです。

| データ | 1日あたりのサイズ |
|:-:|-:|
| データ1 |  10MB |
| データ2 |   1GB |
| データ3 |  10GB |
| データ4 | 100GB |

ログファイルは1日単位でファイル出力されることが多いため、そのまま1日単位でインデックスを作成したくなります。しかし、データ1のような小さいデータに対して、1日単位でインデックスを作成していくと、結果として小さいシャードが多数存在する状態になります。これには、検索のオーバーヘッドに加え、システムで扱える合計シャード数の上限の問題もあります。

> ... Could not index event to Elasticsearch ... this action would add [2] total shards, but this cluster currently has [999]/[1000] maximum shards open ...

合計シャード数の上限に達すると、Logstashのログにこのようなエラーが記録され、それ以上インデックスを登録できなくなっていることが分かります。

これを回避するには、データ1のような小さいデータは、ある程度の日数分をまとめてインデックスに登録することが有効と考えられます。

データ1は1日あたり10MBのため、1年分で4GB程度、10年分で40GB程度です。前節のシャードサイズの基準では、10年分を1インデックスにすれば良いことになりますが、管理のしやすさを考慮して最大でも1年分とします。また、任意の日数をインデックスの作成単位にするのも管理が複雑になるため、インデックスの作成単位は1日、1ヶ月、1年のいずれかとします。

- 1日: data-2025-01-10
- 1ヶ月: data-2025-01
- 1年: data-2025

作成単位の後ろに示しているのは、インデックスの命名規則です。たとえば、2025年1月10日のデータを1ヶ月単位のインデックスに登録する場合、インデックス名をdata-2025-01とします（`data`の部分は実際のデータに合わせて決めます）。

## インデックスとシャードの設定方法

設定のポイントをまとめ、実際の設定方法をステップ1, 2に分けて説明します。

### 設定のポイント

- インデックスの作成単位は1日、1ヶ月、1年のいずれかとする
- その中で、合計シャード数が最小となるものを選ぶ
- インデックスあたりのシャード数は、シャードサイズが30GB程度になるように計算する

### ステップ1: 設定値計算
Elasticsearchに登録するデータのサイズに基づき、インデックス作成単位とインデックスあたりのシャード数を決定します。

![block1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3901159/56ba5c56-ee8d-4621-9efc-368d99482e63.png)

決定方法として、インデックス作成単位を1日、1ヶ月、1年とした場合のそれぞれの1年間の合計シャード数を計算し、最小となるものを選びます。たとえば、1日あたりデータサイズが1GBのデータに対しては、インデックスのサイズは

1. インデックス作成単位が1日の場合: 1GB
1. インデックス作成単位が1ヶ月の場合: 30GB
1. インデックス作成単位が1年の場合: 365GB

となります。したがって、合計シャード数は

1. 1GB / 30GB = 1（切り上げ）より、1日あたり1個なので、1年で365個
1. 30GB / 30GB = 1より、1ヶ月あたり1個なので、1年で12個
1. 365GB / 30GB = 12（切り捨て）より、1年で12個

となります。合計シャード数が最小となるのは、インデックス作成単位が1ヶ月と1年の場合です。合計シャード数が同じなら、インデックスが細かい方が管理しやすいので、1ヶ月を選択します。

以上の設定値計算をPythonで実装したコードを示します。data_sizeを各データに応じて変更して実行します。

```py:設定値計算
# 入力
data_size = 1  # 1日あたりのデータサイズ (GB)

# パラメータ
shard_size = 30     # シャードの基準サイズ (GB)
shard_size_max = 40 # シャードの上限サイズ (GB)

# n日あたりのシャード数を計算
def shards_per_days(data_size, n):
    shards = data_size * n // shard_size
    if shards * shard_size_max < data_size * n:
        shards = shards + 1
    return shards

# インデックスあたりシャード数
shards_for_daily_index = shards_per_days(data_size, n=1)
shards_for_monthly_index = shards_per_days(data_size, n=30)
shards_for_yearly_index = shards_per_days(data_size, n=365)

# 1年間の合計シャード数の最小値
shards_list = [shards_for_daily_index * 365, shards_for_monthly_index * 12, shards_for_yearly_index]
min_shards = min(shards_list)
min_index = shards_list.index(min_shards)

# 出力
print(['1日', '1ヶ月', '1年'][min_index])
print([shards_for_daily_index, shards_for_monthly_index, shards_for_yearly_index][min_index])
```

先ほど示したデータ1～4に対して、設定値を求めた結果を示します。

| データ | 1日あたりのサイズ | インデックス<br>作成単位 | インデックスあたり<br>シャード数 |
|:-:|-:|-:|-:|
| データ1 |  10MB | 1年 | 1 |
| データ2 |   1GB | 1ヶ月 | 1 |
| データ3 |  10GB | 1ヶ月 | 10 |
| データ4 | 100GB | 1日 | 3 |

### ステップ2: 設定ファイル作成
求めた設定値に基づき、Logstashの設定ファイルを作成します。Elasticsearchではなく、Logstash側で設定する形となっています。

![block2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3901159/b7edd337-5d69-4c15-9f38-09deb51e0f8f.png)

例として、データ1に対するLogstash設定ファイル（関連部分のみ）を示します。

```ruby:/etc/logstash/conf.d/data1.conf
filter {
  ruby {
    code => "event.set('[@metadata][local_time]',event.get('[@timestamp]').time.localtime.strftime('%Y'))"
  }
}

output {
  elasticsearch {
    index => "data1-%{[@metadata][local_time]}"
  }
}
```
データ1はインデックス作成単位が1年なので、フィルタープラグインでタイムスタンプから`年`を取り出し、出力プラグインでインデックス名に設定しています。たとえば、2025年1月10日のデータは`data1-2025`というインデックスに登録されます。

インデックステンプレートは設定しません。データ1ではインデックスあたりシャード数を1にしますが、これはデフォルト設定と変わらないためです。

次に、データ4に対するLogstash設定ファイル（関連部分のみ）を示します。

```ruby:/etc/logstash/conf.d/data4.conf
filter {
  ruby {
    code => "event.set('[@metadata][local_time]',event.get('[@timestamp]').time.localtime.strftime('%Y-%m-%d'))"
  }
}

output {
  elasticsearch {
    index => "data4-%{[@metadata][local_time]}"
    template => "/etc/logstash/conf.d/template_data4.json"
    template_name => "template_data4"
    template_overwrite => true
  }
}
```
データ4はインデックス作成単位が1日なので、タイムスタンプから`年`、`月`、`日`を取り出してインデックス名に設定しています。たとえば、2025年1月10日のデータは`data4-2025-01-10`というインデックスに登録されます。

また、`/etc/logstash/conf.d/template_data4.json`というインデックステンプレートを使用するように設定しています。データ4ではインデックスあたりシャード数を3にするため、その設定を書いています。

```/etc/logstash/conf.d/template_data4.json
{
  "index_patterns": ["data4-*"],
  "template": {
    "settings": {
      "number_of_shards": 3,
      "number_of_replicas": 1
    }
  }
}
```

データ2, 3についても同様に設定します。以上で設定完了です。



## おまけ
もっと簡単に設定ファイルを作成できないものかと、ステップ1, 2をまとめてChatGPTによる設定を試みた結果を示します。

以下のプロンプトを送信します。

```:プロンプト
以下の環境に対するElasticsearchのシャード設定を作成してください。

# 環境
- ノード数: 3
- 1日あたりのデータサイズ
  - データ1: 10MB
  - データ2: 1GB
  - データ3: 10GB
  - データ4: 100GB

# シャードサイズのベストプラクティス
- シャード数の上限: 1ノードあたり1000
- シャードあたりのサイズが50GB以下になる範囲で、シャード数を最小化する

# 出力
- 各データのインデックス作成単位 (1日、1ヶ月、1年)
- 各データのインデックステンプレート設定 (Elasticsearch 7.8以降の形式)
- 1年分の合計シャード数
```

これに対し、以下のような回答が返って来ます。

```:ChatGPT回答
以下の環境に対するElasticsearchのシャード設定を作成します。

## 環境
- ノード数: 3
- 1日あたりのデータサイズ
  - データ1: 10MB
  - データ2: 1GB
  - データ3: 10GB
  - データ4: 100GB

## シャードサイズのベストプラクティス
- シャード数の上限: 1ノードあたり1000
- シャードあたりのサイズが50GB以下になる範囲で、シャード数を最小化する

## 各データのインデックス作成単位

### データ1
- データサイズ: 10MB/日
- インデックス作成単位: 1年 (10MB * 365日 = 3.65GB)
- シャード数: 1 (3.65GBは50GB以下)

### データ2
- データサイズ: 1GB/日
- インデックス作成単位: 1ヶ月 (1GB * 30日 = 30GB)
- シャード数: 1 (30GBは50GB以下)

### データ3
- データサイズ: 10GB/日
- インデックス作成単位: 1日 (10GB)
- シャード数: 1 (10GBは50GB以下)

### データ4
- データサイズ: 100GB/日
- インデックス作成単位: 1日 (100GB)
- シャード数: 3 (100GB / 3 ≈ 33.33GB, 50GB以下)

## 各データのインデックステンプレート設定 (Elasticsearch 7.8以降の形式)

### データ1
{
  "index_patterns": ["data1-*"],
  "template": {
    "settings": {
      "number_of_shards": 1,
      "number_of_replicas": 1
    }
  }
}

### データ2
{
  "index_patterns": ["data2-*"],
  "template": {
    "settings": {
      "number_of_shards": 1,
      "number_of_replicas": 1
    }
  }
}

### データ3
{
  "index_patterns": ["data3-*"],
  "template": {
    "settings": {
      "number_of_shards": 1,
      "number_of_replicas": 1
    }
  }
}

### データ4
{
  "index_patterns": ["data4-*"],
  "template": {
    "settings": {
      "number_of_shards": 3,
      "number_of_replicas": 1
    }
  }
}

## 1年分の合計シャード数

### データ1
- インデックス数: 1 (1年)
- シャード数: 1

### データ2
- インデックス数: 12 (1ヶ月ごとに1インデックス)
- シャード数: 12

### データ3
- インデックス数: 365 (1日ごとに1インデックス)
- シャード数: 365

### データ4
- インデックス数: 365 (1日ごとに1インデックス)
- シャード数: 365 * 3 = 1095

### 合計シャード数
- データ1: 1
- データ2: 12
- データ3: 365
- データ4: 1095
- 合計: 1 + 12 + 365 + 1095 = 1473

この設定により、1年分のデータで合計1473シャードとなります。ノード数が3であるため、1ノードあたりのシャード数は約491となり、上限の1000を超えません。
```

プロンプトについては、細かいアルゴリズムまで書くと、結局コードを書いているのと変わらなくなるため、簡潔に書ける範囲の指示に留めました。逆に、試しに追加した制約条件もあります。そういう意味で、元々のステップ1, 2とは異なります。

ChatGPT回答については、内容の吟味は必要ですが、ベースとしては十分使える印象を受けました。インデックステンプレートを出力してくれているため、自分でゼロから書かなくて良いのは便利だと思います。

## おわりに
本文で明確に触れなかった点について簡単に補足して、おわりとします。

### シャード数の上限

本文でシャード数の上限について触れました。デフォルトでは1ノードあたりの上限が1000に設定されていますが、変更することもできます。マシンのスペック次第では増やしても良さそうなものですが、結論としてはデフォルト値の1000を真の上限と捉えておくのが安全だと思います。

公式ドキュメントの関連する記述を確認します。まず、Elasticsearch 8.3より前のバージョンでは、ヒープ1GBあたりのシャード数を20までとする指針があったこと。また、8.3からはヒープの使用量が激減していることが記されています。

> **TIP**: ... A good rule-of-thumb is to ensure you keep the number of shards per node below 20 per GB heap it has configured ... **(Editor’s note: As of 8.3, we have drastically reduced the heap usage per shard, thus updating the rule of thumb in this blog. Please follow documentation for 8.3+ versions of Elasticsearch.)**
>
> 出典: https://www.elastic.co/blog/how-many-shards-should-i-have-in-my-elasticsearch-cluster

したがって、現在のバージョンでは、ヒープ1GBあたりのシャード数を20以上に設定しても問題なさそうですが、具体的な数値は見付かりませんでした。次に、デフォルト設定の意図に関する記述です。

> ### Cluster shard limits
> ...
> These limits are intended as a safety net to protect against runaway shard creation and are not a sizing recommendation. The exact number of shards your cluster can safely support depends on your hardware configuration and workload, and may be smaller than the default limits.
>
> We do not recommend increasing these limits beyond the defaults.
>
> 出典: https://www.elastic.co/guide/en/elasticsearch/reference/current/misc-cluster-settings.html#cluster-shard-limit

ハードウェア構成次第との記載もありますが、デフォルト値より大きくすることを推奨しないと明記されていることから、まずはそれに従っておくのが良いと思います。

### インデックスの管理

シャード数が上限に達しないようにインデックスの管理、より具体的には、古くなったインデックスの定期的な削除が必要になります。その方法について、この記事では省略します。

### インデックスとシャードのサイズ

簡単のため、インデックスとシャードのサイズを、元となるデータの単純な足し算や割り算で考えましたが、これは正確ではありません。実際のサイズは、[cat indices API](https://www.elastic.co/guide/en/elasticsearch/reference/current/cat-indices.html)と[cat shards API](https://www.elastic.co/guide/en/elasticsearch/reference/current/cat-nodes.html)を利用して確認することが出来ます。

### 環境
記事の作成中に使用した環境は以下の通りです。

- OS: Ubuntu 24.04 LTS
- Elasticsearch, Logstash, Kibana 8.15
    - Elasticsearchはノード3台のクラスター構成

各マシンのスペックは省略します。当初、RAM容量に応じてシャード数の上限を決定するようにしたかったのですが、結果的にそうならなかったためです。

ただし、Elasticスタックを本格的に運用するならば、扱うデータに応じて、マシンスペックやクラスタ台数を設計する必要があるかと思います。