クラウドワークス エンジニアブログ

日本最大級のクラウドソーシング「クラウドワークス」の開発の裏側をお届けするエンジニアブログ

Elasticsearchのバージョンを6.8系から7.5系にアップグレードしました

f:id:t0yohei:20200604122036p:plain

こんにちは、 @t0yohei です。今回は、1つ前のElasticsearchのバージョンを5.6系から6.8系にアップグレードしました のブログに続けて、 Elasticsearch v7.5 系までのアップグレードについて書いていきます。

この記事では、 v6.8 系へのアップグレードの方で書かれていたアップグレードの進め方やタスク管理法には触れず、 v7.5 系へのアップグレードで必要だった対応や課題についてのみ書いていきます。 v6.8 系アップグレード時の進め方がすごくやりやすかったので丸パクリ結果、書くネタがないという。

アップグレード全体に対してや、破壊的変更の一覧については下記の公式ドキュメントをご参照ください(リンク先はこの記事作成時点で最新の v7.7 のドキュメントになっています)。 https://www.elastic.co/guide/en/elasticsearch/reference/7.7/setup-upgrade.html https://www.elastic.co/guide/en/elasticsearch/reference/7.7/breaking-changes.html

目次

はじめに

今回のアップグレード対応に際し、アップグレード対応中にも新しいバージョンが告知されるかも?と言う状況だったので、まずはアップグレードのスコープを決めました。結論から言うと、今回の対応では v7.5 まで上げることにしました。理由としては、 Rails アプリケーションから Elasticsearch を操作するために必要な elasticsearch-rubyFaraday に依存しており、 Elasticsearch v7.6 に対応する elasticsearch-ruby v7.6 に上げるためには Faraday のメジャーバージョンを 1 系に上げる必要があり、そのコストを高く見積もったからです。

バージョン6アップグレードの際に、 Elasticsearch を v6.8 まで上げていたので、今回は v6.8 -> v7.5 に上げる対応を実施したということになります。

以下では v6.8 -> v7.5 に上げるに当たって必要だった対応を書いています。

タイプレス移行に向けた対応

バージョン7アップグレードの文脈で自分たちが対応した物の中で一番大きかった物で、本稿のメイントピックとなる対応です。 バージョン6アップグレードのブログでも話されているように、Elasticsearch はタイプレス化の方向に進んでおり、バージョン7アップレードのタイミングでも対応が必要になってきました。

Elasticsearch のデータ構造を RDB のデータ構造との比較で説明しようとした際、 RDB だと Database -> Table -> Column となっているものが、 Elasticsearch だと Index -> Type -> Document という構造になっていると説明されることが多かったのですが(この説明はバッドアナロジーとされていますが、一旦わかりやすくするために書いています)、タイプレス化では RDB の Table に当たるとされていた Type を無くすよ、ということなので変更の大きさがなんとなく想像できるかなと思います。

タイプレス化に対するバージョン7アップレードでの対応は、下記の公式ブログを元に進めて行きました。 https://www.elastic.co/jp/blog/moving-from-types-to-typeless-apis-in-elasticsearch-7-0

対応に当たってのプロセスは下記の通りです。

  1. Elasticsearch v6.8インスタンスに対して、 include_type_name=true オプションを 付けた 状態で index を構築する
  2. Elasticsearch v7.5インスタンスに対して、 include_type_name=true オプションを 付けた 状態で index を構築する
  3. Elasticsearch v7.5インスタンスに対して、 include_type_name=true オプションを 外した 状態で index を構築する

1. Elasticsearch v6.8インスタンスに対して、 include_type_name=true オプションを 付けた 上で index を構築する

Elasticsearch のインスタンスを v7.5 にあげる前準備として、 Elasticsearch v6.8 の状態で、 include_type_name=true オプション(type 有りの index を許可するオプション)を付けて index が構築できるようにします。

この際、 elasticsearch-model の gem を使っていると多少面倒な問題が出てきます。 crowdworks.jp の本体は Rails のアプリケーションとなっており、 Elasticsearch も本体のアプリケーションを通して操作しています。 index の構築にはこの elasticsearch-model の gem を使用していたのですが、この gem が v6.x の段階( elasticsearch-model v6.x は Elasticsearch v6.x に、 elasticsearch-model v7.x は Elasticsearch v7.x に対応している)では include_type_name=true のオプションに対応していません(ナ、ナンダッテー)(include_type_name オプションを設定できるようにしているのはこのコミットで、 v7.0 のリリースに組み込まれていいます)。

そのため、 elasticsearch-model を使用している場合は、下記の3つの手段から自分たちのアプリケーションにあった手段を選択することになります。

  1. Elasticsearch 本体と、 elasticsearch-model を同時に v7 系に上げる
  2. Elasticsearch 本体は v6.7 系で動かしつつ elasticsearch-model を先に v7 系に上げる
  3. モンキーパッチを作成し、 elasticsearch-model が v6 系の状態で include_type_name=true のオプションを使用できるようにする

今回の私たちのチームでは、 3 のモンキーパッチを使用する方法を取ることにしました。 1 の対応では、全ての対応が深夜メンテの時間内に完了できるか怪しかったこと、 2 の対応では、 Elasticsearch v6.8 のインスタンスに対して index を作成しようとすると X-Pack 関連のエラーが出てその対処に時間がかかると判断したためです。 モンキーパッチの多用は危険なのは皆さんご存知の通りだと思いますが、今回はすぐにモンキーパッチを外すことが決まっていたため採用することにしました。

モンキーパッチは config/initializers/elasticsearch.rb に追加しました。 内容は elasticsearch-model v7.1.0 の create_index! メソッドをそのまま持ってきています。

module Elasticsearch
  module Model
    module Indexing
      module ClassMethods
        def create_index!(options = {})
          options = options.clone
          target_index = options.delete(:index)    || self.index_name
          settings     = options.delete(:settings) || self.settings.to_hash
          mappings     = options.delete(:mappings) || self.mappings.to_hash
          delete_index!(options.merge index: target_index) if options[:force]
          unless index_exists?(index: target_index)
            options.delete(:force)
            self.client.indices.create({ index: target_index,
                                          body: {
                                            settings: settings,
                                            mappings: mappings }
                                        }.merge(options))
          end
        end
      end
    end
  end
end

このモンキーパッチで、 elasticsearch-model v6.x の状態で、Elasticsearch v6.8インスタンスに対して include_type_name=true オプションを 付けた 上での index を構築することが可能になりました。

2. Elasticsearch v7.5 のインスタンスに対して、 include_type_name=true オプションを 付けた 状態で index を構築する

1 の手順で include_type_name=true オプションが使えるようになっているので、新しく立てた Elasticsearch v7.5 のインスタンスに対して、 index 構築を行います( crowdworks.jp ではサービスを止めずにアップグレード可能な rolling upgrades ではなく、新しくインスタンスを立て直すアップグレードの方法を採用しています)。 index の構築が完了したら、 elasticsearch-model やその他の Elasticsearch 関連の gem を v7.x にアップデートし、 1 で作成したモンキーパッチを削除します。

3. Elasticsearch v7.5 のインスタンスに対して、 include_type_name=true オプションを 外した 状態で index を構築する

1, 2 の手順を経て、 Elasticsearch v7.5 と Elasticsearch 系の gem v7.x で動くようになっているので、最後に include_type_name=true オプションを外して index 再構築を行い、アプリケーションが正常に動くようにします。 include_type_name=true オプションの削除に伴い、 type を指定した上での検索などの操作が無効になるため、 type を指定している箇所を削除します。またこの際、 Elasticsearch の 内部的な type としては _doc が使用されるようになります。

以上で、公式ブログに書かれていた移行手順は完了となりました。

対応必要だった breaking changes

ここからは、 Elasticsearch のバージョンを v7.5 にあげる際に、 crowdworks.jp のアプリケーションで対応が必要だった breaking changes (破壊的変更)について記載していきます。

track_total_hits defaults to 10,000

検索結果で返す default のヒット件数上限を 10,000 件にするというもの。 https://www.elastic.co/guide/en/elasticsearch/reference/current/breaking-changes-7.0.html#track-total-hits-10000-default

crowdworks.jp でも仕事を探す画面のページネーションで、 total の値を使用していたので対応が必要になりました。 というよりも、事前の対応を行っておらず Elasticsearch のバージョンを上げた後に発覚し、急いで対応しました...。 breaking changes のドキュメントは事前に一通り確認していたのですが完全に見落としていました。ごめんなさい。ちなみに発現したのは Elasticsearch のバージョンを v7.5 に上げたタイミングではなく、 v7.5 に上げた後にタイプレス対応を終えたタイミングでした...(謎い)。

今まで通りヒット件数に上限を設けないようにするには、 elasticsearch-model を使用している場合、下記のように search メソッドを実行している箇所に option として track_total_hits: true を渡すような実装になります。

before

SomeModel.__elasticsearch__.search(elasticsearch_query)

after

search_options = { track_total_hits: true }
SomeModel.__elasticsearch__.search(elasticsearch_query, search_options)

上記の実装で、これまで通り全ヒット件数を取得できるようになります。ただ、そもそもオプション未設定の場合は 10,000 件までがデフォルト値となっていることを考えると、デフォルト値でアプリケーションを動かすようにするか、ヒット件数上限を設けることを検討するのが正しい対応かもしれません(おまいう)。

rest_total_hits_as_int

上記の track_total_hits と近い話で、こちらは hits.total のレスポンスの形式が object になるよ、というもの。 https://www.elastic.co/guide/en/elasticsearch/reference/current/breaking-changes-7.0.html#hits-total-now-object-search-response

一先ず以前と同じ形式でレスポンスを返してもらうようにするには、 track_total_hits の時と同じように、下記のように rest_total_hits_as_int: true のオプションを設定してあげます。

search_options = { rest_total_hits_as_int: true }
SomeModel.__elasticsearch__.search(elasticsearch_query, search_options)

ただ、

This parameter has been added to ease the transition to the new format and will be removed in the next major version (8.0).

と Breaking Changes のドキュメントにあるように、 Elasticsearch v8.0 ではこのオプションは削除されるようなので、最終的にはオプションを削除した上でアプリケーションが正常に動くようにする必要があります。

対応のサンプルは下記です。

#before
response.hits.total

#after
response.hits.total.value

対応必要だった breaking changes に関しては以上になります。

対応した deprecation log

今回のアップデートに際し、 crowdworks.jp のアプリケーションで出ていた Elasticsearch 関連の deprecation log は一件だけでした。

random score

crowdworks.jp では仕事情報のスコアリングに random_score を使用しており、その部分で deprecation log が出ていました。

出ていた deprecation log は下記のようなもの。

[o.e.d.i.q.f.RandomScoreFunctionBuilder] [jlt-BL8] As of version 7.0 Elasticsearch will require that a [field] parameter is provided when a [seed] is set

random_score で seed を使用する場合は、 field パラメータも一緒に入れないとダメだよ、といったもの。

random_score に関するドキュメントを見にいくと、詳細が記述されています。

今まで通りの動きを変えずに deprecation を消す場合は、 "field": "_id" を設定することになります。

{
    "query": {
        "function_score": {
            "random_score": {
                "seed": 10,
                "field": "_id"
            }
        }
    }
}

Elasticsearch アップグレード後に起きた問題とその対応

これまで話してきた一連の対応を通して、起きた問題とその対応について書いていきます。 細かなものも含めていくつか問題あったのですが、その中でも一番大きかったのが Elasticsearch を v6.8 に上げて以降、 Elasticsearch を使用している箇所でタイムアウトエラーが度々発生するようになり、 v7.5 に上げた段階で一度 Elasticsearch のサーキットブレイキングが発動してクラスタが一部落ちたり、タイムアウトエラーがさらに増大した問題です。

原因を調査していくと、ある操作を行った時に Elasticserch のクエリ実行時間が極端に伸びることがわかりました。それは、後ろの方にあるページを指定して検索を行った場合です。 crowdworks.jp には募集期限が過ぎた仕事を含めて約 1,500,000 件が検索対象として Elasticsearch に登録されていました。1ページの表示する仕事の件数が 50 件だったため、最大約 30,000 ページ目までページを指定して検索することが可能でした(ユーザー操作的には、ページ下部にページネーションが表示されているので最後の方のページのリンクを踏む、ということになる)。 Elasticsearch を v6.8 に上げて以降、 30,000 ページに近いところに対してページを指定して検索しようとすればするほど、検索にかかる時間が増えると言う状況になっていました。その際に Elasticsearch に投げられていたクエリは下記のようなものです。

GET some_index/_search?from=1500000&size=50

今回の一連の Elasticsearch のアップグレード対応を通して、特にクラスタの構成はいじっていなかったので、Elasticsearch 内部の検索ロジックの変更によるものなのかな?と予想しています。

そもそも Elasticsearch v2.1 の段階で、検索時に投げるクエリの上限値(max_result_window)のデフォルトが 10,000 になっていたので、バージョンアップで遅くなる可能性はありそうだなと考えています(今までは max_result_window をたくさん増やしてなんとか対応していた)。

この件に対する対応としては下記のものが挙げられました。

  • index に格納する document 数を減らす
  • Elasticsearch に投げるクエリのチューニングを行う
  • sherd 数など、 Elasticserach 本体の設定を見直す

index に格納する document 数を減らすことについては前々から対応の必要性が話されていたので、まずはこの対応を実施して様子を見つつ、改善が見られないようならばさらなる対応を実施する運びとなりました。

具体的な対応としては、募集中または募集が終了してから1年未満の仕事のみを検索対象として、その他の仕事に関しては Elasticsearch の index から削除することにしました。

この対応を実施することで、 1,500,000 近くあった 仕事に関する document を 約 300,000 までと 1/5 減らすことになります。

この対応は、ユーザー影響もあるので下記の通りお知らせブログを通して告知しております。 https://blog.crowdworks.jp/?p=3550

気になる結果ですが、対応完了時のパフォーマンス監視ツールでのグラフは下記のような感じになりました。 f:id:t0yohei:20200604124631p:plain

あるタイミングを境に、グラフがカクッと落ちているのがご覧いただけると思います。 Elasticsearch のクエリー実行時間を短縮できたので、今まで仕事の検索にかかっていた時間を半分以下に減らすことができたり、その結果として頻繁に起きていたタイムアウトのエラーを解消することができました。ここまで改善が目に見えて出てくると思っていたかったので、正直びっくりしています。

おわりに

Elasticsearchのバージョン 6.8 系 -> 7.5 系のアップグレードは当初のスケジュールを前倒しして、かなりスムーズに完了できました。Elasticsearchのバージョンの 5.6 系 -> 6.8 系の際の知見を大いに活用できたり、チームメンバーが高速で対応を進めてくれたことが大きかったです。まじ感謝。

今回の対応を行っている最中も Elasitcsearch の新バージョン(v7.8)が発表されたりと、 Elasticsearch を使っている限りはそこそこ大変なアップグレード作業はまだまだ続きそうな予感がしています。ただ、今回の対応を通して社内に知見がある人も増えたし、次回はそんなに苦労せずに上げれたりするんじゃないかなあと、大きなフラグを立ててこの記事を終わろうと思います。

ありがとうございました。

We are Hiring!!

クラウドワークス では、働き方の変革に挑戦するRailsエンジニア募集しています!

www.wantedly.com

© 2016 CrowdWorks, Inc., All rights reserved.