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

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

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

f:id:k-waragai:20200603120838p:plain

紹介

混沌に秩序を齎すとして爆誕したチームcosmosに所属している@k-waragaiです。

今回はタイトル通りElasticsearchのアップグレードに関するお話になります。

Version: 5.6.x から Version: 6.8.x までを約1ヶ月で上げきる事が出来ました。

今回はその1ヶ月の間にどのような事を行いどのような手順で上げていったのかを紹介していきます。

障害なども起こしてしまった為、その時に何があったのかなども赤裸々にお話していきますのでご期待ください。

この記事の後には6.8.x系から7.5.x系にアップグレードしたときのお話が投稿される予定です。 そちらも合わせてお読みください!

目次

はじまり

我々のサービスの1つであるクラウドワークスでは仕事情報ワーカー情報レコメンド情報などをElasticsearchに投入しており、Elasticsearchに強く依存しています。

強く依存だと優しい表現なので率直に申し上げますと、Elasticsearchがなんらかの理由で死ぬとクラウドワークスの仕事検索などが行えなくなる状態になります。

ありがたいことに現在仕事情報は約450万近い数存在しており、その中で特定の仕事情報のみをElasticsearchのドキュメントとして投入しています。

そんなElasticsearchのアップグレード対応を言い渡されたのは3月2日でした。 EOLを迎えてるのはとっくに気づいてましたし、アラートも確かに上げてたけどおおおおおお(ㆁωㆁ;) まさか我々が対応することになるなんて思いもしなかったのが本音です。

EOLを迎えてる時点でお察しだとは思いますが、このプロジェクトが始まった当初はElasticsearch有識者がいない+メンテされてないという状態でした。

故に誰にも聞けないというところがスタートでした。 とはいえElasticsearchを触ったことはあった為なんとかモチベは保てました。

今回はそんなインフラとして重要なElasticsearch5.6のバージョンを6.8.xまで上げたお話になります。

Elasticsearch バージョン6アップグレード対応

Elasticsearch 6.8までのアップグレードでは、@k-waragaiがオーナーとなってプロジェクトを進めました。 その際に行った内容やスケジュールの管理方法や起きた障害などを赤裸々に話します。

STEP1 ~5.6と6.xのドキュメントを読み漁り、対応すべきところを対応する~

どのようなものでもアップグレードする際に避けて通れないのがBreaking Changesです。

これを知るためにまずはElasticsearchのドキュメントを読み漁ることにしました。

我々の利用しているmapping定義を見ながらドキュメントを読んでクリティカルにヒットしそうなのは以下の4つでした。

  • インデックス内の複数タイプが利用不可
  • Booleanタイプに指定可能な値がtrueまたはfalseのみ
  • タイプstringの廃止
  • _allフィールドが利用不可

これらの修正の大変さを☆の5段階評価で表現すると以下のようになります。

  • インデックス内の複数タイプが利用不可[☆]
  • Booleanタイプに指定可能な値がtrueまたはfalseのみ[☆]
  • タイプstringの廃止[☆]
  • _allフィールドが利用不可[☆☆☆☆☆]

理由としては_allフィールドが利用不可以外は対応不要だったためです。

インデックス内の複数タイプが利用不可

5.x系まではindex内に以下のような複数のタイプを設ける事が可能でした。

  "users-index-2020xxxxxxxx": {
    "mappings": {
      employee: {
        "properties": {
          "display_name": { "type" : "keyword" }
        }
      },
      employer: {
        "properties": {
          "display_name": { "type" : "keyword" }
        }
      }
    }
  }

後述でも説明するのですが、Elasticsearchではタイプレス化を目指しており7.x以降ではtypeそのものが非推奨になり8.x系では廃止する流れとなっております。

そのためこのように複数のタイプを設けているIndexが存在する場合は、Indexを分離する必要がありました。

我々クラウドワークスで利用しているElasticsearchのIndexには複数のタイプ利用が無かったためこちらは対応せずに済みました。

なぜタイプレス化を進めているのかというと、Elasticsearchはよくデータベースに例えられる事が多くタイプはテーブルに相当すると考えられています。 データベースではそれぞれのテーブルが独立して動いており、Aテーブルの情報がBテーブルの情報に影響することはありません。 ただし、Elasticsearchではマッピングタイプではこれとは異なる動きをします。基本的には同じmappingの定義をしなければならないのです。 そのためデータベースのように考えられていた事自体が誤りでした。 インデックスとタイプの分割単位が分かりづらいという設計上に悩みが発生したことと、 同じインデックスに共通のフィールドがほとんどなく、様々なエンティティを保存するとデータがまばらになり効率的に圧縮する機能が低下するということが判明した為タイプを削除する流れになったそうです。

一言で言うなれば インデックスに複数タイプが登録できるメリットが無いということでした。

詳しくはこちらを参考にしてください。

Booleanタイプに指定可能な値がtrueまたはfalseのみ

Elasticsearch5.x系ではBooleanタイプにyes/no1/0true/falseなどを入れることが可能でした。まじかよ

これがElasticsearch6.x系では廃止されるようになりました。

これは我々のドキュメント上にはtrue/falseのみだった為問題ありませんでした。

もしyes/noを入れてる箇所があるのであれば

yes == params["result"] とかを入れる必要があるか、そもそものフォーム値やデータ値を見直す必要がありそうです。

タイプstringの廃止

Elasticsearch5.x系で非推奨扱いだったstring型が廃止されました。 これによってkeywordもしくはtextという指定のみになりました。

扱い方の違いとしては、keywordは単一フレーズのもの(例えば「りんご」)の時に採用し、textは文章の時(例えば「りんごは美味しい」)に採用します。

こちらも我々のマッピングでは非推奨時に撤廃していたおかげで何もせずに済みました。

Elasticsearch5系アップグレードしたときの方々へ。 ありがとうございます。ありがとうございます。ありがとうございます。。。。

_allフィールドが利用不可

全てのフィールドのデータを含む_allフィールドが利用できなくなります。 デフォルト無効なのではなく利用不可になります。

今までは以下のような定義をすることが可能でした。

mappings dynamic: 'false', _all: { analyzer: 'custom_ja_analyzer' } do
  indexes :id, type: 'long'
  indexes :title, type: 'keyword', include_in_all: true
  indexes :description, type: 'text', include_in_all: true
end

こうすることによりタイトルなどを含めた_allフィールドというものを用意する事が出来るため横断的なマルチ検索時に役立ちます。

我々はこれをキーワード検索時に利用していました。

具体的にはキーワードパラメータが飛んできた際以下のようなクエリを組み立てます。

bool: {
  must: {
    match_phrase: { 
      _all: { 
        query: keyword_params, 
        analyzer: 'kuromoji'
      } 
    }
  }
}

こうすることにより依頼タイトル依頼詳細ユーザー情報など様々なデータを含む_allフィールドから検索を行い指定されたキーワードにマッチするものを取り出すということが可能になります。

ですが Elasticsearch 6.x からこちらは利用することが出来なくなった為以下のような修正を行いました。

まずは_allが使えない為、ゴソッと記述を消します。

mappings dynamic: 'false' do
  indexes :id, type: 'long'
  indexes :title, type: 'text', include_in_all: true
  indexes :description, type: 'text', include_in_all: true
end

その後include_in_allが居場所を失うためカスタムなindexesの定義を行いました。

mappings dynamic: 'false' do
  indexes :keyword_search_field, {
    type: 'text',
    analyzer: 'custom_ja_analyzer',
  }

  indexes :id, type: 'long'
  indexes :title, type: 'text', include_in_all: true
  indexes :description, type: 'text', include_in_all: true
end

そしてinclude_in_allオプションの代わりにcopy_toオプションを使うようにします。

copy_toオプションは Elasticsearch6.x 以降で推奨される書き方で、検索対象のフィールドを狭めるという目的で使われます。

例えば以下のようなケースです。

タイトルと詳細のようなtext型のフィールドがいた場合、以下のように書くことが出来ます。

{
  "title": {
    "type": "text",
  },
  "description": {
    "type": "text",
  }
}

キーワード検索時にtitleフィールドとdescriptionフィールドからそれぞれ検索をかけるのはとても効率が悪い為copy_toを使って以下のように書き直す事ができます。

{
  "title_and_description": {
    "type": "text",
  },
  "title": {
    "type": "text",
    "copy_to": "title_and_description"
  },
  "description": {
    "type": "text",
    "copy_to": "title_and_description"
  }
}

こうすることにより、title_and_descriptionフィールドを対象に検索をかけるだけで済みます。

公式では以下のように言っています。

A common technique to improve search speed over multiple fields is to copy their values into a single field at index time, and then use this field at search time. This can be automated with the copy-to directive of mappings without having to change the source of documents. (複数のフィールドで検索速度を向上させる一般的な方法はインデックス時に値を単一のフィールドにコピーし、検索時にこのフィールドを使用することです。)

include_in_allに似た性質を持っている事から以下のように組み替えることにしました。

mappings dynamic: 'false' do
  indexes :keyword_search_field, {
    type: 'text',
    analyzer: 'custom_ja_analyzer',
  }

  indexes :id, type: 'long'
  indexes :title, type: 'text', copy_to: 'keyword_search_field'
  indexes :description, type: 'text', copy_to: 'keyword_search_field'
end

公式の全文はこちらになります。

STEP2 ~やることの見通しがついたらスケジュールを立てる~

Breaking Changesを読んでMappingを眺めて、対応するべき点や流れは把握出来ました。

こうすれば行ける!的な自信が付いた段階でやるべきことはスケジュール決めです。

それぞれのやりやすいやり方で良いと思いますが、 私が一番タスクの管理とスケジュールの管理という面でやりやすく周りに伝えやすいと思い続けているやり方を今回は紹介します。

ガントチャートを作成しよう

まずはスプレッドシートを用いてガントチャートを作ります。

これは、チームでのスケジュール管理で利用する他にプロダクト全体に周知する目的で利用します。 現在の進捗具合や全体でどれくらいかかるのかを説明する時に便利です。

1行目に以下を記載します

  • プロジェクト
  • 予定名
  • 参考関連URL(対応したGithubやQiitaなど)
  • タスク(予定を細分化したもの)
  • 月曜日の日付(半年分くらいあると後々楽)

f:id:k-waragai:20200603121447p:plain

そうしましたら肉付けを行っていきます。

今分かっている事を脳内で想像しながら順番を決めて上から予定を埋めていきます。 その予定に合わせてどういうタスクが生まれるのかを細かく書いてきます。目安としては1PR分としています。

これは確定ではないので、作業中に前後したり追加したりしても良いです。 人類が2~3ヶ月先の予定をびっちり確実に漏れなく埋めるなんて事出来ません。少なくとも私にはできません。

そうしましたらそのタスクにどれくらいの時間がかかるのかをP(plan)で埋めていきます。

最後に条件付き書式からPは黄色、Dは緑の書式を適用すると以下のようなシートが出来上がります。

f:id:k-waragai:20200603121549p:plain

タスクを元にTrelloカードを作成しよう

こちらは完全にチームで利用するために使っています。 今週はどんなタスクをしているのか、今何を着手しているのか、今週やるタスクのうち何が終わっているのか、などを可視化するために利用しています。

やることは単純で予定のカードとタスクのカードをガントチャート通りに作っていくだけです。 TrelloにもスプレッドシートにもAPIがあるのでAPIで作成してもありです。

f:id:k-waragai:20200603121746p:plain

あとは今週のTODODoingInReviewWaintingForRelease今日終わらせたなどのレーンを用意して出来る限りの状態管理を行っています。

f:id:k-waragai:20200603122753p:plain

我々cosmosチームでは1スプリント1週で行っている為、毎週月曜日にガントチャートで予定していたカードを今週のTODOレーンに移動させていました。

タスクを消化したら

タスクを消化したらガントチャートの予定末端をD(done)にし参考関連URLにGithub等のリンクを貼ります。

f:id:k-waragai:20200603122823p:plain

STEP3 ~ふりかえる~

Elasticsearchのアップグレードが落ち着いたタイミングで振り返りを行いましたのでGoodとBadでまとめていきます。

Good

Elasticsearch 6系のアップグレードでは、遅れも早く進む事もなくスケジュールどおりに進める事が出来ました。 むしろ予定では調査含めて2,3ヶ月かかる見込みだった為、かなり短縮できてVery Goodでした。 Breaking Changesを読んで実コードを読んで、何が必要なのか、どう修正すればいいのかなどを明確にできたのが大きかったです。

Bad

Badとしては2回障害を起こしていました。

1回目はmappingの定義だけを変更して、実検索対象の変更を行わなかったことでした。

これの影響としては仕事検索やワーカー検索でのキーワード検索が行えないというものでした。 PRは作っていた為すぐに修正をデプロイし問題の解決を急ぎました。

なぜ一緒にリリースをしなかったのかというと、copy_toに置き換えたmapping変更のデプロイ後はindexの再構築が必要でした。

この時にリリースをしてしまうと_allフィールドはまだある状態でcopy_toの検索フィールドはまだないという状態になっています。

aliasの張替えを行う瞬間にcopy_toに置き換えた検索フィールドに置き換えを行う必要があり、かなりシビアな条件となっていました。

これ以降の反省としてGithubのPRに デプロイ手順などを記載 するようにしました。

2回目の障害はGemアップグレードの際でした。 elasticsearch-railsの方でtypeの修正がありデフォルトで_docというものが使われるようになりました。 我々のindexではjob_offerというtypeを使っており、typeの指定を行って検索をかけています。

elasticsearch-railsのGemをアップグレードした際、これを把握していたにも関わらずリリース直前に見落としてしまい検索結果が0件になるという障害が起きてしましました。

直前にローカルやStagingと呼ばれる仮本番サーバーでの検証も行っていたのですが、この際にindexの再構築をしてしまっており、問題無いと判断してしまったのが原因でした。

実際のデプロイではindexの再構築は伴わなかった為障害となってしまいました。

この反省として確認手順をもっと厳密に書くようにしました。

具体的にはcheckout後からのログを貼り付けたりしました。

最後に

良い点としては、プロジェクトの進め方などの基盤を序盤でしっかり出来たことだと思います。 (いわゆるレールを敷いてく作業を速い段階で終わらす事ができたこと)

また、序盤の方で修正方針で悩んでいる時にチームリーダである山本さんがラバーダッキングの相手になってくれたり、深夜メンテのチェックなどを行ってくれたのはかなり救いになりました。

ラバーダッキングとは、何らかの作業中に直面した問題について、ゴム製のアヒルのおもちゃ(ラバーダック: Rubber Duck)に相談することで、有効な解決策を探ろうとする試み。 https://enpedia.rxy.jp/wiki/ラバーダッキング

自分は独り言をしながら実装をしていくタイプなのですが、相槌や問題点の指摘がある方がやはり思考って研ぎ澄まされるんだなーという気づきを得ました。 バックトラッキングとかがあると肯定されてる感が強まって自信もつくんですよね(ㆁωㆁ*)

あとはElasticsearchに結構詳しくなれたと思います! elasticsearch-railsを使った内部の実装とかも全く意味分からなかったんですがだいぶ詳しくなるようになりリファクタリングなども行いました。

今回の詳細には書かなかったのですが、レコメンドで使っている別のElasticsearchインスタンスが存在しており、そちらはもう完全に無人化しており環境構築さえ困難な状態になっていたのでDocker化をしたりElasticsearch5以降メンテされていないelasticsearch-embulkというGemを使わずにbulk APIを使うようにリファクタリングしたりしました。 周りの環境の整備をまとめて行えたので良かったかなと思います。

悪い点としては、スピードに乗り過ぎて細かいところの見落としが多かったところかなと思います。 立ち止まりつつ冷静になれば気づけていた問題などもあったので、次回以降はそこに気をつけたいと思います。

という感じのアップグレード話でした!次回 Elasticsearch 7アップグレードのお話ではオーナーが異なるのでそちらも期待していてください。

・ω・ノシ

© 2016 CrowdWorks, Inc., All rights reserved.