Open12

ElasticSearch 概要をさらう

hassaku63hassaku63

ELK + Beats で Elastic Stack と呼ぶらしい(Beats は初見だった)

https://dev.classmethod.jp/articles/beats-entry-matome/

ES 自体はドキュメントベース、スキーマレスなDBのこと。可視化は Kibana のような別の製品が担うし、よく利用される「ログデータの集約」に関しても Logstash や fluentd など別の製品との組み合わせて実装する

Logstash

ログの収集・加工・転送を行う。

Input, Filter, Output という3つの主要概念で理解できる。

Input はデータソースからの取り込み+接続、Filter が加工、Output が送り先に関する設定(ES 以外にも対応)。

Beats

"Data Shipper" と呼ばれている。メトリックを収集して転送する。軽量であることが特徴らしい。

agent を入れることで利用する。どのようなメトリックを収集するかでインストールすべき agent も使わける。

  • Filebeat
  • Metricbeat
  • Packetbeat
  • Winlogbeat
  • Heartbeat
  • Auditbeat

サーバー本体に負荷をかけないため、収集したデータはそのまま転送するだけでそれ以上の仕事はしない。

hassaku63hassaku63

ES のユースケース

  • 全文検索
  • アクセスログ、アプリケーションログの分析
  • 分散システムの横断ログ検索
  • 異常検知
  • IoT デバイスのセンサーデータ可視化

分散システムの横断検索は、サーバーレス構成で取り入れたい気持ちもある。一方で Datadog でやれるのでは?とも思う

異常検知は machine laerning plugin という有償の追加プラグインを入れることで簡単に利用可能。

事例

-日経

  • Naver
  • GitHub
  • Netflix
  • CERN

など

hassaku63hassaku63

用語と概念

RDB で言うレコードは、ES では「ドキュメント」。ドキュメントは JSON 形式のオブジェクト

Uniq key 相当の概念もあるが、あまり表には出てこない。デフォルトでは自動採番の ID がドキュメントに付与される。

1つの Key-value の組は "フィールド" と言う。これは js で言うところのプロパティとほぼ同義(と思っている)

普通のJSON とは、使える value の型の概念が微妙に異なる。

例えば、文字列の型が2種類 (text, keyword) ある。

text は人間が想像する「文章」を持つフィールドとして、keyword はその value を完全一致で検索する用途に向いている。要するに適用できるインデックスの種類が異なるためクエリ要件も異なるという話。

Date 型があることも普通の JSON とは異なる。

使える型は以下参照。

https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html

「マルチフィールド」という特殊な型も存在する。これは例えば 1つのフィールドに text, keyword を同時に割り当てるといったことが可能。異なるインデックス(検索要件)を同時に満たす目的で利用可能。

Index

実態は RDB のインデックスとだいたい同じ。

ES の場合は MySQL における "Database" に近いニュアンスで使われることもあるし、「格納する」の意味を持つ動詞的な用法で利用されることもある。

Document Type

いわゆるテーブルのスキーマ定義に近い。フィールドの構成を宣言しておく。

Mapping

Document Type を実際に定義しているもの、JSON Schema っぽい形式を使う

{
  "properties": {
    "user_name": {"type": "text"},
    "message": {"type:" "text"}
  }
}

Cluster

ノードをグルーピングする概念。

※ES は複数ノードでクラスタ構成することを前提に設計されている

Shard

RDB のシャーディングと意義は同じ。性能のスケールを目的としている。

実体は Lucene のインデックスファイルを各ノードに分散配置している。

シャード数はインデックスを構成する時点で決めておく必要があり、あとから増加できない。

シャードはノードと 1:1 対応する概念ではないので注意。

Replica

シャードを複製する仕組み。イメージや利用するモチベーションは RDS の Read Replica に近い。

オリジンにあたるシャードはプライマリシャード、レプリカはレプリカシャードと呼ぶ。

データ更新はプライマリの反映ありきで逐次レプリカにも反映させる。

可用性目的でシャードを有効に使うためには、割り当てたプライマリとレプリカが別々のノードに分散している必要がある。これは ES が暗黙的にやってくれる。また、ノード障害でプライマリがなくなった場合はレプリカから1つをプライマリに昇格する動きも備えている。

hassaku63hassaku63

疑問点

書籍の書きっぷりでは、Redshift のように Query を取りまとめる役目を持つマスターノード的な構成がいないような記述に見える。個人的にそれは疑わしいので、調べて裏取りしたい

回答

Ingest がその役割を持っている

hassaku63hassaku63

疑問点

RDB (MySQL) の database, table の概念と、ES における Index, DocumentType の概念は綺麗に対応しないのではないか?

回答

Yes. どう違いが出るのかはまだ說明できないが、クエリの節を見ていると多少違いがわかる

Index, DucumentType はいずれも横断検索が可能。

Index も DucumentType も横断検索が可能。Index に関してはパーティショニングしたテーブルを横断で検索するような使い方が想定できるし、DocumentType に関しても1つないしは複数を対象としたクエリが投げられる。このへんが MySQL や PostgreSQL の概念では対応するものがないので 1:1 のきれいな対応があるとは考えない方がよい

hassaku63hassaku63

システム構成

ノードの種別

  • Master (master-eligible)
  • Data
  • Ingest
  • Coordinating

master node

最低1台必要。

  • 各ノードへの heartbeat を行い生死判定
  • クラスタ内部のメタデータ保持(これらを各ノードに伝搬する役目も持つ)
    • ノード構成
    • Index, mapping の設定
    • Shard の割り当て、ステータス

master が落ちた場合に昇格できる候補ノードを設定可能で、"master-eligible" 属性が有効なノードがこれに該当。デフォルトではすべてのノードが master-eligible である。

master-eligible 専用のノードを構成することも可能。設定例は以下

# elasticsearch.yml
node.master: true
node.data: flase
node.ingest: false

data node

  • データ格納
  • クエリ応答

内部的に (Lucene) インデックスファイルをマージしてたりもする

クエリをシャードにルーティングするのも Data ノードの役割。

データの格納は「どのシャードに入れるか」の判断が必要で、それは routing と呼ばれる

クエリの場合は各シャードへのクエリ (scatter) と集約 (gather) という2つのフェーズを通してクライアントに結果を応答する。

Ingest

ES 5.0 から登場した。

ES ノードの内部でデータ変換・加工を実行できる。Logstash でやっていた機能を一部代替できる。

"pipeline" と呼ばれる処理のフロー定義をやることで、クライアントから送られたデータの前処理が可能になる。

デフォルトでは、すべてのノードは Ingest の役割を持っている。

Coodinating

クライアントからのリクエストをハンドリングする役目。

データ格納時の routing, クエリ時の scatter, gather をやる。

hassaku63hassaku63

クラスタの構成について

まず、split-brain 問題を避けるために、Master-eligible は3ノード以上の奇数で構成すること。ノード数以外にもやることがある

  • master-eligible を3以上の奇数にする
  • elasticsearch.yml の discovery.zen.minimum_master_nodes の数を、master-eligible ノード数の 過半数 に設定する
\lceil nMasterEligibleNodes/2 + 1 \rceil

これで、誰かが「勝手に master を名乗ってしまう」可能性がなくなる。生きているノードが過半数に満たない場合は誰も master 昇格ができない。

クラスタへの参加

ノードの起動時に設定ファイルの discovery.zen.ping.unicast.hosts を読んで、そこに定義されたノード郡に接続を試みる。

cluster.name: my-cluster
discovery.zen.discovery.unicast.hosts: ["master1", "master2", "master3"]
discovery.zen.minimum_master_nodes: 2

疎通するとクラスタ参加は完了になる。以降は master が heartbests して unhealthy なら障害により離脱したとみなされる。

このような discovery の仕組みは ES が組み込みで備えている。これは "Zen discovery" という名前がある。

hassaku63hassaku63

疑問点

master 選出のアルゴリズム、split-brain 問題が回避できたとしてどうやって決定できるのか?

実行ノードに関係なく一意に定まるアルゴリズム(ノードid のラウンドロビンなど)なのか、それとも分散処理を考慮した調整アルゴリズムがあるのか。

hassaku63hassaku63

クラスタ構成の続き

シャード分割

シャードは可用性のモチベーションがあるので、複数ノードにアクセスを分散したい狙いがあった。

よって、ノードとシャードの数は近い値であることが望ましい。

1ノードに多数のシャードが配置されてしまうのは負荷分散や可用性の観点を満たせないケースがあり、設定値としてバランスが良くない (over location)。しかし、ノード数は後から追加可能なのでリカバリは可能な状況といえる。

ノード数に対してシャードが少なすぎる場合 (under location) は、プロビジョニングしたノードの性能を使い切れない。シャードがあとから追加できない制約上、ノードのリソースを使い切れない状況は変えられない。

underlocation の状況は後からリカバリができないので、ノードを増やす計画があるならばそれを見越してシャード数を決めておくのがひとつの観点になる。

また、格納するデータの総量が少ない想定であればシャード数は少なくても良い。

レプリカ

Read/ Write の観点がある。

Read に関してはクエリ偏重なユースケースならレプリカを増やす、というもの。レプリカは後で追加可能なので事前見積りでさほど神経質になる必要はない。

Write に関しては、バルク・バッチ的な書き込みがある場合に複製処理が重たくなる懸念がある。運用上許容できるのであれば、一時的にレプリカを 0 にしておく、という運用も検討余地がある、らしい。

※投入時の障害リスクもある。それに、レプリカ数を元に戻したタイミングに書き込みの負荷を先延ばししているだけなのでその「先延ばし」が自らの運用シーンの想定に即しているのかどうかは考えて判断する必要がある。無理なら一度に投入するデータをチャンクに分けるなど、投入側で調整することも検討すると良い、はず

hassaku63hassaku63

REST API の節はおよそ想像がつく話だったので省略。


導入

リソース要件

メモリ

JVM の上で動作する。

データアクセスでキャッシュを多用するのでメモリは強めに必要。キャッシュは JVM のヒープ領域を使うので、JVM の方も設定がいる。

JVM ヒープの目安は OS が認識しているメモリの半分くらいが目安上限となる。

残りは OS レイヤー(ファイルシステムのキャッシュ)に使う。こちらは Lucene インデックスファイルの都合。JVM と OS どちらのレイヤーでもメモリを必要とする。

そして、JVM のヒープは 32GB を超えると性能上の問題を引き起こす、という情報がある。ES のドキュメントに記述があるのでそちらを参照

ディスク

強めの IO を要求する。ES 自身がデータの冗長化の仕組みを持っているので、仮に自前で構築するのであれば RAID1 のようなストレージレイヤーの冗長化はやる意義が乏しい。

N/W

シャード複製やリバランスを行うときに影響が大きい。ノードの追加・削除(障害によるものも含む)が発生すると一時的に大量のデータが N/W を通ることに加え、生死確認のパケットも飛んでいる。データ転送がネックになったり、生死確認が遅延することで生死判定の結果に予期しない影響をもたらす可能性もある(遅延によって偽陽性の障害と判断され、シャードのリバランスを誘発する)。

このような理由で、N/W 帯域の性能以外にも、レイテンシが大きい分散構成はあまり好ましくないと言える

hassaku63hassaku63

※ パスや HTTP メソッドでだいたい推測可能、メモする意義も薄いのでクエリ結果に関わるパラメータじゃない部分は省略する

_source エンドポイント

通常のレスポンスはドキュメント本体に加えて、どこから取得したかなど付加情報も含まれている。"_source" エンドポイントをつけるとドキュメント本体だけを戻すようにレスポンスを変えられる。

更新

全体を置き換える場合は既存のドキュメントに対して PUT を出す。

ドキュメントの一部を更新したい場合は _update のエンドポイントに対して POST を投げる必要があり、インタフェースは異なる。

ただし、内部的にはどちらも delete -> create -> indexing の流れを踏むことになる。

インデックスやドキュメントタイプの事前定義の必要性

これらの定義は、やらなくても良い。ES が自動でやってくれる。しかし、明示的にやっておきたい場面もある。

インデックスはデフォルト値となるシャード・レプリカ設定と異なる設定値を利用したい場合(Read/Write の要件が他と異なるケース)。

ドキュメントタイプは、事前にデータの形式がわかっていて、ES に型の推論処理をスキップさせることができる。

インデックステンプレート

ES はドキュメントを登録する時点で動的にインデックスやドキュメントタイプを自動生成できる。これを利用して、パーティショニングに近いユースケースを実現できる。

例えば、時系列のデータを月次・日次などでパーティショニングしたいケースが該当する。

インデックステンプレートは、「インデックスの名前」を条件としてマッピングを適用できる機能となる。

インデックステンプレート側では "my-index-*" のような条件を指定しておき、データを投げる側ではドキュメント自身の日付からインデックス名を導出して、明示的に "my-index-2021-01" のインデックスに対してデータ登録するようにリクエストを組み立てる、といった使い方になる。

検索

/<index>/<doc-type>/_search エンドポイントに対して投げる。

Request Body にクエリを表現する JSON を付ける。

{
    "query": {
        "match": {
            "message": "Elasticsearch"
        }
    }
}

レスポンスの一例は以下

{
    "took": 10,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 1,
        "max_score": 0.25,
        "hits": [
            {
                "_index": "my_index",
                "_type": "my_type",
                "_id": "1",
                "_score": 0.25,
                "_source": {
                    "user_name": "John Smith",
                    "date": "2017-10-15T15:09:45",
                    "message": "Hello Elasticsearch world."
                }
            }
        ]

    }
}

詳しいクエリの話は次で