みなさん、こんにちは。那須です。
気がつけば技術記事を3ヶ月も書かずに仕事してました。もう何年もブログを書いてきたので、書いてない期間が長ければ長いほど自分の中に「書かないと気持ち悪い」という思いが出てきます。その思いをこの記事で消してしまおうと思ってこれを書いています。
今回はAmazon Elasticsearch Service(以下、Elasticsearch)の運用についてのお話です。実際にElasticsearchを運用されている方にとっては当たり前の内容なのかもしれません。しかし私にとっては初めての経験でとても大変な思いをしたので、忘れないようにここにまとめておきます。
なぜreindexをすることにしたのか? 私が所属する dotD では onedog というサービスを提供していて、その一部の機能でデータストアにElasticsearchを使っています。2021年6月現在、Elasticsearchは以下のような使い方をしています。
また、以下のAWSのドキュメントにはこのような記載があります。
AWSサポートの方によると、シャードサイズが50GBを超えているとシャードの再配置や移行で問題が発生する可能性が高くなることがあるそうです。Elasticsearchの構成変更やアップグレード時にドメインが処理中のままになる等ですね。ただ、必ずそうなるというわけではなさそうです。
というわけで、放っておくとElasticsearch運用の観点から危険な状況になる可能性があることがわかりました。reindexでシャード数を増やしてシャードサイズを減らせることがわかったので、データがシャードから溢れることを防ぐためにやってみることにしました。
なぜalias作成をしたのか? reindexで同じマッピング設定で新インデックスにドキュメントをコピーして、アプリから新インデックスにアクセスするようにすれば何も問題はないのですが、こういった作業をするたびにアプリの接続先を変更するのは正直面倒です。また、作業ミス等で変更に失敗すると、その時間だけサービスを止めてしまうことになります。
Elasticsearchにはaliasという便利なものがあって、インデックスに対してaliasを作れます。実際のインデックス名が index1 で、alias名が index だった場合、アプリからは index という名前のインデックスにアクセスするようにしておけば、裏でインデックス名が変わってもアプリには何も影響がないようにできます。こうすることで、サービス停止することなくインデックスに対する作業を行うことができますね。
何が問題だったのか? 2021年6月からめでたくonedogを海外のユーザにもご利用いただけるようになりました! ですので、単純にreindexとalias作成するだけではなく、いかにサービスを停止することなく、もしくは停止時間を短くできるかが重要なポイントになっています。日本国内だけにしかユーザがいない場合は夜間にある程度サービスを止めても影響は小さいですが、グローバル展開後はそうもいかないですよね。
また、reindex実施中はインデックス内のドキュメントの丸ごとコピーが行われるので、CPU負荷とIOが激増します。それが一瞬で終わればいいのですが、Elasticsearchには以下のドキュメントがありました。
一度本番環境のElasticsearchのレプリカを作って何もチューニングせずにreindexのテストをしたところ、1日経過しても終わりませんでした。CPU利用率も90%近くまで上がります。しかも6時間くらい経過するとCPU負荷もIOも1/4程度まで下がってしまって、reindexのスピードがかなり低下します。これではサービスへの影響が大きすぎますね。
AWSサポートに上記の事象を説明し、どうすれば最短時間でreindexを完了できるかを問い合わせてみたところ、以下のように遅くなった原因の回答がありました。
最近、EC2インスタンスの運用をしていなかったのでEBSのバーストクレジットの存在を忘れていました。しかも、まさかElasticsearchにもそれが適用されるとは… というわけで、以下のようにElasticsearchドメインを変更しました。
データノードのインスタンスサイズを4倍、EBSサイズを10倍以上にしました。これだけでもかなり時間短縮できたんですが、さらにElasticsearch内の新インデックスのチューニングを以下のように行いました。
これで再度reindexのテストをしたところ、約3時間でreindex作業を完了することができました!
いつやるか? あとは手順を正確に書いて実行するだけですね。最初は何かあった時のために夜間作業にしようと思っていました。ただ、インフラ運用に携わっている方ならわかると思いますが、夜間作業って安全なようで安全じゃないんですよね。
無停止でできる作業ならなるべく平日日中にやりたい。そう思うのは当然です。今回の作業は、事前のテストで負荷がかかるのは約3時間とわかっていますし、サービスの停止も必要ありません。そして、以下のCloudWatchメトリクスが作業対象のElasticsearchのCPU利用率とIndexingRateなんですが、ピークが1日に2回あってほぼちょうど12時間周期なんです。
もしかして平日日中にできるのでは…と思ったので、一番頭が元気な月曜の日中にすることにしました。平日日中であればエラーにもすぐ気づけますし、他のメンバーのサポートも受けることができるので安心です。
実際の作業の流れ 最初にElasticsearchのスケールアップです。スケールアップ中はインスタンス数が2倍になりますが、その時にシャードの状態を見てみると新しくできたデータノードにRELOCATINGしている様子がわかりますね。
curl https : / / xxx . ap - northeast - 1 . es . amazonaws . com / _cat / shards ? v index shard prirep state docs store ip node aaa 3 p STARTED 171386089 40 . 2gb x . x . x . x tFeRJ2T aaa 3 r STARTED 171386089 40 . 2gb x . x . x . x WDfvLeZ aaa 1 p STARTED 171383176 40 . 2gb x . x . x . x WDfvLeZ aaa 1 r RELOCATING 171383176 40 . 3gb x . x . x . x 7ZU3PTa - > x . x . x . x tFeRJ2TKRP6_mNettuLwOw tFeRJ2T aaa 4 p RELOCATING 171374950 40 . 2gb x . x . x . x qUSFDz6 - > x . x . x . x rLpedxzSQQ - ZWUtVF44QKA rLpedxz aaa 4 r RELOCATING 171374950 40 . 3gb x . x . x . x xslgACl - > x . x . x . x tFeRJ2TKRP6_mNettuLwOw tFeRJ2T aaa 2 p STARTED 171377573 40 . 2gb x . x . x . x tFeRJ2T aaa 2 r RELOCATING 171377573 40 . 3gb x . x . x . x 7ZU3PTa - > x . x . x . x rLpedxzSQQ - ZWUtVF44QKA rLpedxz aaa 0 p STARTED 171356113 40 . 4gb x . x . x . x rLpedxz aaa 0 r RELOCATING 171356113 40 . 3gb x . x . x . x xslgACl - > x . x . x . x WDfvLeZoTKmuvNVL2HQhgA WDfvLeZcurl https : / / xxx . ap - northeast - 1 . es . amazonaws . com / _cat / shards ? v index shard prirep state docs store ip node aaa 3 p STARTED 171386089 40 . 2gb x . x . x . x tFeRJ2T aaa 3 r STARTED 171386089 40 . 2gb x . x . x . x WDfvLeZ aaa 1 p STARTED 171383176 40 . 2gb x . x . x . x WDfvLeZ aaa 1 r RELOCATING 171383176 40 . 3gb x . x . x . x 7ZU3PTa - > x . x . x . x tFeRJ2TKRP6_mNettuLwOw tFeRJ2T aaa 4 p RELOCATING 171374950 40 . 2gb x . x . x . x qUSFDz6 - > x . x . x . x rLpedxzSQQ - ZWUtVF44QKA rLpedxz aaa 4 r RELOCATING 171374950 40 . 3gb x . x . x . x xslgACl - > x . x . x . x tFeRJ2TKRP6_mNettuLwOw tFeRJ2T aaa 2 p STARTED 171377573 40 . 2gb x . x . x . x tFeRJ2T aaa 2 r RELOCATING 171377573 40 . 3gb x . x . x . x 7ZU3PTa - > x . x . x . x rLpedxzSQQ - ZWUtVF44QKA rLpedxz aaa 0 p STARTED 171356113 40 . 4gb x . x . x . x rLpedxz aaa 0 r RELOCATING 171356113 40 . 3gb x . x . x . x xslgACl - > x . x . x . x WDfvLeZoTKmuvNVL2HQhgA WDfvLeZ
スケールアップが完了したら、作業前の状態を確認しておきます。
# index数確認 curl https : / / xxx . ap - northeast - 1 . es . amazonaws . com / _cat / indices ? v # shardサイズ確認 curl https : / / xxx . ap - northeast - 1 . es . amazonaws . com / _cat / shards ? v # index settings確認 curl https : / / xxx . ap - northeast - 1 . es . amazonaws . com / aaa / _settings ? pretty # mapping確認 curl https : / / xxx . ap - northeast - 1 . es . amazonaws . com / aaa / _mapping ? pretty# index数確認 curl https : / / xxx . ap - northeast - 1 . es . amazonaws . com / _cat / indices ? v # shardサイズ確認 curl https : / / xxx . ap - northeast - 1 . es . amazonaws . com / _cat / shards ? v # index settings確認 curl https : / / xxx . ap - northeast - 1 . es . amazonaws . com / aaa / _settings ? pretty # mapping確認 curl https : / / xxx . ap - northeast - 1 . es . amazonaws . com / aaa / _mapping ? pretty
ではreindexの宛先となる新インデックスを作成しましょう。アプリからの接続とElasticsearchの内部の状態の図は以下になります。旧インデックスは今まで使っていたインデックス、新インデックスは今作った空のインデックスです。
# 新index作成 curl - XPUT 'https://xxx.ap-northeast-1.es.amazonaws.com/bbb' - H 'Content-Type: application/json' - d' { "settings" : { "refresh_interval" : "-1" , "number_of_shards" : "12" , "number_of_replicas" : "0" , "translog.durability" : "async" } , "mappings" : { ... 省略 } } ' # index数確認 curl https : / / xxx . ap - northeast - 1 . es . amazonaws . com / _cat / indices ? v -- health status index uuid pri rep docs . count docs . deleted store . size pri . store . size green open aaa uEsxWninRgOyhF1RN_V6tg 5 1 856959620 1017639 20 . 1kb 10kb green open bbb EzBqmn7pRUuM56fpi5ynaQ 12 0 0 0 2 . 6kb 2 . 6kb# 新index作成 curl - XPUT 'https://xxx.ap-northeast-1.es.amazonaws.com/bbb' - H 'Content-Type: application/json' - d' { "settings" : { "refresh_interval" : "-1" , "number_of_shards" : "12" , "number_of_replicas" : "0" , "translog.durability" : "async" } , "mappings" : { ... 省略 } } ' # index数確認 curl https : / / xxx . ap - northeast - 1 . es . amazonaws . com / _cat / indices ? v -- health status index uuid pri rep docs . count docs . deleted store . size pri . store . size green open aaa uEsxWninRgOyhF1RN_V6tg 5 1 856959620 1017639 20 . 1kb 10kb green open bbb EzBqmn7pRUuM56fpi5ynaQ 12 0 0 0 2 . 6kb 2 . 6kb
それではreindexを実施しましょう。1回目のreindexで実行時の全データをコピー、2回目のreindexで1回目のreindex中に発生した差分だけをコピーしています。
# reindex実施 curl - X POST "https://xxx.ap-northeast-1.es.amazonaws.com/_reindex?slices=auto&wait_for_completion=false&pretty" - H 'Content-Type: application/json' - d' { "conflicts" : "proceed" , "source" : { "index" : "aaa" } , "dest" : { "index" : "bbb" , "version_type" : "external" } } ' # reindex task一覧を確認する curl https : / / xxx . ap - northeast - 1 . es . amazonaws . com / _tasks ? actions = * reindex # reindex 親タスクで完了確認する curl https : / / xxx . ap - northeast - 1 . es . amazonaws . com / _tasks / < task_id > ? pretty # 差分だけreindex実施 curl - X POST "https://xxx.ap-northeast-1.es.amazonaws.com/_reindex?slices=auto&pretty" - H 'Content-Type: application/json' - d' { "conflicts" : "proceed" , "source" : { "index" : "aaa" , "query" : { "range" : { "timestamp" : { "gte" : "now-4h" } } } } , "dest" : { "index" : "bbb" , "version_type" : "external" } } '# reindex実施 curl - X POST "https://xxx.ap-northeast-1.es.amazonaws.com/_reindex?slices=auto&wait_for_completion=false&pretty" - H 'Content-Type: application/json' - d' { "conflicts" : "proceed" , "source" : { "index" : "aaa" } , "dest" : { "index" : "bbb" , "version_type" : "external" } } ' # reindex task一覧を確認する curl https : / / xxx . ap - northeast - 1 . es . amazonaws . com / _tasks ? actions = * reindex # reindex 親タスクで完了確認する curl https : / / xxx . ap - northeast - 1 . es . amazonaws . com / _tasks / < task_id > ? pretty # 差分だけreindex実施 curl - X POST "https://xxx.ap-northeast-1.es.amazonaws.com/_reindex?slices=auto&pretty" - H 'Content-Type: application/json' - d' { "conflicts" : "proceed" , "source" : { "index" : "aaa" , "query" : { "range" : { "timestamp" : { "gte" : "now-4h" } } } } , "dest" : { "index" : "bbb" , "version_type" : "external" } } '
これでほぼ全てのデータが新インデックスにコピーできました。新インデックスを使うように切り替える前に、障害発生してもデータが消えないようにレプリカシャードを作成しておきましょう。その他のインデックス設定も通常運用の状態にしておきます。
# index設定を戻す # レプリカ作成完了までstatusはyellowになる curl - X PUT 'https://xxx.ap-northeast-1.es.amazonaws.com/bbb/_settings?pretty' - H 'Content-Type: application/json' - d' { "index" : { "refresh_interval" : null , "number_of_replicas" : "1" , "translog.durability" : null } } '# index設定を戻す # レプリカ作成完了までstatusはyellowになる curl - X PUT 'https://xxx.ap-northeast-1.es.amazonaws.com/bbb/_settings?pretty' - H 'Content-Type: application/json' - d' { "index" : { "refresh_interval" : null , "number_of_replicas" : "1" , "translog.durability" : null } } '
本来であれば、ここでalias作成とともに旧インデックスを削除するのですが、最後のreindex実施からここまでで僅かながら差分データが発生しています。この差分も取り込みつつ今も流れてくるデータを新インデックスに入れたいので、今回はアプリのElasticsearchの接続先を一時的に切り替えます。その後、最後のreindexを実施して新インデックスが最新の状態になるようにしましょう。
これでalias作成の準備が整いました。以下のコマンドでaliasを作成しましょう。aliasを作成すると同時に旧インデックスは削除します(名前が重複するため)。alias作成後は、アプリのElasticsearchの接続先を元の名前(今作ったalias名)に戻しましょう。
# snapshotが実行されていないことを確認 curl https : / / xxx . ap - northeast - 1 . es . amazonaws . com / _snapshot / _status # alias作成 curl - X POST "https://xxx.ap-northeast-1.es.amazonaws.com/_aliases?pretty" - H 'Content-Type: application/json' - d' { "actions" : [ { "add" : { "index" : "bbb" , "alias" : "aaa" } } , { "remove_index" : { "index" : "aaa" } } ] } ' # alias確認 curl https : / / xxx . ap - northeast - 1 . es . amazonaws . com / _aliases ? pretty# snapshotが実行されていないことを確認 curl https : / / xxx . ap - northeast - 1 . es . amazonaws . com / _snapshot / _status # alias作成 curl - X POST "https://xxx.ap-northeast-1.es.amazonaws.com/_aliases?pretty" - H 'Content-Type: application/json' - d' { "actions" : [ { "add" : { "index" : "bbb" , "alias" : "aaa" } } , { "remove_index" : { "index" : "aaa" } } ] } ' # alias確認 curl https : / / xxx . ap - northeast - 1 . es . amazonaws . com / _aliases ? pretty
これでメインの作業は完了です。reindexを実施すると.tasksというインデックスが作成されていると思いますが、これはタスクの状況を確認できるようにするためのインデックスなので削除してもOKです。
# . task index削除 curl - X DELETE "https://xxx.ap-northeast-1.es.amazonaws.com/.tasks" # . task index削除 curl - X DELETE "https://xxx.ap-northeast-1.es.amazonaws.com/.tasks"
最後のステップです。最初にElasticsearchをスケールアップしているので、元のインスタンスタイプとEBSサイズに戻しましょう。そのままでもサービスは大丈夫ですが、コスト的には大丈夫じゃないです。
作業結果 サービス無停止、データロスもなし、ピーク時間帯に負荷をかけることもなく、平日日中に作業を完了させることができました! CloudWatchメトリクスでみるとCPUは50%程度の増加だったので、ピーク時間帯にかかってもギリギリ大丈夫だったのかもしれません。シャード数は5から12に増えているので、CPU利用率が若干上がっているような雰囲気もありますが、負荷への影響もほとんどなさそうです。
これでしばらくはデータ量の増加にも耐えられますし、aliasを利用することで背後のインデックスを考慮する場面もかなり減りました。
これからのこと 今回の作業でシャード数が増えてデータ保存量は増えましたが、根本的には同じ問題を抱えたままです。古いデータを別のデータストアにアーカイブするのか、ユーザによって残すデータを定義して保存量を制御していくのか、そもそもElasticsearchではなく別のデータストアに変更するなど、どうするかはこれから考えていきます。データ量が一定量を保つようになれば今回のようなreindex作業はほぼなくなると思いますので、真剣に考えていきます。
さいごに Elasticsearchのreindexとalias作成について書きました。やってみて初めてわかりましたが、Elasticsearchの運用って意外と難しいですね。誰か1人にでもこの記事が役に立てば嬉しいです。
今回はElasticsearchについて書きましたが、弊社では主にAWSサービスを活用して様々なサービスを開発しています。いい感じで運用できている部分やまだまだ改善の余地ありな部分までいろいろありますが、日々改善を重ねています。 一緒にインフラの運用や改善活動をやっていただけるエンジニアの方を大募集しています!少しでも興味があればお声がけください!