こんにちはこんにちは。技術部クックパッドサービス基盤グループの id:riseshiaです。
本記事では直前の記事で提案された新しい検索システム(以下、 solr-hako と呼びます)を利用し、レシピサービスの検索インフラの切り替えた話をします。 solr-hako の設計を直接参照する内容はありませんが、それを前提においた移行作業ですのでそちらの記事を先に読むことをおすすめします。
インフラ構成の変化
まずインフラ構成の変化ををみておきましょう。
今まではこのようなインフラ構成でした。特徴としては、 search-cache というキャッシュサーバ(Varnish)が手前にあることくらいでしょうか。今回、 solr-hako を利用することで以下のような感じになりました。
しれっとキャッシュレイヤーである Varnish がなくなったことがわかります。これに関しては後ほど述べます。 では、このインフラの切り替えのためにどういう作業をしてきたのかを話していきたいと思います。
取り組んだ施策
新しい検索システムにマイグレーションするために次のような施策を行いました。
- solr-hako という新しい仕組みに沿った実装
- Solr バージョンアップによる影響調査 & 対策
- 負荷実験
- 試験運転
- 切り替え
solr-hako の設計に関しては同僚の id:koba789が書いた記事で紹介しているのでそちらを読んでください。実装に関してはひたすら必要な設定を書いたり、権限を付与したり、 kuroko2にジョブ定義を作成したりのようなものがいっぱいなので割愛し、本記事では残りの施策に関して説明します。
Solr バージョンアップによる影響調査 & 対策
あらゆるフレームワークに対して、バージョンアップの正攻法はまずリリースノートおよびチェンジログを調べるところから始まるのかなと思います。
ですが今回の場合だと、 Solr 4.9 から Solr 8.6 までの変更履歴を全て調べることになるので流石に無理です。 ではどうやって変更を調べるのか。今回は実クエリを利用することにしました。 実際のクエリを数日分抽出し、 Solr 4.9 と Solr 8.6 に投げてそのレスポンスの差分を取りました。そこで見つかった差分から関連の変更を逆引きし、必要な対策をするという流れです。
影響が大きかったのは以下の2つです。
- LatLonPointSpatialField の導入による geodist の一部クエリオプションの強制
- クエリノーマライズの廃止
前者は一応クエリの書き方を変えるだけでよかったのですが、後者は検索順番への影響がありました。 流れが大きく変わってしまうので詳細は省きますが、検索サービスの責任を持つサービスチームにお願いし、いくつかの指標を選定した上で検索の品質評価を行いました。結論としては品質に悪影響してない、むしろ少し改善した可能性がありそう?ということになったのでこのまま切り替え作業をやっていくことになりました。 これに関しては機会があれば、別の記事を通して紹介できるかなと思います。
性能実験によるパラメータ・チューニング
影響範囲が分かったし、検索順の変更に対する影響調査を依頼したところで、次は性能実験です。 Solr のバージョンが大幅に変わるので、 Solr の設定及びリソース要求を見直す必要があります。ぱっと思い浮かぶだけでも各種 Solr のキャッシュ設定、スキーマの設定、JVM の設定、 solr-hako で動かすわけですから、ECS のタスクのリソース割り当てなどもあります。 それにオートスケーリングも設定していくので負荷状況による影響なども把握したい。
これらの設定値をいい感じの組み合わせにして一つ一つ試しながら良さそうな組み合わせを探す必要があります。とはいえ、丁寧に新しい組み合わせを試すたび設定を更新して Docker image を作って〜というのは疲れるし、やってられません。 そこで Solr の Config APIに目をつけました。 Config API を利用すると Solr の設定を REST-like API で取得したり、更新したりすることができます。これを利用して実験をあるほど自動化できるツールを作ることにしました。
solr-hako-load-tester
設計目標としては次のようなことを上げました。
- 多様な Solr のパラメータを試せること
- 手軽に設定を変更できること
- 試したい設定をキューに詰め込んで順番に実行できるようにすること
一方でこのツールは今後 solr のバージョンアップの時くらいにしか使われないと予想されるので、実装コストに見合わない機能は目標外にしました。
- Solr の外のパラメーター(e.g. ECS タスクごとの CPU や JVM の設定)は考慮しない
- これらは比較的変更が少ない値であり、かつこれらの設定を動的に変更可能な設計にするのは利点に比べて実装コストが高い
- 積極的なメトリクスの収集
- 負荷をかけるツール(k6)、 Prometheus、 CloudWatch があれば詳細なデータが取れるのでツールではデータ収集を頑張らなくても問題ない
- 負荷テストの対象の状態管理
- 費用はやや高くなるが実験の間は負荷テスト対象(ECS Service)を常駐にすることでエンドポイントを毎回用意する手間を減らせるし、メトリックス収集も楽になる
結果、どうなったかというと、S3 に設定セットをキューイングし、 kuroko2の定期実行でそれを消費しながら実験を行う仕組みが誕生しました。
この図からありそうな質問点をあげていくとこんな感じかなと思います。
- 負荷をかけるときに使ったクエリはピークタイムのログをサンプリングしたものです
- 実験ツールとして k6 を選択したのはクエリログをリプレイするという観点で非常にシンプルで扱いやすかったからです
- 結果はどうみるかというと kuroko2 のジョブ実行ログ、 Prometheus、 CloudWatch の監視結果から確認できるのでそちらを参考にしました。
- 直前の実験の影響を受けないように実験の直前には ECS タスクの入れ替えをしています
実験結果
適切そうな設定を決めることができました。 そしてどういう設定にしても Solr 4.9 より速い、現状より少ないリソースでも十分なスループットを提供できることが判明したので、急遽このタイミングで Varnish のキャッシュレイヤーを捨てるという意思決定が行われました。
試験運転
使えそうな設定が出来上がったところで実ワークロードでも問題なく動作するかを確認するために試験運転をすることにしました。 試験運転をする方法はいくつかあるかなと思うのですが、今回は Traffic shadowing という手法を選びました。これはリクエストをコピーし、テスト目的でサービスインしてない別の upstream に送る手法です。 もう一つの候補として、一部のユーザに対してロールアウトしてみるという選択肢もありましたが、検索サービスである voyager は検索をリクエストしているユーザが誰なのかわからないので、その情報を何らかの方式で渡すなり、新しい API をはやすなり、新しい Solr 用の voyager を用意するかなりいくつのサービスを跨る対応が必要でコストが増えるし、予想される設計もあまりうれしくないものでした。
一方 Traffic shadowing の場合、2つの利点があります。
- ユーザに影響を出さない
- 検索品質の検証は別途やっている、今回の実験の目的は実ワークロードでも安定して動くのかを確認するためで、必ずしもユーザに出す必要はないし、むしろ出さなくていいならそっちがいい
- すべてのリクエストを流せるので実ワークロードを完全再現できる
ということで Traffic shadowing を試してみることにしました。
Envoy と request_mirror_policy
クックパッドは Service mesh を導入しており、その Data plane として Envoy を採用しています。そして Envoy は Traffic shadowing に利用可能な request_mirror_policyという設定を提供しています。 これはあるクラスタへのリクエストをコピーして別のクラスタに送る機能で、コピーして送ったリクエストのレスポンスはそのまま捨てられます(メトリクスは残ります)。設定を少しいじるだけなので 実際使ってみた感じだと以下のような感じでした。
- envoy による CPU 負荷はやや増えるが、そこまで目立つ変化ではない
- Host ヘッダーに
-shadow
という suffix をくっつけてくるので、 Host ヘッダーを利用する処理が必要な場合、注意が必要
それ以外の気になる点はなく、快適に利用できました。
実験結果 & 対策
予想外のスパイクによるキャパシティー不足
実環境だと利用サービス側のキャッシュパージやバッチ実行などによりリクエストのスパイクが発生するわけですが、これが予想以上に影響が大きく、観測した範囲だと直前の rps より最大2倍くらいに跳ねたり、レイテンシーが不安定になる現象が観測されました。対策としてはオートスケーリングの閾値をやや下げ、もう少し余裕をもたせることでレイテンシーを安定させることができました。
Cold start 問題
Solr は起動してからキャッシュが温まるまではリクエスト処理速度が遅く、いわゆる暖気が必要ということが知られているのですが、性能実験の時はあまり意味のある遅延がみられませんでした。 solr-hako は tmpfs を利用し、メモリーにインデックスを乗せることを推奨しており、実設計でもそれに従ってインデックスストレージとして tmpfs を採用していました。ですのでこれが tmpfs の力なのか?!と思っていたのですが、そんなことはなく実ワークロードだと普通に Cold start 問題が目立つようになってました。後になって思うと、性能実験にはピークタイムのクエリをサンプリングしていたのでクエリのパターンが偏っていたのかもしれません。 対策として2つ考えられて投入前にクエリをいくつか投げるとこで暖気を行う方法と ELB の Slow start modeを利用する方法がありました。 前者だと送られてくるクエリの傾向に合わせて暖気用クエリをメンテするコストがあるので、徐々にリクエストを流すことでレイテンシーの劣化を抑えることができる ELB の Slow start mode を有効にすることにしました。
切り替え
Traffic shadowing を一定期間運用したことにより、これは大丈夫だなという確信を得られたので実際に切り替えることにしました。ロールアウト作業は検索結果の順序の変化と、利用サービス側のキャッシュ事情が混ざるとややこしいことになりかねないので徐々に切り替えるのではなく一気に展開することにしました。試験運用で時間帯による要求キャパシティーも完全に把握できていたので特に懸念点もなく無事切り替えることができました。
と思っていたら、その数日後に移行漏れのバッチが発覚したのはまた別の話です 😇 移行後もしばらくの間、運用上の理由でロールバックする可能性を考慮し古い Solr サーバを残しておきインデックスも更新していたのがここで役に立ちました。備えあれば憂いなし。
結果
コスト節約
実運用が始まったのが 9月中旬、古いインフラのリソースを片付けたのが10月初です。こちらは Cost Explorer からみた検索インフラのコストの推移ですが、新しいインフラコストが 1/3 くらいになったことがわかります。これは2つの理由があります。 今までの仕組みではそもそも運用しやすい形に実現するのが難しいのでオートスケーリングが有効ではなかったのが一つで、もう一つは Solr のバージョンアップによりそもそも Solr の性能が向上したことがあります。ピークタイムでも以前に比べて半分以下のリソースで運用できているんですね、これが。すごい。
レイテンシーの改善
レイテンシーも相当改善されました。これも2つの理由があります。 わかりやすい理由は Solr の性能向上ですね。図の2つ目の崖がそれです。途中でも触れていましたが、 Varnish を介さなくなったのにも関わらず早くなったのが印象的です。 左側の崖は差分検証中に見つけた激重クエリが無駄な subquery を発行していることに気づいてそれを解消した結果発生したものです。
その他
その他の細かい改善点というと以下のようなものがありました。
- Solr 起因の staging 環境でのエラーがほぼなくなった
- staging 環境で利用してた Solr は本番用に比べると大変小さく、リクエスト数が急増すると悲鳴を上げがちだったのですが、今回の Solr の性能向上により安定するようになりました
- 今まではインデックスの更新直後の負荷を恐れてピークタイムでのインデックス更新を避けていたが、そうする必要がなくなりました
- 普通の ECS サービスになったことによりスキーマの更新と運用がしやすくなりました
まとめ
今回の検索インフラ改善作業の流れ、その時利用した技術などに関する説明は以上になります。このような面白いお仕事がいっぱいあるので興味があるエンジニアの方はぜひご連絡ください! https://info.cookpad.com/careers/jobs/