これは、なにをしたくて書いたもの?
Elasticsearchクラスタのシャード数やノード数を算出する時の考え方についていろいろ調べたので、メモをしておこうかなと。
Elasticsearchクラスタでのシャード数とノード数を計算する
基本となるのは、以下のブログエントリでしょう。
How many shards should I have in my Elasticsearch cluster? | Elastic Blog
こちらに、シャードひとつあたりのサイズについての考え方が書かれています。
ヒント: 小さなシャードは小さなセグメントとなり、結果としてオーバーヘッドが増えます。そのため、平均シャードサイズは最小で数GB、最大で数十GBに保つようにしましょう。時間ベースのデータを使用するケースでは、シャードサイズを20GBから40GBにするのが一般的です。
20〜40GBにするのが、一般的だと書かれていますね。AWSのブログエントリを見ると、30GBが目安とも書かれています。
Amazon Elasticsearch Service をはじめよう: シャード数の算出方法 | Amazon Web Services ブログ
ここでは、30GBを目安に考えましょう。
また、ひとつのElasticsearchノードが、どのくらいシャードを持てるかについては、以下のようにヒープサイズで算出するようですね。
ヒント: ノードに保持できるシャード数は、利用できるヒープ量に比例しますが、Elasticsearchによって強制される固定の上限はありません。経験則では、ノードごとのシャード数は構成したヒープのGBあたり20未満に維持することが良いと言えます。したがって30GBのヒープでは最大600シャードとなりますが、この上限よりも大幅に下回る数にするほうがより適切です。
ヒープ1Gあたり20シャード、ということになります。
とすると、インデックスのサイズとレプリカ数、Elasticsearchのヒープサイズが決まれば、シャード数とElasticsearchのノード数が算出できることに
なります。
たとえば、以下の条件で考えます。
- Elasticsearchに保持するインデックスは1種類
- ひとつのインデックスのサイズが150GB
- レプリカ数が1
- Elasticsearchのヒープサイズが15GB
- 1日単位に同じ種類のインデックスを作成して、最大3ヶ月(90日)分保持する
- LogstashやBeatsで日単位のインデックスを作成するイメージ
- 1日あたり、150GBのインデックスができるものとする
計算すると
- インデックスあたりのシャード数: 150GB / 30GB = 5シャード(余りが出た場合は、1シャード分切り上げ)
- クラスタ内のシャード数: 5シャード × 2(プライマリー+レプリカ) × 90(保持日数がインデックス数になる) = 900シャード
- 必要なElasticsearchノード数: 900シャード / 15 × 20(ヒープサイズ × 1Gヒープあたり20シャード) = 3ノード(余りが出た場合は、1ノード分切り上げ)
という感じでしょうか。さらに、複数の種類のインデックスを持つなら、シャード数の部分に追加計算していく感じですね。
レプリカが計算から落ちやすい気がしないでもないですが、レプリカシャードも含めます。レプリカシャードは、検索にも使われるようですし、
プライマリーシャードが更新された後に合わせてレプリカシャードも更新されますからね。ふつうに使われます、と。
Elasticsearchでは、各クエリはシャードごとに単一のスレッドで実行されます。ただし、同一のシャードに複数のクエリおよび集約が実行できるのと同様に、複数のシャードを並行して処理することが可能です。
インデックスの実データ量を見る
それはそうと、インデックスのサイズはどうやって測る?ということになると思いますが、データを入れてcat APIを使うことになるのでは
ないでしょうか。
cat indices API | Elasticsearch Reference [7.5] | Elastic
cat shards API | Elasticsearch Reference [7.5] | Elastic
実例がないとなんともなので、適当なサイズ(簡単に終わらせたいのでGBはいかず、でもMBは欲しい?くらいの…)のネタがないかどうか
考えた結果、このブログのデータを使うことにしました。
※ここでは、ここまでに書いてきた1シャードあたり30GBなどの目安は無視して、シャードを分けたことによる変化やレプリカ数の影響を見ます
このブログのデータをエクスポートして取得。


このブログを対象にしているので、「kazuhira-r.hatenablog.com.export.txt」というファイルが取得できます。サイズは32MBほどですが、まあいいでしょう。
$ ll -h kazuhira-r.hatenablog.com.export.txt -rw-rw-r-- 1 xxxxx xxxxx 32M 1月 2 15:48 kazuhira-r.hatenablog.com.export.txt
中には、ドラフト状態のものも入っていますが…。
$ head -n 20 kazuhira-r.hatenablog.com.export.txt AUTHOR: Kazuhira TITLE: Elasticsearchクラスタのシャード数を計算する BASENAME: 2020/01/02/011901 STATUS: Draft ALLOW COMMENTS: 1 CONVERT BREAKS: 0 DATE: 01/02/2020 00:58:10 CATEGORY: Elasticsearch ----- BODY: <p>Elasticsearchのシャード数を算出する時の考え方を、メモをしておこうかなと。</p> <p>基本となるのは、以下のブログエントリでしょう。</p> <p><a href="https://www.elastic.co/jp/blog/how-many-shards-should-i-have-in-my-elasticsearch-cluster">How many shards should I have in my Elasticsearch cluster? | Elastic Blog</a></p> <p>こちらに、シャードひとつあたりのサイズについての考え方が書かれています。</p> <blockquote><p>ヒント: 小さなシャードは小さなセグメントとなり、結果としてオーバーヘッドが増えます。そのため、平均シャードサイズは最小で数GB、最大で数十GBに保つようにしましょう。時間ベースのデータを使用するケースでは、シャードサイズを20GBから40GBにするのが一般的です。</p></blockquote>
形式は、Movable Type。
ここから、インデックスに
- タイトル(TITLE)
- カテゴリー(CATEGORY)
- 投稿日時(DATE)
- コンテンツ(BODYからHTMLのテキストだけを抜き出したもの)
- ステータス(STATUS)
を登録し、idとしてはBASENAMEを「/」抜きで使うものとします。あと、コメントは全部読み飛ばします。
そういうちょっとしたプログラムをPythonで書いてみましょう。HTMLからテキストを取得するのにBeautifulSoup4、Elasticsearchへの
データ登録に、PythonのElasticsearchクライアントを使用します。
$ pip3 install beautifulsoup4 elasticsearch
バージョン。
$ pip3 freeze beautifulsoup4==4.8.2 elasticsearch==7.1.0 pkg-resources==0.0.0 soupsieve==1.9.5 urllib3==1.25.7 $ python3 -V Python 3.6.9
で、適当にスクリプトを作ります。「mv_export_to_es.py」というファイル名で、引数にエクスポートしたMovable Typeのファイルパスを渡すように
します。作成したスクリプトはこのエントリの本筋ではないので、最後に載せますね。
このスクリプトでは、データを登録するインデックス名を「blog」として、バルク処理で100件ずつ登録します。
接続先のElasticsearchは、192.168.33.11〜13でクラスタ構成されているものとします。
情報はこちら。
$ curl localhost:9200/_cat/nodes?v
ip heap.percent ram.percent cpu load_1m load_5m load_15m node.role master name
192.168.33.12 9 96 0 0.00 0.01 0.00 dilm - node-2
192.168.33.13 7 96 0 0.02 0.02 0.02 dilm - node-3
192.168.33.11 8 95 0 0.00 0.00 0.02 dilm * node-1
$ curl localhost:9200
{
"name" : "node-1",
"cluster_name" : "elasticsearch",
"cluster_uuid" : "0PtgLGF_Q2-IWYMVUtcV4Q",
"version" : {
"number" : "7.5.1",
"build_flavor" : "default",
"build_type" : "deb",
"build_hash" : "3ae9ac9a93c95bd0cdc054951cf95d88e1e18d96",
"build_date" : "2019-12-16T22:57:37.835892Z",
"build_snapshot" : false,
"lucene_version" : "8.3.0",
"minimum_wire_compatibility_version" : "6.8.0",
"minimum_index_compatibility_version" : "6.0.0-beta1"
},
"tagline" : "You Know, for Search"
}
$ java --version
openjdk 11.0.5 2019-10-15
OpenJDK Runtime Environment (build 11.0.5+10-post-Ubuntu-0ubuntu1.118.04)
OpenJDK 64-Bit Server VM (build 11.0.5+10-post-Ubuntu-0ubuntu1.118.04, mixed mode, sharing)
とりあえず、なにも考えずにデータを登録します。
$ python3 mv_export_to_es.py kazuhira-r.hatenablog.com.export.txt
1243件のデータが入りました。
$ curl localhost:9200/blog/_count?pretty
{
"count" : 1243,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
}
}
インデックスの情報を見てみます。
$ curl localhost:9200/_cat/indices?v health status index uuid pri rep docs.count docs.deleted store.size pri.store.size green open blog jTUukj5PQ-eZoMo3GSVO8w 1 1 1243 0 23.6mb 11.8mb
データサイズは23.6MBです。でも、よく見ると「pri.store.size」は11.8MBと表示されていますね。
一方でシャードのサイズを見てみます。
$ curl localhost:9200/_cat/shards?v index shard prirep state docs store ip node blog 0 p STARTED 1243 11.8mb 192.168.33.13 node-3 blog 0 r STARTED 1243 11.7mb 192.168.33.11 node-1
2つシャードがあり、11.7〜8MBですね。
インデックスはデフォルトの設定で作成したので、シャード数1、レプリカ数1になっています。
$ curl localhost:9200/blog/_settings?pretty
{
"blog" : {
"settings" : {
"index" : {
"creation_date" : "1577954953969",
"number_of_shards" : "1",
"number_of_replicas" : "1",
"uuid" : "jTUukj5PQ-eZoMo3GSVO8w",
"version" : {
"created" : "7050199"
},
"provided_name" : "blog"
}
}
}
}
というわけで、インデックスの方に出ているデータサイズ(store.size)は、プライマリーシャードとレプリカシャードの値を
足したものですね。
計算のベースとしては、プライマリーシャードの合計値を見て必要となるデータサイズを見てみればよいでしょう。
別パターンとして、シャード数とレプリカ数を変更したものも試してみましょう。
今のインデックスを1度削除。
$ curl -XDELETE localhost:9200/blog
{"acknowledged":true}
シャード数9、レプリカ数2でインデックスを作成。
$ curl -XPUT "localhost:9200/blog" -H 'Content-Type: application/json' -d '{ "settings" : { "index" : { "number_of_shards" : 9, "number_of_replicas" : 2 } } }'
{"acknowledged":true,"shards_acknowledged":true,"index":"blog"}
再度データ登録。
$ python3 mv_export_to_es.py kazuhira-r.hatenablog.com.export.txt
確認。
$ curl localhost:9200/_cat/indices?v health status index uuid pri rep docs.count docs.deleted store.size pri.store.size green open blog 4iAlQWUURxypKkNqmilbsg 9 2 1243 0 41.1mb 14mb
プライマリーシャードの容量が、1シャードの時からちょっと増えていますね。トータルのサイズとしても、レプリカ数が2つになったので、
プライマリーシャードの約3倍になっています。
シャードの状態は、こんな感じです。
$ curl localhost:9200/_cat/shards?v index shard prirep state docs store ip node blog 3 p STARTED 143 1.6mb 192.168.33.13 node-3 blog 3 r STARTED 143 1.5mb 192.168.33.11 node-1 blog 3 r STARTED 143 1.6mb 192.168.33.12 node-2 blog 4 r STARTED 124 1.4mb 192.168.33.13 node-3 blog 4 p STARTED 124 1.4mb 192.168.33.11 node-1 blog 4 r STARTED 124 1.4mb 192.168.33.12 node-2 blog 6 p STARTED 137 1.4mb 192.168.33.13 node-3 blog 6 r STARTED 137 1.3mb 192.168.33.11 node-1 blog 6 r STARTED 137 1.3mb 192.168.33.12 node-2 blog 7 r STARTED 163 1.7mb 192.168.33.13 node-3 blog 7 p STARTED 163 1.9mb 192.168.33.11 node-1 blog 7 r STARTED 163 1.7mb 192.168.33.12 node-2 blog 8 r STARTED 126 1.3mb 192.168.33.13 node-3 blog 8 r STARTED 126 1.3mb 192.168.33.11 node-1 blog 8 p STARTED 126 1.5mb 192.168.33.12 node-2 blog 2 r STARTED 129 1.3mb 192.168.33.13 node-3 blog 2 r STARTED 129 1.2mb 192.168.33.11 node-1 blog 2 p STARTED 129 1.3mb 192.168.33.12 node-2 blog 1 r STARTED 141 1.6mb 192.168.33.13 node-3 blog 1 p STARTED 141 1.6mb 192.168.33.11 node-1 blog 1 r STARTED 141 1.4mb 192.168.33.12 node-2 blog 5 r STARTED 129 1.5mb 192.168.33.13 node-3 blog 5 r STARTED 129 1.5mb 192.168.33.11 node-1 blog 5 p STARTED 129 1.5mb 192.168.33.12 node-2 blog 0 p STARTED 151 1.6mb 192.168.33.13 node-3 blog 0 r STARTED 151 1.6mb 192.168.33.11 node-1 blog 0 r STARTED 151 1.5mb 192.168.33.12 node-2
実際に計算する時は1シャードにデータを入れて基準となるデータサイズを見つつ、実際にシャード分割してどれくらい余剰に
増えるかを確認してみるといった感じになるんでしょうね。
なかなかcat APIに結果が反映されない時は、Refresh APIを使って、インデックスをリフレッシュするとよいでしょう。
$ curl -XPOST localhost:9200/[インデックス名]/_refresh $ curl -XPOST localhost:9200/_refresh
オマケ
最後に、今回作成したスクリプトを載せて終わりにします。 mv_export_to_es.py
from bs4 import BeautifulSoup from elasticsearch import Elasticsearch from elasticsearch import helpers import re import sys elasticsearch_hosts = ["192.168.33.11", "192.168.33.12", "192.168.33.13"] elasticsearch_urls = [ f"http://{host}:9200" for host in elasticsearch_hosts ] es = Elasticsearch(elasticsearch_urls) index_name = "blog" bulk_size = 100 mv_export_file = sys.argv[1:][0] body_separator = "-----" entry_separator = "--------" with open(mv_export_file, "rt", encoding = "utf-8") as file: docs = [] while True: line = file.readline() # AUTHOR if line == "": break title = re.match("TITLE:\s+(.+)", file.readline().strip()).group(1) # TITLE basename = re.match("BASENAME:\s+(.+)", file.readline()).group(1).replace("/", "") # BASENAME status = re.match("STATUS:\s(.+)", file.readline().strip()).group(1) # STATUS file.readline() # ALLOW COMMENT file.readline() # CONVERT BREAKS m = re.match("DATE:\s+(\d\d)/(\d\d)/(\d\d\d\d) (\d\d:\d\d:\d\d)", file.readline()) # DATE datetime = f"{m.group(3)}-{m.group(1)}-{m.group(2)} {m.group(4)}" categories = [] while True: line = file.readline() if line.startswith("CATEGORY:"): category = re.match("CATEGORY:\s+(.+)", line).group(1) categories.append(category) else: # BODY separator(------) break file.readline() # BODY body = "" while True: line = file.readline() if line.strip() == body_separator: maybe_contents = line line = file.readline() if line.strip() == entry_separator: break elif line.strip() == "COMMENT:": # skip comment while True: line = file.readline() if line.strip() == entry_separator: break break else: body += maybe_contents body += line else: body += line soup = BeautifulSoup(body, "html.parser") body_text = soup.get_text() doc = { "_index": index_name, "_id": basename, "_source": { "title": title, "datetime": datetime, "categories": categories, "body": body_text, "status": status } } docs.append(doc) if len(docs) >= bulk_size: helpers.bulk(es, docs) docs = [] helpers.bulk(es, docs) # last