EC2
Elasticsearch
Docker
ECS

EC2上で大容量データを扱うElasticsearchクラスターを構築する


背景

日あたり1億レコード、2千変数を超える(セミ)スキーマレスなデータをニアリアルタイムで投入・集計したいというニーズがあったときに、EC2上でどのように構築するのがいいか?

X-Packを使いたかったのでElasticsearch Serviceが使えず、Security Groupやチューニング余地、コスト等々の都合でElastic社のマネージドサービスも選択せず、なおかつX-Packのライセンス料を最小にしたかったのでノード数も少なくしたかった。


概要


  • Elasticsearch 6.4移行を想定(5.x でもほぼ共通だがノード間通信の暗号化が必須か否か、G1GCが使えるか否か、が違う)

  • EC2インスタンスはメモリーが多いもの、256GB以上を選ぶ

  • EBSではなく、SSD(所謂エフェメラルストレージ/揮発性のNVMe)が付いてくるインスタンス、ストレージが2台以上、できれば4台くらいあるものを選択し、RAIDでストライピングする

  • ノード間の通信を暗号化するのに証明書を入れるが、手間を少なくするために一度Dockerイメージを作ってECRに入れる

  • ノードあたりのヒープサイズは128GBとか、「JVMの32GB制限」を気にせず、G1GCを有効化することで大容量メモリーと性能をバランスする

  • データノードの他に、独立したコーディネーターノードを建てる


構築手順


前提


  • インスタンスタイプはi3.8xlarge を想定


    • m5d、c5d、r5d等のNitro世代のディスク付きでも同じだが、ルートのEBSがNVMe扱いなのでデバイスの番号がずれるのでRAID構成時に注意する



  • OSは初期状態のAmazon Linux


    • ElasticsearchそのものはDocker上で動かすのであまり拘らなくて良いかもしれない




ホストマシンのチューニング

基本的な手順は公式ドキュメントと変わらないが、RAID構成をする


アップデートしてDockerをインストール

OS等の更新を適用し、Dockerをインストールしておく

yum -y update

yum -y install docker


limits.confに設定を追加

ファイルディスクリプタの上限を緩和、メモリーロックを無制限にする。

シェルで以下のように実行すれば追記される(くれぐれもlimits.confにコピペしない)

bash -c 'echo root soft nofile 1048576 >> /etc/security/limits.conf'

bash -c 'echo root hard nofile 1048576 >> /etc/security/limits.conf'
bash -c 'echo * soft nofile 1048576 >> /etc/security/limits.conf'
bash -c 'echo * hard nofile 1048576 >> /etc/security/limits.conf'
bash -c 'echo * soft memlock unlimited >> /etc/security/limits.conf'
bash -c 'echo * hard memlock unlimited >> /etc/security/limits.conf'


sysctl.confに設定を追加

ネットワークとスワップの設定をする。こちらもシェルに貼り付ければ追記できる。

bash -c 'echo net.ipv4.tcp_tw_reuse = 1 >> /etc/sysctl.conf'

bash -c 'echo net.ipv4.tcp_fin_timeout = 30 >> /etc/sysctl.conf'
bash -c 'echo net.ipv4.ip_local_port_range = 16384 65535 >> /etc/sysctl.conf'
bash -c 'echo vm.max_map_count = 262144 >> /etc/sysctl.conf'
bash -c 'echo vm.swappiness = 1 >> /etc/sysctl.conf'


/etc/rc.local に追記

/etc/rc.local を開いて以下を追記する。

ホスト名はAWSのあまり知られていない(?) 169.254.169.254 をたたいてインスタンスIDを得る。

RAIDの構成をここに書くのは、エフェメラルストレージだとシャットダウン後にデータが消えてRAIDの構成も失われるので、RAIDが構成されていなかったら構成しなおすため。

この例では i3.8xlarge を想定しているのでRAIDを組む対象デバイスが /dev/nvme0 から /dev/nvme3 の4台になっている。Nitro世代だと /dev/nvme1 から始まる。

/data をElasticsearchのデータディレクトリにするのでこうしているが、パーミッションはちゃんと考えた方がよさそう。

# Set Hostname

hostname "$(curl -s http://169.254.169.254/latest/meta-data/instance-id |sed 's/\./-/g')"

# Init RAID
if [ ! -d /data ]
then
mkdir /data
fi

if [ ! -d /dev/md0 ]
then

# Initialize the RAID with four NVMe drives
mdadm --create --verbose --level=0 /dev/md0 --name=DATA --raid-devices=4 /dev/nvme0n1 /dev/nvme1n1 /dev/nvme2n1 /dev/nvme3n1
mkfs.ext4 /dev/md0
mdadm --detail --scan | tee -a /etc/mdadm.conf

# Just in case, Update the kernel option
dracut -H -f /boot/initramfs-$(uname -r).img $(uname -r)

# Mount the RAID
mount -a

fi

chmod 777 /data


fstabに追記

以下をシェルで実行して、構成したRAIDを起動時に /data にマウントするようにする。

bash -c 'echo /dev/md0 /data ext4 defaults,nofail,noatime,discard 0 2 >> /etc/fstab'


ホストを再起動

reboot


Elasticsearchコンテナを起動する


Dockerfileを作る

基本的にはElasticの公式イメージをベースとし、必要なプラグインを入れたりする。最近はX-Packは最初から入ってたりするので、あまり必要になるケースは多くないかもしれない。

ノード間の通信を暗号化することが必須になっているので、p12ファイルを生成してイメージに組み込んだ。(くれぐれもパブリックなリポジトリに入れないこと)

FROM docker.elastic.co/elasticsearch/elasticsearch:6.6.2

#### 形態素解析のプラグイン等インストールする場合はここに列挙する
RUN \
elasticsearch-plugin install --batch analysis-icu && \
elasticsearch-plugin install --batch analysis-kuromoji

#### ノード間の通信の暗号化に使うp12ファイルを作っておいて、よしなに入れてやる
RUN mkdir /usr/share/elasticsearch/config/certs
ADD elastic-certificates.p12 /usr/share/elasticsearch/config/certs/elastic-certificates.p12
ADD elastic-stack-ca.p12 /usr/share/elasticsearch/config/certs/elastic-stack-ca.p12
RUN chown -R elasticsearch /usr/share/elasticsearch/config/certs
RUN chgrp -R root /usr/share/elasticsearch/config/certs
RUN chmod o-rx /usr/share/elasticsearch/config/certs
RUN chmod 640 /usr/share/elasticsearch/config/certs/elastic-stack-ca.p12
RUN chmod 640 /usr/share/elasticsearch/config/certs/elastic-certificates.p12


ビルドする

ビルドする。プラグインや証明書が入ったイメージができる。

ECRに入れやすいタグを付けておく。

docker build -t 0000000000.dkr.ecr.ap-northeast-1.amazonaws.com/es/high-performance-es:6.6.2 .


ECRに入れて置く

ECRにログインして、それからプッシュ。

aws ecr get-login --no-include-email --profile hoge | sudo /bin/bash

docker push 0000000000.dkr.ecr.ap-northeast-1.amazonaws.com/es/high-performance-es:6.6.2


Elasticsearchを起動する

個々のノードで以下のようにElasticsearchコンテナを立ち上げる。

- XmsXmx はホストのメモリーの50%くらいにする。JVMの32GB(アドレス空間が32ビットか64ビットか問題)は気にしない。EC2だと31GBくらいで64ビットになった気がするけどマシンの性能が高いしG1GCを使うようにしてからなおさら気にならない。

- discovery.zen.ping.unicast.hosts にクラスターを構成するマシンのIPアドレスを列挙する

- データを格納するディレクトリは、コンテナ内で場所は変更できなかった

- ELASTIC_PASSWORD 環境変数で、デフォルトユーザーのパスワードを指定する


データノード(データを格納したり、集計処理をする)


  • データノードは node.masternode.data=truenode.ingest が全て true

docker pull 0000000000.dkr.ecr.ap-northeast-1.amazonaws.com/es/high-performance-es:6.6.2

docker run -d --net=host -v /data:/usr/share/elasticsearch/data \
-e "node.master=true" \
-e "node.data=true" \
-e "node.ingest=true" \
-e "ELASTIC_PASSWORD=P4ssW0rd" \
-e "node.name=$(curl -s http://169.254.169.254/latest/meta-data/instance-id |sed 's/\./-/g')" \
-e "ES_JAVA_OPTS=-Xms128G -Xmx128G -XX:-UseConcMarkSweepGC -XX:-UseCMSInitiatingOccupancyOnly -XX:+UseG1GC -XX:InitiatingHeapOccupancyPercent=75" \
-e "network.host=0.0.0.0" \
-e "bootstrap.memory_lock=true" \
-e "discovery.zen.ping.unicast.hosts=3.0.0.1,3.0.0.2,3.0.0.3" \
-e "discovery.zen.minimum_master_nodes=1" \
-e "xpack.security.transport.ssl.enabled=true" \
-e "xpack.security.transport.ssl.verification_mode=certificate" \
-e "xpack.security.transport.ssl.keystore.path=/usr/share/elasticsearch/config/certs/elastic-stack-ca.p12" \
-e "xpack.security.transport.ssl.truststore.path=/usr/share/elasticsearch/config/certs/elastic-certificates.p12" \
-e "thread_pool.bulk.queue_size=1000" \
-e "cluster.name=high-performance-es" \
--ulimit nofile=524288:524288 --ulimit memlock=-1:-1 \
--privileged \
--name elasticsearch \
0000000000.dkr.ecr.ap-northeast-1.amazonaws.com/es/high-performance-es:6.6.2


コーディネーターノード(入出力の窓口のみを担う)


  • コーディネーターはクラスターのエンドポイントの役割を担い、入出力をデータノードに割り振る役割


  • node.masternode.data=truenode.ingest が全て false

  • インスタンスサイズは小さくて大丈夫だが、EC2で小さいインスタンスを使う場合、ネットワーク帯域が制限されるか、クレジットの枯渇でパフォーマンスが落ちることがあるので、そこそこ大きめのインスタンスがオススメ

  • コーディネーターノードはゆとりがあるので、ここでKibanaを動かすといい

docker pull 0000000000.dkr.ecr.ap-northeast-1.amazonaws.com/es/high-performance-es:6.6.2

docker run -d --net=host -v /data:/usr/share/elasticsearch/data \
-e "node.master=false" \
-e "node.data=false" \
-e "node.ingest=false" \
-e "node.name=Coordinator" \
-e "ELASTIC_PASSWORD=P4ssW0rd" \
-e "ES_JAVA_OPTS=-Xms30G -Xmx30G -XX:-UseConcMarkSweepGC -XX:-UseCMSInitiatingOccupancyOnly -XX:+UseG1GC -XX:InitiatingHeapOccupancyPercent=75" \
-e "network.host=0.0.0.0" \
-e "bootstrap.memory_lock=true" \
-e "discovery.zen.ping.unicast.hosts=3.0.0.1,3.0.0.2,3.0.0.3" \
-e "discovery.zen.minimum_master_nodes=1" \
-e "xpack.security.transport.ssl.enabled=true" \
-e "xpack.security.transport.ssl.verification_mode=certificate" \
-e "xpack.security.transport.ssl.keystore.path=/usr/share/elasticsearch/config/certs/elastic-stack-ca.p12" \
-e "xpack.security.transport.ssl.truststore.path=/usr/share/elasticsearch/config/certs/elastic-certificates.p12" \
-e "thread_pool.bulk.queue_size=1000" \
-e "cluster.name=high-performance-es" \
--ulimit nofile=524288:524288 --ulimit memlock=-1:-1 \
--privileged \
--name elasticsearch \
0000000000.dkr.ecr.ap-northeast-1.amazonaws.com/es/high-performance-es:6.6.2


インデックス設定の調整

インデックスの設定はドキュメントのサイズ、入出力の頻度、性能要件、可用性等と相談する必要がある。特にエフェメラルストレージを使って性能を引き出している場合、ハードウェア障害が起こるとデータが失われるので、レプリカを持つのかEBSにバックアップを作るのかとか、恒久的なデータ保管は別のDBで担いElasticsearchは柔軟性と速度を重視するのかとか、選択と集中が必要。

性能に影響する設定としては以下のものが重要だと考えた。


  • refresh_interval


    • 投入したデータが反映されるまでの時間に影響する。長くすれば書き込み処理が粗くなるので軽くなるが、投入から集計までの時差=Latencyが延びる



  • number_of_shards


    • インデックスをクラスター内でいくつに分割するかの値。多すぎでも少なすぎでもよくないので難しいが、ノード数の4倍にした。インデックスあたりのドキュメント数や容量でも考える必要があるし、インデックスをどういう単位で分割するかにもよる。



  • codec


    • 圧縮設定。 best_compression でいいと思う。圧縮と伸張はCPUを使うが、ディスクIOの方が問題になるので、書き込みと読み出しの時点でのサイズを最小にする方が早い印象。



  • number_of_replicas


    • レプリカを作ればデータの保全はしやすいが、データを複製・同期するので遅くなる。最速を目指すならレプリカは作らない。



  • ドキュメントに対する制約をいじって使わないデータを扱わないようにする


    • max_docvalue_fields_search

    • total_fields

    • depth

    • nested_fields



  • analyzer


    • 変数ごとに有効・無効を切り替えられるが、形態素解析をしないような変数では基本的に無効にしておく。




終わりに


  • Dockerにしておくとアップデートの時に作業しやすい

  • Kibanaの設定は人間の工数がかかっているデータなので、S3にバックアップする方が良い。Kibanaの設定も当然クラスター内にあるので、誤ったシャットダウンや障害、作業ミスで消えると困る。

  • なにか色々はしょってる気がする

  • 誤りや更新が必要なところはコメントください

  • すでに7.0が出ている… プラグイン周りが変わったと思うけど他は使えるはず