1
/
5

【TECH BLOG】ZOZOTOWNのWebホーム画面をNext.jsでリプレイスして得た知見

はじめに

ZOZOTOWN開発本部の武井と申します。ZOZOTOWNのフロントエンドリプレイスプロジェクトを主に担当しております。ZOZO DEVELOPERS BLOG でも「ZOZOのリプレイスプロジェクトで得られる唯一無二の経験。大規模サービスを進化させるやりがいとは」というインタビュー記事を掲載しておりますので、もしよろしければこちらも併せてご覧ください。

さて、本題です。現在ZOZOTOWNではオンプレミスかつ、モノリスだった既存システムをマイクロサービスAPIに責務を分割したり、インフラをクラウドに移行したりしています。しかし、いわゆるWebのUIを構築するためのシステムは現在も既存システムに新機能開発や機能改修を行なっており、リプレイスに着手できていませんでした。

そこで、まずホーム画面から段階的にリプレイスすべく設計・開発を昨年から行ない、無事リリースできました。ZOZOTOWNのソースファイルを見るとNext.jsで提供されていることがフロントエンドエンジニアの方にはお分かりいただけると思います。

本記事では、ホーム画面のリプレイスをどのようなシステム構成で実現したのかの事例と、Next.jsのアプリケーションをプロダクションレディにするナレッジや設定内容の一部などをご紹介します。

目次

  • はじめに
  • 目次
  • 背景
    • セッションオフロード
    • カナリアリリース
    • フロントエンドリプレイスPhase1
  • システム構成
  • Next.jsのアプリケーションをプロダクションレディにするナレッジ
    • 要件を実現するためのレンダリング選定
    • Next.jsの性能試験でレンダリングとスループットの関係性を調査
    • CDNキャッシュを用いた最適化
    • URLに対して複数のキャッシュを作成する
    • カスタムサーバーでルーティングのカスタマイズとロギングの実現
    • Sentryでのエラーログ集積とソースマップのアップロード
    • ソースマップアップロードを可能にするDockerイメージ作成
  • 効果と今後の課題
  • まとめ

背景

ZOZOTOWNのリプレイスは開発効率を上げる、運用コストを下げる、人材獲得の強化を目的として掲げています。その手段としてAPIのマイクロサービス化をしています。これと同時並行でフロントエンドに新フレームワークを導入しリプレイスする計画がありました。過去の弊社瀬尾の発表資料では下記の図で示され、数年前から検討はされていました。



歴史が長く、アクセス数の多いサービスを、稼働させたままリプレイスするのは一筋縄ではいきません。さらに、我々は既存のサービスの成長を止めずに、リプレイスする方針で取り組んでいます。こうしたリプレイスを実現するベースを構築する必要がありました。ベースの構築にどのような困難があったのか、セッションオフロードとカナリアリリースを例にあげて紹介します。


セッションオフロード

サービスを止めないリプレイスを実現するために、ストラングラーパターンというレガシーシステムを徐々にモダナイズするためのアーキテクチャパターンを採用しています。具体的にはリプレイスしたパスへのリクエストはモダンシステムに、置き換え前のパスはレガシーシステムにパスルーティングします。最終的には、リプレイス前のシステムへのパスルーティングはなくなり、リプレイス完了となります。

フロントエンドのHTMLの配信においても前述したパスルーティングを用いて、パスごとに段階的なリリースを計画していました。しかし、このパスルーティングを実現できない事情が既存システムにはありました。既存システムはIISのユーザーセッション機能を利用しており、セッションがWebサーバーに紐づいています。つまり、ユーザーはセッションが続いている限り以前接続したサーバーに接続されます。いわゆるスティッキーセッションです。これでは、パスルーティングを機能させることができません。この問題を解消するために、セッション情報をAmazon ElastiCache for Redisにオフロードする取り組みなどが必要でした。セッションをオフロードすることで、サーバーとセッションが分離でき、ストラングラーパターンによる置き換えが可能になりました。

セッションオフロードの詳細は、杉山の記事をご参照ください。


ZOZOTOWNにおけるセッションストアのリプレイス完了までの道のり - ZOZO TECH BLOG
こんにちは。技術本部SRE部ZOZO-SREブロックに所属している杉山です。SRE部のテックリードとして、オンプレ/クラウドのインフラを担当しています。 ZOZOTOWNでは、既存システムのリプレイスプロジェクトを進めています。各サービスのマイクロサービス化は進んでいますが、バックエンドでは「WindowsServer + ...
https://techblog.zozo.com/entry/session-offload-phase2


カナリアリリース

ZOZOTOWNはUIや機能改修によってビジネス指標に大きく影響が生じるサイトです。これを考慮し、UIや機能要件はリプレイス前と可能な限り互換性を維持し、挙動は変えないという方針を定めています。リリース前に念入りにQAテストを実施しますが、リリースでの不具合の発生や他システムに影響を及ぼすリスクは存在します。このリスクを軽減するために、一部のユーザーだけに絞り新システムを提供し、段階的にリリースするカナリアリリースを実施しています。このカナリアリリースについてはAkamai Application Load Balancerの加重ルーティングという機能を利用して実現しています。

加重ルーティングの詳細は、秋田の記事をご参照ください。


ZOZOTOWNにおけるAkamai Application Load Balancerの導入 - ZOZO TECH BLOG
こんにちは、SRE部の秋田と鈴木です。ZOZOTOWNのオンプレミスとクラウドの運用・保守・構築に携わっています。 現在、ZOZOTOWNはリプレイスプロジェクトの真っ只中です。そのため、いくつもの壁にぶつかりつつも、それらを1つずつ解決してプロジェクトを進めている状況です。 ...
https://techblog.zozo.com/entry/zozotown-akamai-alb


これらの例以外にも、リプレイスサービスを構築するためにCI/CD戦略BFF APIサービスメッシュプログレッシブデリバリーなどの施策を実施しベースが整ってきました。こうした背景から満を持してフロントエンドのリプレイスが始動しました。

リプレイスは、全く別のものに一気に刷新するのではなく、このようにサービスを構築するためのベースを構築し、それぞれの機能ごとにマイルストーンを設定し段階的に置き換えていくことが有効です。


フロントエンドリプレイスPhase1

リプレイスをどのページから着手するか検討した結果、ホーム画面を選択しました。理由は下記の通りです。

  • ホーム画面で利用しているAPIは、大部分がBFF(Backend For Frontend)から提供されており、レガシーシステムへの依存が比較的少ない
  • アクセス数や機能が多く、開発や運用のナレッジを蓄積しやすい
  • サービスの象徴的ページであり、開発のモチベーションが湧きやすい

さらにリプレイスの付加価値としてSPA(Single Page Application)化することでページ遷移を高速にし、UXの向上を考えました。具体的にはホーム画面では、下記の図のような商品のカテゴリーや性別を切り替えるタブUIがあります。



このタブUIでのページ遷移時にページ全体を読み込まず、商品データだけを動的に切り替えるSPAを実装しました。

次にフロントエンドフレームワークについてです。フレームワークはNext.jsを採用しました。選定理由は下記の通りです。

  • Reactベースのフレームワーク
  • ゼロコンフィグで開発を始められる
  • ページごとのレンダリング手法を柔軟に切り替えるができる
  • 数々のパフォーマンス最適化など新機能が毎年リリースされており、とてもアクティブに開発されている
  • 利用している開発者が多く、コミュニティーが盛んでWebに情報が多い
  • HeadlessCMSと相性が良い

Next.js以外の新たに導入したライブラリを紹介します。



それぞれの選定意図については記事の本題ではないため紹介のみとします。Emotionの選定意図は、菊地の記事をご覧ください。


ZOZOTOWN Webフロントエンドリプレイスにおける CSS in JS の技術選定で Emotion を選定した話 - ZOZO TECH BLOG
こんにちは。ZOZOTOWN開発本部フロントエンドの菊地(@hiro0218 )です。 現在、 ZOZOTOWN ではWebフロントエンド技術のリプレイスプロジェクトが進行しています。本記事では、WebフロントエンドのリプレイスでCSS in JSの技術選定をした際の背景や課題についてご紹介します。 リプレイス以前の環境は、Classic ASPのテンプレートエンジンに依存したUI実装が多く存在しており、新規開発や変更のタイミングで実装をReact + CSS Modulesへ改修しています。そのため、レ
https://techblog.zozo.com/entry/zozotown-css-in-js


これらの新フレームワークや新技術の導入とインフラを構築することで、フロントエンドリプレイスの礎を作るマイルストーンを社内ではフロントエンドリプレイスPhase1と呼んでいます。以降も複数のマイルストーンをおき、2024年を目処にフロントエンドのリプレイスをすべて完了させる計画です。

システム構成

リプレイスに際して構築したシステムは下記のような構成です。



まずCDNを経由してユーザーのブラウザにコンテンツが配信されます。弊社ではCDNにAkamaiを採用しております。このAkamaiでは、(1)キャッシュ、パスルーティングとあるように、コンテンツのキャッシュや、ユーザーのリクエストパスから適切なサービスにルーティングをするパスルーティングなどを行なっています。具体的にはホーム画面のパスをリプレイス後のシステムへ、ホーム画面以外のパスは既存システムにリクエストをルーティングしています。

次にリプレイス後のパブリッククラウドのシステムですが、AWS上に構築しており、コンテナアプリ基盤にマネージドKubernetesサービスであるEKSを採用しています。また、複数サービスを単一Kubernetesクラスタで稼働させる、いわゆるマルチテナントクラスタ方式です。このクラスタにマイクロサービス群と、BFF API、そして今回新設したNext.jsのSSRを実行するサーバーが稼働しています。

最後に(2)データ取得、セッション共有とあるように、リプレイス後のシステムと既存オンプレミスのIISサーバーとセッションデータ共有や、まだリプレイスが完了していないデータストアからデータ取得を可能にしています。これによりあらゆる機能要件を満たすことができます。

なお、実際には認証サービスや、APIルーティングを行うAPI Gatewayなどのサービスとも通信していますが、ここでは省略しております。

以上がシステムの全体像です。

Next.jsのアプリケーションをプロダクションレディにするナレッジ

Next.jsの機能はシンプルなため、Reactを使ったプログラミングに習熟していれば、スムーズに開発を進めることができました。Web上に開発に関するナレッジが多く集積されている点が大きな要因と思われます。一方で、Next.jsのアプリケーションのサーバー負荷への考慮、ロギングやエラーハンドリングなどのプロダクションレディにするための情報がWeb上に少ないように感じました。なのでここからはそれらのナレッジについて紹介したいと思います。


要件を実現するためのレンダリング選定

Next.jsのアプリケーションにおいて、SSR(Server Side Rendering)するか否かというのはとても重要な決断です。 アプリケーションの性質や要件によれば、SSRせずSG(Static Generation)やCSR(Client Side Rendering)も可能です。その場合は静的ファイルを配信するのみとなりインフラの管理コストは低くなります。

一方、SSRする場合はNode.jsの実行環境を必要とするため、アプリケーションを監視するエンジニアのオンコール体制の構築、サーバーコスト、パフォーマンス的な懸念等々の管理コストが発生してしまいます。可能であればSSRしたくはありませんが、下記のような機能要件やSEOを考慮してSSRすることは不可避でした。

  1. メタタグにブランド数、商品名、OGP画像などの動的データを含めたい
  2. ファーストビューに表示されるUIはローディングなど挟まず表示したい
  3. セールやキャンペーンの開始や終了のタイミングに合わせて時限式に切り替わるUIを提供したい

3の要件についてクライアントサイドのJavaScriptで、時限式に切り替わる実装をする選択もあります。しかし、クライアントのJavaScriptはソースファイルが公開されます。そのため将来のキャンペーンやセール情報が露見してしまう可能性があります。したがって、クライアントではなくSSRするという結論になりました。


Next.jsの性能試験でレンダリングとスループットの関係性を調査

GoやJavaのAPIサーバーは運用実績があり、性質や運用についてのノウハウがあります。一方でNode.jsを運用するのは初めての試みでした。加えてNode.jsはシングルスレッドのランタイム環境という特性があります。そのため、CPUバウンドなタスクを実行する場合、サーバー処理をブロックしてしまいパフォーマンス低下の可能性があります。具体的には、SSRの処理がCPUバウンドな処理で知られており5、この事象が起きてしまえばインフラコストが高くついてしまうことや、パフォーマンス要件を満たせない懸念があると考えました。本番にリリースしてからパフォーマンス要件が満たせないことになれば問題ですので、Next.jsアプリケーションの性能試験を実施しました。

を実行しているためです。性能試験は、Gatling Operatorというツールを用いて、本番に近いインフラやサーバーをセットアップし、リクエストを送りその結果をモニタリングして計測します。パフォーマンス要件の基準はLighthouse の TTFB の基準値を参考に600ms
以内とし、この状態で秒間どの程度のリクエストを捌けるかスループットも計測します。SSRするコンポーネントの規模によって、パフォーマンスやスループットの目処をつけておきたかったため3パターン実装しました。スペックのcore数が2core以上なのはNext.jsアプリケーション以外にもサービスメッシュとしてistio proxy

結果は下記のとおりです。



高負荷の場合はパフォーマンス基準を安定的に満たし、スループットは5req/secと効率が悪い結果となりました。やはり前述した通り、CPUバウンドなSSRになってしまうとインフラのコストパフォーマンスは悪くなりそうです。しかし、中程度の負荷であれば、スループットも性能はまずまずという結果も得られました。この結果から、負荷を考慮したSSR実装をすることに加え、負荷増加が考えられるリリースをする際には負荷試験を行ない事前に検知するなど対策すればスケーラブルに運用できるという判断をしました。以上の性能試験から、SSRという選択肢ありきで安心して開発に着手できました。


CDNキャッシュを用いた最適化

性能試験の結果から、Node.jsをスケーラブルに運用できることが分かりました。しかしながら、性能は常に最適な状態に保つことが望ましいため、CDNでキャッシュを使用することでパフォーマンスを向上し、コスト削減を実現できます。HTMLをSSRした結果を一定期間CDNでキャッシュすることにより、オリジンサーバー上でNode.jsサーバーの負荷を大幅に軽減できます。ただし、パフォーマンス要件を満たすためにキャッシュを有効にできない場合もあります。ホーム画面の要件に関してはキャッシュが可能であるため、キャッシュを有効にしています。具体的には、下記のようにレスポンスヘッダーのCache-Controlを使用して、キャッシュの保持期間を制御できます。

Cache-Control: s-maxage=seconds

検証の際には、時間の文字列をHTMLに埋め込んでおくと、キャッシュできているか検証しやすいので、実装しておくのがおすすめです。Next.jsでは下記のように書けます。

import { GetServerSideProps, InferGetServerSidePropsType } from 'next'

export default function Index({ time }: InferGetServerSidePropsType<typeof getServerSideProps>) {
  return (
    <script
      type="application/json"
      data-type="cacheTimeDisplay"
      data-time={time}
    />
  )
}

export const getServerSideProps: GetServerSideProps<{ time: string }> = async ({
  res,
}) => {
  const second = '10'

  res.setHeader(
    'Cache-Control',
    `s-maxage=${second}`
  )

  return {
    props: {
      time: new Date().toISOString(),
    }
  }
}

このようにしてレスポンスヘッダーをページごとに異なる時間を設定することで、キャッシュを最適化していくことができます。ただし、CDNキャッシュを利用する際には注意が必要です。特に、SSRするHTMLには個人を特定できるようなパーソナルな情報を含めないようにすることが重要です。ユーザーごとに異なるパーソナル情報はAPIから取得し、クライアントサイドでレンダリングするようにする必要があります。パーソナル情報をSSRしてしまうと、CDNキャッシュを利用できなくなってしまうためです。もし誤ってパーソナル情報をキャッシュさせてしまった場合、重大な情報漏えいが起こる可能性もあるため、キャッシュを用いる際には慎重に実装する必要があります。


URLに対して複数のキャッシュを作成する

通常、1つのURLに対して1つのキャッシュが作成されますが、Cache ID Modificationという機能を使うことで、複数のキャッシュを1つのURLに対して作成できます。例えば、ホーム画面にはカルーセルバナーがあり、このバナーは指定なし、レディース、メンズ、キッズの4つのパターンがあります。



これをSSRするには、4つのキャッシュを作成する必要があります。これを実現するために、Cache ID Modification機能を使用しています。Cache IDは、CDNの管理画面で設定でき、Cookieやリクエストヘッダーなどを指定できます。この場合、Cookieに性別を保存し、このCookieをCache IDに設定しました。これにより、4つの異なるキャッシュが作成され、適切なカルーセルバナーがSSRされます。


カスタムサーバーでルーティングのカスタマイズとロギングの実現

zozo.jpのホーム画面はデスクトップ向けにはhttps://zozo.jp/、モバイルでサイトhttps://zozo.jp/sp/とURLが異なります。そのため、モバイルデバイスでhttps://zozo.jp/にアクセスした場合は、https://zozo.jp/sp/にリダイレクトされるような実装が入っています。例えばこのようなルーティングをカスタマイズしたい場合に利用できるのがカスタムサーバーという機能です。この機能を使えばNode.jsサーバーのモジュールとしてNext.jsを利用できます。Node.jsの組み込みモジュールでも実装は可能ですが、WebフレームワークのFastifyを利用しました。理由はパフォーマンスの良さ、TypeScriptとの相性、ロギングのしやすさなどです。

Fastifyを利用する場合Next.jsカスタムサーバーは下記のように書けます。

続きはこちら

株式会社ZOZOからお誘い
この話題に共感したら、メンバーと話してみませんか?
株式会社ZOZOでは一緒に働く仲間を募集しています
1 いいね!
1 いいね!

同じタグの記事

今週のランキング

株式会社 ZOZOさんにいいねを伝えよう
株式会社 ZOZOさんや会社があなたに興味を持つかも