はじめに
こんにちは、検索基盤部 検索基盤ブロックの可児(@KanixT)とSRE部 ECプラットフォーム基盤SREブロックの大澤です。
本記事では、ZOZOTOWNの商品検索で利用しているElasticsearchをバージョンアップした知見と、その際に実施した検索基盤の改善についてご紹介します。
目次
- はじめに
- 目次
- 背景
- バージョンアップの流れ
- 変更箇所の調査
- LTRプラグインのバージョンアップにともなうJavaのバージョンアップ
- Elasticsearchクラスタのコード管理化
- 検証環境での負荷試験
- 別クラスタに新しいバージョンのElasticsearchを構築
- 新旧の両クラスタに対してのインデクシング
- 各種サービスの参照を旧クラスタから新クラスタへ切替え
- 旧クラスタの削除
- 最後に
背景
ZOZOTOWNでは商品の検索エンジンとして、Elastic社が提供するElastic Cloudを利用しています。公式サポートの恩恵を受けるためElasticsearchのEOLに気を遣う必要がありました。Elasticプロダクトのサポート期限は、一般公開日(GA)から18か月と定義されており、弊社で利用しているElasticsearchの期限も迫っていました。ZOZOで実施したバージョンアップ、およびバージョンアップのタイミングに合わせて実施した検索基盤の改善について、知見をご紹介します。
なお現在は8.xがリリースされていますが、作業当時の最新は7.xだったため7.10.xからのマイナーバージョンアップについての知見となります。
バージョンアップの流れ
Elasticsearchをバージョンアップするタイミングに合わせて、LTRプラグインの独自ビルド廃止やクラスタのコード管理化など、以前からチーム内で課題感のあった点も改善しています。作業は以下の流れで進めました。
なおバージョンアップに関して、当初はRolling upgradeによる更新を検討していました。しかし検索機能で利用しているindexはドキュメントの更新が常時動いており、Rolling Upgradeで失敗した際にデータの復旧が難しくなり、リスクが高いと判断しました。そのため別クラスタに新しいバージョンのElasticsearchを構築し、切替えを行う方針を採用しました。
主な作業
- 変更箇所の調査
- 新バージョンのMappingやQueryなどの調査
- Javaクライアント
- LTRプラグインのバージョンアップにともなうJavaのバージョンアップ
- Elasticsearchクラスタのコード管理化
- IaC方法の選択
- TerraformによるIaC化
- 検証環境での負荷試験
- 負荷試験の実施方法
- インスタンスタイプ検証結果
- サービスイン試験結果
- 本番環境の構築
- 別クラスタに新しいバージョンのElasticsearchを構築
- 新旧の両クラスタに対してのインデクシング
- 各種サービスの参照を旧クラスタから新クラスタへ切替え
- 旧クラスタの削除
変更箇所の調査
新バージョンのMappingやQueryなどの調査
バージョンアップの事前準備としてまずはMigration guideを確認し、利用予定の新バージョンまでにリリースされた機能でMappingやQueryに影響がある変更を一通り確認しました。
次にアップデートターゲットとなるバージョンのElasticsearchを新クラスタに検証目的で構築しました。その環境で現在動作している検索クエリとインデキシングを実行します。Deprecation logsを有効にすると非推奨のElasticsearchの機能を確認できるため、その方法についてご紹介します。
なお、今回のバージョンアップでは検索クエリとインデクシングの両方でクエリ修正は1件もありませんでした。
Deprecation logsが有効になっていることの確認
Kibana Dev Toolsを使用して下記リクエストを実行し、Deprecation logsの設定を確認します。詳細はDeprecation logsの公式ページをご覧ください。
GET /_cluster/settings?include_defaults&filter_path=defaults.cluster.deprecation_indexing
実行結果は次のようになり、"deprecation_indexing.enabled" : "true"
の場合に非推奨のログが出力されます。
{ "defaults" : { "cluster" : { "deprecation_indexing" : { "enabled" : "true", "x_opaque_id_used" : { "enabled" : "true" } } } } }
Deprecation logsの有効が確認できましたので試しにログを出力し、出力内容を確認します。
非推奨のログメッセージを確認するため、Elasticsearch 7.16.0の環境にて、バージョン7.0で廃止されたタイプ(type)を利用します。
バージョン7.16.0でタイプ(type)を利用
次のPUTクエリを実行し、廃止されたタイプ(type)を利用します。
PUT /corp/employee/1 { "first_name" : "hakoneko", "last_name" : "max", "age" : 25 }
実行結果のレスポンスはこちらです。タイプ(type)を廃止した旨のワーニングが表示されました。
#! [types removal] Specifying types in document index requests is deprecated, use the typeless endpoints instead (/{index}/_doc/{id}, /{index}/_doc, or /{index}/_create/{id}). { "_index" : "corp", "_type" : "employee", "_id" : "1", "_version" : 1, "result" : "created", "_shards" : { "total" : 2, "successful" : 1, "failed" : 0 }, "_seq_no" : 0, "_primary_term" : 1 }
次にDeprecation logに出力された内容を確認します。ログを検索するクエリはこちらです。
GET .logs-deprecation.elasticsearch-default/_search { "size": 1, "sort": [ { "@timestamp": { "order": "desc" } } ] }
検索結果のレスポンスはこちらです。
{ "_index" : ".ds-.logs-deprecation.elasticsearch-default-2022.05.19-000006", "_type" : "_doc", "_id" : "tZ9Q8YAB8Tww7jHGnFO2", "_score" : null, "_source" : { "event.dataset" : "deprecation.elasticsearch", "@timestamp" : "2022-05-23T14:27:11,423Z", "log.level" : "CRITICAL", "log.logger" : "org.elasticsearch.deprecation.rest.action.document.RestIndexAction", "elasticsearch.cluster.name" : "es-docker-cluster", "elasticsearch.cluster.uuid" : "********************", "elasticsearch.node.id" : ""********************",", "elasticsearch.node.name" : "elasticsearch", "trace.id" : "", "message" : "[types removal] Specifying types in document index requests is deprecated, use the typeless endpoints instead (/{index}/_doc/{id}, /{index}/_doc, or /{index}/_create/{id}).", "data_stream.type" : "logs", "data_stream.dataset" : "deprecation.elasticsearch", "data_stream.namespace" : "default", "ecs.version" : "1.7", "elasticsearch.event.category" : "types", "event.code" : "index_with_types", "elasticsearch.http.request.x_opaque_id" : "" }
PUTクエリのワーニングと同じように、タイプ(type)を廃止した旨のワーニングが表示されました。このように非推奨の機能が確認可能なため、バージョンアップの際は是非ご利用ください。
Javaクライアント
ZOZOTOWNの商品検索API(Spring Boot)は、Elasticsearchへ接続するクライアントに下記テックブログで紹介している通り、High Level Rest Client(以下、HLRC)を使用しています。
しかしながらHLRCは7.15.0で非推奨になり、新たなJava API Clientがリリースされました。そのため今回のバージョンアップ作業として、新Java API Clientに移行するかを移行ドキュメントと検索クエリのドキュメントで確認し検討しました。ドキュメントから実装方法が大きく異なっていることを確認したため、改修にはある程度の期間が必要であると想定出来ました。そのためEOLの迫っている現状での対応は見送ることとしました。新Java API Clientを利用することで得られる恩恵は少なからずあると思うので早めの移行したいと思います。
LTRプラグインのバージョンアップにともなうJavaのバージョンアップ
こちらのテックブログに記載があるように、弊社ではLTRプラグインを利用しており、特徴量を出力する過程で利用している特徴量キャッシュの機能をコントリビュートしました。
特徴量キャッシュの機能がマージされた
特徴量キャッシュの機能がリリースされるまでの期間は、本家のリポジトリをForkした独自ビルドを利用していました。本家のリポジトリに送ったプルリクエストがマージされ、Elasticsearch v7.16.3以降を対象にリリースされました。そのため独自ビルドを止め本家のLTRプラグインを利用することとしました。
対象 | バージョン |
---|---|
LTRプラグイン | v1.5.8-es7.16.3 |
LTRプラグインを利用しているAPI(Spring Boot)は、Java 11で開発していました。 そのためSpring BootのPOM.xmlに依存関係を追加します。
<!-- https://mvnrepository.com/artifact/com.o19s/elasticsearch-learning-to-rank --> <dependency> <groupId>com.o19s</groupId> <artifactId>elasticsearch-learning-to-rank</artifactId> <version>1.5.8-es7.16.3</version> </dependency>
いざv1.5.8-es7.16.3以上のLTRプラグインを利用しようとするとこのようなエラーが出ました。
クラス・ファイル/xxxx/.m2/repository/com/o19s/elasticsearch-learning-to-rank/1.5.8-es7.16.3/elasticsearch-learning-to-rank-1.5.8-es7.16.3.jar!/com/o19s/es/ltr/logging/LoggingSearchExtBuilder.classは不正です クラス・ファイルのバージョン58.0は不正です。55.0である必要があります 削除するか、クラスパスの正しいサブディレクトリにあるかを確認してください。
このエラーの内容は、LTRプラグインはJava 14(クラス・ファイルのバージョン58.0)でコンパイルされ、開発環境で利用しているJava 11(クラス・ファイルのバージョン55.0)ではLTRプラグインを利用できないことを意味します。
そのためAPI開発で利用しいているJavaを14以上にバージョンアップする必要が出てきたため、Javaのバージョンアップも急遽実施しました。
Javaクラスファイルのバージョン確認方法
ここではJavaクラスファイルのバージョン確認方法をご紹介します。コンパイルで生成されたクラスファイルに対してjavapコマンドを実行します。
javap -v test.class
実行結果より、major versionの記述がある箇所を確認します。
・・(抜粋)・・ major version: 55 ・・(抜粋)・・
この例では major version: 55
のため、Java 11をターゲットにコンパイルされたクラスファイルであることが分かります。クラスファイルのバージョンを確認する必要がある場合は、javapコマンドを利用して確認してみてください。
Elasticsearchクラスタのコード管理化
SREチームで運用しているバージョンアップ前のクラスタには以下の課題がありました。
- Webコンソールからの操作でクラスタを作成しており、再作成時に必要な初期設定などの再現性が低い
- ノード拡張はecctl(Elastic Cloud Control)をラップしたスクリプトで操作し、プラグイン設定はWebコンソールから操作する、といった半手動運用によりオペレーションミスが混入し易い
- 手動運用が入りIaC化出来ていない箇所があるため、インフラ構成変更のレビューコストが高い
- ecctlをラップしたスクリプトのメンテナンスコストが高い
こうした課題を解決するため、SREチームではElasticsearchの構築・運用を全てIaC化し管理したいモチベーションがありました。今回は新しいクラスタへ立て替える機会に合わせてIaC化を実施しました。
IaC方法の選択
IaC化するにあたって幾つかの選択肢が考えらました。
- ecctlを利用する
- Elasticsearch Service APIを利用する
- Elastic社より提供されるTerraform providerを利用する
前述の通り既に一部運用にecctlを利用していますが、ノード拡張といった特定の操作を簡略化するためにecctlをラップしたスクリプトを作り込んでいる状況があります。Elasticsearch Service APIを利用した場合も同様にラップしたスクリプトを作り込む必要が想定されました。またスクリプトを作り込んでいった結果、Terraformで提供されている機能を再現してしまった、という車輪の再発明に至る可能性もあります。
そのため今回は独自な作り込みが不要なこと、またSREチームで既に運用しているTerraform用CI/CDが利用可能なメリットもあることから、TerraformによるIaC化を選択しました。
TerraformによるIaC化
TerraformでのIaC化を進めるにあたり、運用に必要な機能が提供されているか検証する必要があります。
resource “ec_deployment” “poc_cluster” { region = “ap-northeast-1” version = “7.17.0" deployment_template_id = “aws-cpu-optimized-arm” name = “poc_cluster” elasticsearch { autoscale = “false” topology { id = “hot_content” size = “1g” zone_count = “1" size_resource = “memory” } extension { name = ec_deployment_extension.poc_plugin.name type = “bundle” version = “*” url = ec_deployment_extension.poc_plugin.url } } kibana {} apm {} enterprise_search {} } resource “ec_deployment_extension” “poc_plugin” { name = “poc_plugin” description = “poc_plugin” version = “*” extension_type = “bundle” file_path = “./poc_plugin.zip” file_hash = “xxxxxxxxx” }
これは最小ノード構成のクラスタを作る場合のサンプルコードです。例えばノードサイズの変更ができるか確認する場合は、以下のようにコードを変更しterraform applyを実行する方法で検証しました。
topology { id = “hot_content” size = “1g” -> “60g // 1GBから60GBへ変更できるか確認 zone_count = “1” size_resource = “memory” }
このような方法で検証を進め、現在SREチームで行っている運用業務が全てコード変更で行えるか確認した上で、TerraformでのIaC化を最終決定しました。
また、Elasticsearchクラスタは開発環境〜本番環境それぞれ個別に存在しており、クラスタ構成自体に差分もあります。ある環境への変更が他の環境に影響を及ぼすことは避けなければなりません。
こうした環境間の問題を考慮し採用したディレクトリ構成がこちらです。
elastic_cloud ├── main.tf // Terraform verや使用するproviderを定義 ├── plugin │ └── xxxx.zip // 各種プラグインファイルをこのフォルダに配置 ├── dev │ ├── main.tf -> ../main.tf │ ├── local.tf // 使用するプラグインpathなど、環境変数を定義 │ └── elasticsearch.tf // Elasticsearchクラスタを定義 ├── stg │ ├── main.tf -> ../main.tf │ ├── local.tf │ └── elasticsearch.tf └── prd ├── main.tf -> ../main.tf ├── local.tf └── elasticsearch.tf ※.tfstateの出力先管理ファイル等、本解説に関係のないファイルについては割愛しています。
- 本番環境クラスタは専用のマスタノードを構成する、など環境毎にクラスタ構成の差異が存在します。こういった差異は環境変数では吸収できないため共通化ファイルとはせず、各環境で定義する方式としています。
- Elastic Cloud上でのプラグインはクラスタ単位ではなくアカウント単位での管理となります。 全環境で共通に使用されているプラグインを更新すると全環境へ同時に反映されてしまいます。これを防ぐためプラグイン定義を環境毎に分離しました。
このようにElasticsearchクラスタのIaC化を行いました。まだSREチームで運用しているクラスタ全てがIaC化されてはおりませんが、引き続きバージョンアップ作業などを機にIaC化を取り組む予定です。
検証環境での負荷試験
バージョンアップ前のクラスタはインスタンスタイプにm5d(general purpose)インスタンスを選択していました。その後、日々運用していく中でパフォーマンス改善に期待できるc6gd(CPU optimized)インスタンスが提供されました。SREチーム内でも検証したいインスタンスタイプではありましたが、一度作成したクラスタのインスタンスタイプは変更できないこともあり低い優先度となっていました。今回クラスタを作り直す機会に合わせて、m5dインスタンスからc6gdインスタンスへの変更を検討するため負荷試験を実施しました。
また、今回はElasticsearchのバージョンアップの他に、LTRプラグインのバージョンアップもありパフォーマンスの変化が予想されます。 そのため、バージョンアップ後クラスタをサービスインさせるにあたり、年末年始の安定稼働を保証するためZOZOTOWNが想定する最大負荷を掛ける負荷試験を実施しました。
負荷試験の実施方法
負荷試験は検証環境に本番環境と同等構成のアプリケーションpod・バージョンアップ後クラスタを用意し、試験パターンに応じてpod・ノード数を可変させ実施しております。また負荷をかける方法としてOSS負荷試験フレームワークであるgatlingを用いており、API Gatewayから検索API・Elasticsearchを通したレイテンシを計測しています。
gatlingの実行には分散負荷試験ツールGatling Operatorを用いました。Gatling Operatorは分散負荷試験のライフサイクルを自動化するKubernetes Operatorです。先日SRE部より紹介しておりますので詳細はこちらをご覧ください。
インスタンスタイプ検証結果
バージョンアップ前のクラスタを基準にした、バージョンアップ後のm5dクラスタとc6gdクラスタの傾向は以下です。
インスタンスタイプ | CPU使用率 | 99パーセンタイルレイテンシ |
---|---|---|
m5d | 同等 | 同等 |
c6gd | 改善 | 悪化 |
※あくまでZOZOにおける検索の利用方法による結果となります。一概に全ての利用方法で同じ傾向になるということではありません。
SREチームでは99パーセンタイルレイテンシに基準値を設けており、基準値を超えた場合はリリースNG判定をしています。
c6gdインスタンスは、CPU使用率について改善が見られたものの99パーセンタイルレイテンシがリリース基準値を超えてしまいました。
m5dクラスタとc6gdクラスタは異なるCPUアーキテクチャを採用しており、この差異が今回の結果となったと想定しています。
ただし今回はEOLまでの期間が迫っていたこともあり、この差異の詳細についての深掘りはせず引き続きm5dクラスタを使う方針で決定しました。パフォーマンス改善のためにもCPU optimizedインスタンスへの切替は、今後も機を見て挑戦する予定です。
最大負荷時におけるバージョンアップ前後の比較結果
最大負荷を想定したノード数まで拡張を行い実施した負荷試験のバージョンアップ前後の比較です。
CPU使用率 | 99パーセンタイルレイテンシ |
---|---|
同等 | 同等 |
バージョンアップに伴う性能変化はないことを確認した上でサービスインを実施しました。
別クラスタに新しいバージョンのElasticsearchを構築
前述の通り、Terraformコードを基にクラスタを自動構成するため、IaC化が完了した時点で本番環境の構築は簡単に進む想定でした。しかしながら、実際にはクラスタ構築をしたところ以下のようなエラーが発生しました。
Error: failed creating deployment: 2 errors occurred: * api error: clusters.cluster_plan_version_not_permitted: The requested version of [7.17.0] set in [elasticsearch.version] is not permitted as it violates the ESS version policy (resources.elasticsearch[0].elasticsearch.version) * set “request_id” to “l1vwuu43qlzgkgppw51j1lncgc2mboe78cqf1g3g913nyq8co0pbut6xdmtbz81l” to recreate the deployment resources
これはElastic Cloudサポートへ確認したところバージョン7.17.0m5dインスタンスクラスタの対応が終了していたことが原因でした。そのため、Elastic Cloudサポートより一時的にクラスタ作成の制限を解除する対応をとっていただきました。
制限の解除後、m5dインスタンスに対応する最終バージョンである7.13.0でクラスタを構築し、構築後7.17.0にアップデートする手順でクラスタを構成しました。
resource “ec_deployment” “elasticsearch” {
region = “ap-northeast-1”
version = “7.13.0" -> “7.17.0”
こちらはTerraformによるクラスタバージョンアップ時の変更コードです。図らずもTerraformによるバージョンアップ作業が正常に行えることの確認にもなり、今後のバージョンアップにも役立つ結果となりました。
新旧の両クラスタに対してのインデクシング
冒頭にも記載しました通りRolling upgradeでのバージョンアップはリスクがあると判断したため新クラスタを準備しました。そのため旧クラスタと新クラスタの両方にインデクシングを行い、同様の頻度で更新を実施することで全く同じ環境を構築しました。
新旧の両クラスタへのインデクシングには単純に直列で行うと2倍の時間がかかりますが、ZOZOTOWNのインデクシングの仕組みには、以前より並列で動作させる仕組みがありました。そのため並列度を上げることで、インデクシングのサービスレベルを落とすこと無くインデックスを構築しました。
構築した新旧クラスタのインデックスの検証には、インデックス比較用にPythonスクリプトを作成し、定期的にインデックス差分が無いかをチェックしました。また、品質管理部にも協力いただき、ZOZOの画面レベルでも検索結果の比較テストを実施することで品質を担保しました。
各種サービスの参照を旧クラスタから新クラスタへ切替え
旧クラスタから新クラスタへの切替作業に関して、弊チームが管理しているAPIを経由するリクエストは容易に切替えることができました。しかしZOZOTOWNの検索機能で利用しているインデックスには他チームが管理しているモデルやインデックスもあり、さらに直接Elasticsearchを参照しているチームもありました。そのため、切替時には各チームと事前に日程を調整し、切替えを実施しましたが、想像以上に連携・調整コストが掛かりました。
バージョンアップでクラスタを切替える度に、他チームと連携・調整するコストはとても負担が大きいです。そのため今後はElasticsearchの直接参照は出来る限り廃止し、Elasticsearchへのリクエストは弊チームが開発したAPIを一律経由させるよう継続して改善を進めています。
旧クラスタの削除
新クラスタへの切替が完了した後に旧クラスタを削除しました。その際、Elasticsearchを直接参照している機能があるため、旧クラスタへの参照が残っていないことの確認はSlowlogを用いて確認しました。
Slowlog
インデックス毎にquery/fetch/indexの処理時間をwarn, info, debug, traceレベルで設定できます。設定時間を上回ったクエリは専用ログに出力され、ログを検索することでどのような処理が動いたかを確認出来ます。詳細はSlowlogの公式ページをご覧ください。
インデックスに対する設定内容はこちらです。0sにすることで全てのクエリがログ出力されるようになります。
PUT /zozo-demo-index/_settings { "index.search.slowlog.threshold.query.debug": "0s" }
Slowlogを確認するクエリはこちらで、ログ出力時のタイムスタンプを降順にソートさせています。
GET /elastic-cloud-logs-7/_search { "size":1000, "query": { "bool": { "must": [ { "term": { "log.level": { "value": "DEBUG" } } }, { "range": { "@timestamp": { "from":"2022-04-27T08:55", "to":"now" } } } ] } }, "sort": { "@timestamp": { "order": "desc" } }, "_source": ["@timestamp","message"] }
最後に
Elasticsearchのバージョンアップサイクルは早いため、追従するにはとても労力が必要な作業です。今回紹介した知見でElasticsearchのバージョンアップが少しでも楽になれば幸いです。
またElasticsearchは新機能の追加やパフォーマンス向上も積極的に行われているため、バージョンアップで得られる恩恵は少なからずあると思います。そのため手が付けられないほど最新バージョンと差が広がる前に、定期的なバージョンアップをおすすめ致します。
弊社では、検索機能を開発・改善していきたいエンジニアを募集中です。