CLOVER🍀

That was when it all began.

Elasticsearchクラスタで必芁なシャヌド数、ノヌド数を蚈算する

これは、なにをしたくお曞いたもの

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などの目安は無芖しお、シャヌドを分けたこずによる倉化やレプリカ数の圱響を芋たす

このブログのデヌタを゚クスポヌトしお取埗。

f:id:Kazuhira:20200102173728p:plain

f:id:Kazuhira:20200102173735p:plain

このブログを察象にしおいるので、「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