こんにちは、エンジニアのクリモトです。
ここ最近はアメリカ株の調子が悪く、積み立てNISAが積み下がっているのを見て落ち込んでいますが、皆さんのNISAはご無事でしょうか?
さて、今回の記事では、Rails × slimの構成で記述されていたaumoの一部ページを、GraphQL on Rails × Nuxtの構成に移行した際の話をします。
目次
移行の経緯
aumoでは「東京のオススメ人気スポット30」といった、「あるエリアの人気スポット30選を紹介するページ」(以降では「一覧ページ」と表記)を以下のように4つのドメインを使って運営しています。
- https://gourmet.aumo.jp/prefectures/13
- https://leisure.aumo.jp/prefectures/13
- https://travel.aumo.jp/prefectures/13
- https://aumo.jp/prefectures/13
これら4つのドメインの一覧ページは、どれもほぼ同じデザインであり、gourmet.aumo.jp, leisure.aumo.jp, travel.aumo.jpの3つのドメインは、GraphQL on Rails × Nuxtの構成の実装を共有しています。
一方aumo.jpドメインの一覧ページだけはRails × slimの構成で作られているため、他3つのドメインとは実装を共有していません。
そのためgourmet.aumo.jp, leisure.aumo.jp, travel.aumo.jp, aumo.jpの4つのドメイン全てに同じ機能追加を行おうとすると、gourmet.aumo.jp, leisure.aumo.jp, travel.aumo.jpの3つのドメインのみに機能追加をする場合に比べて約2倍の工数が発生する状況になっていました。
この技術的負債を解消するために、今回aumo.jpの一覧ページの構成をRails × slimからGraphQL on Rails × Nuxtの構成に移行し、他3つのドメインと実装の共有を行うという決断に至りました。
aumoでは業務時間の1~2割程を割いて自分が興味のある改善活動等を行って良いことになっているので、今回はその時間を使って移行を進めます。
移行前システム概要
移行前のシステムは以下の図のようなイメージです。aumoでは基本的ににAWS上にインフラを構築しており、aumo.jpドメインとその他のgourmet.aumo.jp, leisure.aumo.jp, travel.aumo.jpドメインで使うEC2インスタンスやALBは完全に分かれています。
移行後システム概要
移行後のシステムは以下の図のようなイメージです。aumo.jpドメインの一覧ページへのアクセスのみを、ALBのリスナールールの設定を編集して、Nuxtサーバーに転送しています。
開発の流れ
開発は、まずコードを移植した後インフラ構成の変更を行うという順番で進めていきます。
デザインや表示ロジックは基本的にはaumo.jpドメインも、それ以外のドメインもほぼ一緒なので使いまわせるコードが多く、コードの移植にはそれほど時間はかかりませんでした。
とはいえ表示ロジックが異なる箇所も当然存在しており、その場合は基本的にはaumo.jpではない方のドメインにロジックを合わせる方針でコードの書き換えを行いました。また、もはや不要だったり、無駄に複雑になっているロジックもあったので、それらのコードはPMさんに確認をとった後ついでに削除しました。
コードの実装が終わった後は、qa環境でALBの転送設定がうまく機能するかの確認を行いました。既存のNuxtサーバーが動いてるEC2上に新たにaumo.jpから転送されてきたリクエストを捌くサーバーを1つ追加する形にしたところ、問題なく動くことが確認されました。
リリース時の苦労
コードの移行とqa環境での動作確認はすんなり終わったのですが、本番環境へのリリースで何度かつまづきました。
リリース挑戦1回目
1回目のリリースではリリース直後に500エラーが頻発する結果となりました。
原因を調査した結果、aumoではElasticsearchを使っているのですが、リリース前から負荷が高めだったため、今回の移行のタイミングでElasticsearchの使用量が少し増えたことで、CPUが限界を迎えたのが理由だと分かりました。
この問題はElasticsearchのspecを一段上げたり、ElasticsearchのインスタンスタイプをStorage OptimizedなものからCPU Optimizedなもに変えることで解消されました。
リリース挑戦2~4回目
Elasticsearchのspecを上げたことで万事解決かと思われたのですが、その後のリリースでは、リリース後の最初の数分間はレスポンスが安定しているものの、そこから一気にレスポンスが遅くなるという事象に襲われました。
この時は以下のような対応をとってみました。
- 移行時に書いたコードはほとんどパフォーマンスチューニングをしておらず、他の部分のコードと比べると処理速度が数倍遅かったので、N+1を消すなどして改善
- リクエストを転送する分、今までよりもサーバーへのアクセスが増えることになるのでサーバーの台数を増やしてみる
しかしどの対策を取っても、レスポンスが悪化する現象がなくなることはありませんでした。
リリース挑戦5回目
何をやっても効果がないので、リクエストごとのレスポンスタイムのログを取れるようにした上で、再度リリースして様子を見てみました。するとエリア番号が後ろのページ(例 https://aumo.jp/areas/1630)へのレスポンス速度がエリア番号が前のページのページ(例 https://aumo.jp/areas/1)よりも数倍遅く、それにつられて徐々に全体のレスポンス速度が悪化していくことが分かりました。原因を調査してみたところ、今見ているエリアが属する都道府県や地域を取得する以下のようなコード(10^3回程度のループ)が、複数箇所で記述されていたのがボトルネックになっていたようでした。
currentAreaRegion () {
if (this.targetArea) {
return this.allRegions.find((region) => region.prefectures.some((pref) => pref.areas.some((area) => Number(area.id) === Number(this.targetArea))))
}
return null
},
そこで、これらのデータはvueのstateで管理し、呼び出される回数を1度だけにするという対処を取りました。
この修正を入れたコードを入れて再度デプロイしたところ、無事エラーが出なくなり移行プロジェクトを無事終わらせることができました。
*余談ですが、nuxtはSSR時にはcomputedプロパティの値がキャッシュされないようです(参考)。今回問題があったコードも元々はcomputedに入れてあり、キャッシュが効くのでコード全体で3回程度しか実行されないだろうと思っていたら、実際はcomputedの値にアクセスされるたびにループ処理が走っていたため、全体で見ると数十回ほど実行されてしまっていたようです。
学んだこと
困ったら生のログを見る
昔から言われていることですが、やはり生のログは大事だなと今回実感しました。ALBのメトリクスを見るだけでは分からない情報が手に入ります。またログを見るだけでなく、実際にページに自分でアクセスしてみることも大事だなと感じていて、これをしなければリリース後の最初の数分だけはレスポンスが安定しているといった事実には中々気づけなかったのではないかと思います。
やり切ることの大事さ
リリースに数回失敗した時点で、移行プロジェクトが打ち切りになりそうな雰囲気があったのですが、なんとか本番運用まで辿り着いたことで、自信がつきましたし、インフラ面の知見や原因不明のエラーに対処する力も格段に身についたと感じます。完璧でなくても良いからとにかくリリースすることはこれからも大事にしていきたいですね。
最後に
コード移行プロジェクトはこれにて終了ですが、aumoには他にも技術的負債がチラホラ見受けらるのでまた時間を見つけて解消していきたいと考えています。
最後になりますが、aumoでは技術的負債を一緒に解消してくれるメンバーを募集しています!
ご興味がある方は是非、弊社採用ページ からご応募ください!