2024.12.19
システムの穴を運用でカバーしようとしてミス多発… バグが大量発生、決算が合わない状態から業務効率化を実現するまで
Pros and Cons of SSR and JAMStack(全1記事)
リンクをコピー
記事をブックマーク
_sskyu氏:では、「Pros and Cons of SSR and JAMstack」と題して発表させていただきます。
私はSasaki Yutakaと申します。メルペイには一昨年の9月1日に入社しまして、フロントエンドエンジニアをやっております。
今日話す内容ですが、去年の8月からキャンペーンのマイクロサービスの高速化について調査をしてきて、先月、本番リリースを一部したので、その過程というか、そこで得られた知見について発表させていただきます。
アジェンダはこんな感じです。
まずメルペイキャンペーンとは何なのか? こういった1枚のLPを作っていて、だいたい1ヶ月に1〜2本ずつ開発しています。
これはキャンペーンマイクロサービスに限らず、フロントエンドチームで運用しているマイクロサービスで使っている技術スタックです。Node.jsの上でNuxt.jsを動かしていて、リバースプロキシにNginxを使っていて、CI/CDにCircleCIとSpinnakerを使っていて、それをKubernetes環境で動かしています。
ここでキャンペーンマイクロサービスの問題があって、SSRのパフォーマンスが出ないという問題がありました。1Podあたり20rpsしか捌けないというのは、これは2019年の4月時点だったんですけれども、Podを横にどんどんスケールアウトさせて、40〜80Podデプロイしてキャンペーンの負荷を耐えている状態でした。とても単純なLPを配信したいだけなのに、すごくコスパが悪い状況になっていました。
そこで、SSRのパフォーマンスについて8月に調査していったのですが、まずはいろいろなパターンで負荷試験をしてみました。
パターンは5個あります。1つ目は簡単な素のNuxt.jsアプリケーション。次にコンテンツの量が少ないキャンペーン。その次にコンテンツの量が多いキャンペーン。campaign BをNuxt.jsのclient-only tagで囲んでみるパターン。これはSSRはしないでSPAモードみたいに動きます。最後に、ファーストビューはSSRしますが、それ以降スクロールで隠れるような部分はclient-only tagで囲むというのをやってみました。
こちらが結果をグラフにしたものです。
当然のように、素のNuxt.jsアプリケーションが一番rpsが出ています。興味深いのが、コンテンツの量によってrpsに差が出たことです。コンテンツの量が多くなるほどrpsが出ていなくて、SPAモードにするとSSRしないので当然のようにrpsは上がって、5のパターンだと中間ぐらいになるというグラフになっています。
ここからわかったことは、SSRするコンポーネント数に比例してパフォーマンスが落ちることがわかりました。client-only tagを使うと、SSRしない範囲を決めて負荷を減らすことができることがわかりました。なので、ファーストビューだけをSSRして、スクロールで見ない部分はclient-only tagで囲むのが良さそうかなと思います。
一方で、このアプローチには限界が見えています。client-only tagを使うことで数十パーセントの改善が見込めるのですが、飛躍的な改善は難しそうなことが分かります。
そこで、nuxt generateというまったく別のアプローチを試してみることにしました。以降、nuxt generate=JAMstackという意味合いで使っていきます。
そもそもNuxt.jsをご存じない方もいらっしゃるかもしれないので軽く説明をすると、srcのpagesというディレクトリの下に.vueファイルを置くと、それがRouteComponentと呼ばれるものになって、このRouteComponentはそのままディレクトリ構造のままURLに露出するようなかたちで使うことができます。例えばsrc/pages/foo.vueであったら/fooでアクセスできます。
nuxt generateは何をやっているかというと、SSR時に出力しているRouteComponentのhtmlをあらかじめ出力しておいて、それを読み込むとクライアントサイドで、rehydrationと呼ばれる、もう1回SSRと同じ処理が実行される流れです。そのあたりのことは、https://ja.nuxtjs.org/guide/に詳しく書いてあります。
「すすメルペイキャンペーン」を例として持ってきました。ここでは4枚出していますが、これがユーザーエージェントを見て、PC、iOS、Android、メルカリアプリ上のWebViewで出し分けを行っています。
ここで問題になるのが、nuxt generate時にユーザーエージェントが取れないという問題です。
nuxt generateをやっている箇所というのは、下の図で言うと、例えばmasterブランチにdiffを取り込んだときに、CircleCIがそれをhookして、nuxt generateを走らせます。それをrsyncでGCSにアップロードしてクライアントからアクセスするという流れです。
nuxt generateするときはreqやresという変数が使えません。「req.headers[‘user-agent’’]」みたいなOptional Chaningをしないで書いていると、ここで実行エラーになって、nuxt generate時にエラーが出ます。回避策としては、常に「ユーザーエージェントがある」と想定しないコードを書く必要があります。
次にv-ifです。ここにサンプルコードがありますが、v-ifの中でユーザーエージェントをもとに判定するような評価式を書くと、SSRだとユーザーエージェントが取れないので常にこのothersというのが出力されるのですが、クライアントサイドでrehydrationが起こるとユーザーエージェントが取れるのでこのisAndroidかiOSかどっちらかに入って、サーバサイドで出力したhtmlとクライアントサイドで計算したhtmlが不整合を起こしてDOMエラーが出るみたいなことが起こりました。これを解決するためには、v-ifをv-showに変えると一旦は収まるのがわかりました。
次に、キャンペーンは並行で開発されるものであるので、一部のキャンペーンは本番に出したくないという機能を作る必要がありました。
もともとはNuxt.jsのmiddlewareで実装されていて、この実装を見るとmiddlewareをRouteComponentで読み込んで、IncomingなRequestが来るたびに、都度判断して、404ページを返すようになっています。
これをそのままnuxt generateすると、generateした結果が404ページを出力するみたいになってしまって、これはあまり意図しない動きだったので使えませんでした。
そこでnuxt generate用のモジュールを独自で作りました。やりたいこととしては、本番にキャンペーンを出ないようにするということだったので、nuxt.configで除外したいキャンペーン一覧のリストを正規表現で列挙して、次にnuxt generate時のbuild:extendRoutesをhookしてRouting情報から除外します。このままだと画像や静的なassetは本番に出る可能性があったので、generate:doneをhookしてassetsの中から正規表現にマッチする画像名を削除するというコードを書きました。
あとはいくつかのnuxt lifecycleがブラウザ上で使えないという問題もあります。middlewareやfetchはnuxt generate時に実行されるので、これはCircleCI上で実行されます。赤枠で書いてある部分ですね。これらはブラウザ上で実行されないことに注意が必要です。
キャンペーンにおけるfetchで使うAPIは、ユーザーのステータスを取得するAPIがほとんどで、これをCircleCI上から叩くと、何をもってユーザーを識別するかが判断できないので、これはクライアントサイドにJSがロードされてからAPIを叩くようにソースコードを編集しました。
JAMstackの開発とデプロイフローも参考までに書きましたが、ローカルでは普段通りnuxt dev(universalモード)で開発します。
メルペイでは「dev」「test」「production」という3つの環境があり、dev環境はfeatureを気軽に開発するための環境で、testは、QAエンジニアと一緒になってクオリティを担保していくステージングみたいな環境です。
branchをpushすると、dev用のGCSバケットにnuxt generateしたものをデプロイして、Pull Requestのコードレビュー前に確認できる環境を用意してもらいました。
キャッシュレスキャンペーンをJAMstack環境に最初に移行しようとしました。キャッシュレスキャンペーンはすごくシンプルなLPで、APIを叩く箇所はありません。キャンペーン期間が2019年の10月〜2020年6月末と今も開催しているキャンペーンで、「これを先に移行しないことにはどうしようもないな」という感じで移行することにしました。
ほかに動いているキャンペーンは長くても1〜2ヶ月で期限が切れてしまうので、今のマイクロサービス上で動かして、終わったらそのまま終了にしました。このキャッシュレスキャンペーンを先行して移行して、運用が問題なさそうであればそのまま新規開発はJAMstack環境で行う、としました。
移行に関して3つのステップ踏んでいます。まずJAMstack環境を作る。ドメインを決めて、マイクロサービス版とJAMstack版のキャッシュレスキャンペーンを並行で2箇所で動かすようにしました。当然ながら本番に出ないようにIP制限をかけて、社内からのみアクセス可能にしています。
2つ目に、本番環境にキャッシュレスキャンペーンをデプロイする。これは主にデプロイスクリプトの検証とマイクロサービス版のキャッシュレスキャンペーンとのコンテンツの差異がないことをQAにて保証するためにやりました。
最後に本番の有効化ですが、IP制限を解除して、メルカリアプリの「メルペイ」タブのキャッシュレスキャンペーンのバナーのURLを変更して、JAMstack版のキャッシュレスキャンペーンにリクエストが流れるようにしました。
これが全体のアーキテクチャです。マイクロサービス版は、CIでnuxt buildを走らせると、Node.jsとNginxのDockerイメージができてGCRにpushされて、Kubernetes上でサービスを動かすようになっています。htmlはNode.jsが返すんですが、それ以外のstaticなassetsのJSとかCSSとかはFastlyでキャッシュされているような構成になっています。
JAMstack版は、CIでnuxt generateを走らせたら、GCSにアップロードされて、htmlも事前にプリレンダリングされて、リクエストは全てFastlyがハンドルするようなかたちです。クライアントにhtmlやサブセットがロードされた後に実行されるAPIリクエストはMerpay Gatewayを経由してKubernetes環境で動いているMerpay APIに向けるという構成になっています。
この2つの環境でキャッシュレスキャンペーンを動かしている状態です。
どうやってマイクロサービス版からJAMstack版を見分けるかということで、最初はドメインを別にしようか考えていたんですが、議論して「ドメインは同じでいこう」となりまして、pathの先頭に「s/」がついているほうをJAMstack版にするという結論に至りました。
このsは「static」のsです。今は移行期間で、sがついていることでURL的に違和感があるので、そのうち消す予定です。
別ドメインでいこうと思っていたのですが、同じドメインにしたことによって、nuxt core fileや画像が参照できないという問題が起きました。ですが、これはnuxt.configのrouter.baseに/s/を設定すると、htmlの
内にこの
マイグレーションが完了して、SSRとJAMstack版のキャッシュレスキャンペーンは、コンテンツに差異がなく、今のところ運用できています。
最後にSSRとJAMstackの差についてです。この図は「Rendering on the Web」というブログ記事からの図です。今回細かくパフォーマンス面で、例えば「TTIがFCPよりすごくかかるよ」と書いてありますが、そういう説明はしませんでした。というのも、このブログを読むとパフォーマンスに関して網羅的に関して書いてあるので、ぜひ読んでみてください。
今日ずっと話していたSSRというのは、この表の真ん中の「SSR with (Re)hydration」で、JAMstackと言っていたnuxt generateに関しては「CSR with Prerendering」です。
最後に、このタスクを通してProsとConsを個人的にまとめます。SSRはコードで表現できる自由度が高いのがとてもいいです。また、JAMstackではNode.js周りの機能を追加することはできないので、どうしても追加したい場合はSSRでやるしかないと思いました。
一方で、SSRは運用コストがとても高くて、サーバを運用しているためオンコール体勢が必要になってしまいます。また、メルペイのキャンペーンの場合はrpsが出ないので、スケールアウトで対応せざるを得ません。つまり、金銭面ですごくコストが高くなります。
JAMstackのProsとConsなんですが、一番は運用コストが低いことです。当初のコスパが悪いIssueに関して言うと、マイクロサービスと比べて10分の1くらいに削減できそうという試算が出ました。あと、JAMstackは静的なassetをただホスティングするだけなのでオンコール体勢が不要になります。
一方で、アプリケーションによっては向き・不向きがあります。LPのようなペライチのサイトであったり、ブログやLPとかには適しているんですけれども、管理画面のような、動的であったりページ数が多かったりAPIをバンバン叩くようなものには不向きかなと思います。
それと制約がSSRに比べて多く、SSRと同等の表現をするときに工夫が必要になって、これから知見が貯まっていくかもしれませんが、けっこう苦労するかもしれません。
キャンペーンの場合は、ユーザー情報を叩くAPIがSSRで叩くとおかしなことになるので、クライアント側のJS実行タイミングでやる必要がありました。
発表は以上になります。本日はありがとうございました。
(会場拍手)
関連タグ:
2024.12.20
日本の約10倍がん患者が殺到し、病院はキャパオーバー ジャパンハートが描く医療の未来と、カンボジアに新病院を作る理由
2024.12.19
12万通りの「資格の組み合わせ」の中で厳選された60の項目 532の資格を持つ林雄次氏の新刊『資格のかけ算』の見所
2024.12.16
32歳で成績最下位から1年でトップ営業になれた理由 売るテクニックよりも大事な「あり方」
2023.03.21
民間宇宙開発で高まる「飛行機とロケットの衝突」の危機...どうやって回避する?
PR | 2024.12.20
モンスター化したExcelが、ある日突然崩壊 昭和のガス工事会社を生まれ変わらせた、起死回生のノーコード活用術
2024.12.12
会議で発言しやすくなる「心理的安全性」を高めるには ファシリテーションがうまい人の3つの条件
2024.12.18
「社長以外みんな儲かる給与設計」にした理由 経営者たちが語る、優秀な人材集め・会社を発展させるためのヒント
2024.12.17
面接で「後輩を指導できなさそう」と思われる人の伝え方 歳を重ねるほど重視される経験の「ノウハウ化」
2024.12.13
ファシリテーターは「しゃべらないほうがいい」理由 入山章栄氏が語る、心理的安全性の高い場を作るポイント
2024.12.10
メールのラリー回数でわかる「評価されない人」の特徴 職場での評価を下げる行動5選
Climbers Startup JAPAN EXPO 2024 - 秋 -
2024.11.20 - 2024.11.21
『主体的なキャリア形成』を考える~資格のかけ算について〜
2024.12.07 - 2024.12.07
Startup CTO of the year 2024
2024.11.19 - 2024.11.19
社員の力を引き出す経営戦略〜ひとり一人が自ら成長する組織づくり〜
2024.11.20 - 2024.11.20
「確率思考」で未来を見通す 事業を成功に導く意思決定 ~エビデンス・ベースド・マーケティング思考の調査分析で事業に有効な予測手法とは~
2024.11.05 - 2024.11.05