Backends For Frontends(BFF)はじめました

はじめに

こんにちは。EC基盤本部SRE部プラットフォームSREの三神です。
2021年3月18日、ZOZOTOWNは大規模なリニューアルをしました。その中でも、コスメ専門モールのZOZOCOSMEと、ラグジュアリー&デザイナーズゾーンのZOZOVILLAを同時にオープンし、多くの反響をいただきました。
今回のリニューアルではBackends For Frontends(以下、BFF)にあたるZOZO Aggregation APIを構築しています。本記事ではZOZOTOWNが抱えていた課題とBFFアーキテクチャを採用した理由、またZOZO Aggregation API構築時に発生した課題と解決法についてご紹介します。
ZOZO Aggregation APIのサービスメッシュについてはこちらの記事でご紹介していますので合わせてご覧ください。

BFFとは

BFFはアーキテクチャ設計パターンの1つです。フロントエンドのリクエストに応じて各種のAPIコールをしたり、バックエンドから取得した内容を加工してフロントエンドに返却したりするフロントエンド専用のサーバーを用意するアーキテクチャ設計パターンです。
より詳細な役割はこちらを御覧ください。

ZOZOTOWNにおけるBFFの役割

今回のZOZOTOWNリニューアルにおける大きな特徴の1つに「パーソナライズ」が挙げられます。
我々は、ZOZOTOWNにおいてユーザーと商品のより良い出会いを提供したいと考えています。そのためには画面に表示する情報も画一的なものではなく、ユーザーの趣味嗜好に合わせた商品情報をお届けする必要があります。
これを実現するために、リニューアルしたZOZOTOWNではユーザーが登録している情報から適切な商品情報を提供する機能を実装する事にしました。さらに、商品情報を提供する際にはユーザーが利用しているクライアントに合わせて表示方法を変更する事でより良い体験を提供する事も重視しました。
これらの要件を満たすために、BFFであるZOZO Aggregation APIが各種処理を実施しています。ZOZO Aggregation APIでは各バックエンドから取得した情報をモジュールという単位で管理します。モジュールは性別や年齢、お気に入りブランドといった情報によって取得する内容が異なっており、複数のモジュールからパーソナライズする処理をZOZO Aggregation APIにて行っています。接続したクライアント毎にモジュールの数も調整しており、各クライアントのUIに合わせて最適な形でレスポンスを返す処理もZOZO Aggregation APIが担当しています。

なぜBFFを採用したのか?

下記の記事でも紹介している通り、昨年よりZOZOTOWNのリプレイスの一環でシステムのマイクロサービス化を進めています。認証機能や検索機能といった様々なシステムのマイクロサービス化を進めており、今後もその範囲は拡大していく予定です。
リニューアル後のZOZOTOWNではパーソナライズ機能を強化しているので、クライアントが必要とするデータの種類が増えました。そして、マイクロサービス化が進む事で、クライアントが必要なデータを取得するために多数のマイクロサービスへリクエストする必要性が出てきます。この様な背景から下記の課題が出てきました。
  • クライアントが接続するマイクロサービスの増加により、各マイクロサービスのAPI仕様とクライアント実装が複雑になる
  • 各APIにアクセスを行うため、クライアント・サーバー間の通信量が増加する
  • リプレイスの進捗に応じてバックエンドAPIの粒度や提供データの内容に修正が発生した場合、各クライアントが要件に追従する必要がある
上記課題の解決手段としてBFFアーキテクチャを採用する事にしました。
BFFの存在により、下図のようにフロントエンドはBFFにのみリクエストを送る事になり、通信量の肥大化を防ぐ事ができます。クライアントの実装も接続先はBFFのみとなるのでシンプルにできます。また、バックエンドAPIに修正があった際もBFFにてその対応が吸収できるので、各クライアントでの対応は不要です。
これら踏まえ、各バックエンドの情報を集約/整形してフロントエンドに返す処理をするBFFをZOZO Aggregation APIとして実装する事にしました。

ZOZOTOWNにおけるBFFアーキテクチャ

ZOZO Aggregation APIはZOZO API Gateway(以下、API Gateway)の配下に設置する設計で構築しています。
ZOZOTOWNではAPI Gatewayパターンのアーキテクチャを採用しており、認証認可やカナリアリリース機能を備える高機能な内製API Gatewayを軸にしたシステム構成となっています。BFFであるZOZO Aggregation APIはAPI Gateway配下の1マイクロサービスとして設置しており、リクエストはAPI Gatewayを経由してルーティングされます。
なお、API Gatewayに関しては以下の記事でご紹介していますので合わせてご覧ください。
次に、ZOZO Aggregation APIをリリースする上で発生した課題を2点ご紹介します。

BFFによるキャッシュの一元化

リニューアル後のシステム要件を整理する中でZOZO Aggregation APIにおけるバックエンドの最大負荷が想定以上に多い事が判明しました。
リニューアル後のトップページでは、パーソナライズを強化するためにバックエンドAPIへのリクエストがリニューアル前に比べて増加していました。それに加え、セール等のイベント時は通常時と比較して圧倒的に負荷が高くなるため、スパイクを考慮する必要もあります。
リニューアル後のセール時に発生するスパイクをシミュレーションすると、既存の各バックエンドの規模では負荷に耐えられない事がわかりました。バックエンドが高負荷状態になり、ZOZO Aggregation APIのレスポンスが遅延した場合、トップページ生成時間が長くなるのでユーザー体験を著しく損なう可能性があります。
この問題の対策として、各バックエンドの増設もしくはキャッシュの導入を検討しました。前者の増設による対策の場合、すべてのバックエンドをイベント毎に増設する必要があり、そのための工数や維持費用が膨れ上がり現実的な解決策とは言えませんでした。そこで、後者のキャッシュによる解決策を中心に検討を進めていきました。
トップページにおけるアクセス増が原因なので、AkamaiやFastlyといったCDNにてキャッシュする事による負荷軽減を模索しました。しかし、パーソナライズを実現するにあたり、多種多様なモジュール内容と組み合わせを想定しているため、ユーザーごとに表示される内容に差異が多い仕様でした。したがって、ZOZO Aggregation APIにて集約した後のページをキャッシュするCDNの様な方式は、本サービスにおける負荷対策としてあまり効果的ではないと判断しました。
そこで、モジュール単位でキャッシュする案を検討しました。ページを生成する前のモジュール単位であれば、同一条件下でのレスポンスデータ生成においてキャッシュが利用できます。そのため、モジュール単位でキャッシュするシステム構成に変更しました。ZOZO Aggregation APIと各バックエンドの間にElastiCacheによるRedisを利用する事で各モジュールをキャッシュする仕組みを構築しました。
BFFの存在のおかげで、キャッシュ利用に関する実装はBFF内で完結させる事ができました。BFFが無ければ各フロントエンドでキャッシュに関する実装が必要となり、開発工数が大きく増加していました。
下図はキャッシュが無いタイミングのアプリケーショントレーシングの結果です。ZOZO Aggregation APIが各バックエンドに対して大量にリクエストしている事がわかります。
そして、次に示す図はキャッシュがある場合のアプリケーショントレーシングの結果です。ZOZO Aggregation APIが各モジュールのキャッシュを取得しているため、バックエンドに対する負荷が下がっている事がわかります。また、レイテンシも200msから40msと短縮されており、レスポンス速度も大幅に改善している事がわかります。
キャッシュを非常に活用できている状態になったのですが、運用上の課題が一点残りました。
ZOZOTOWNでは毎日お得なクーポンを発行しており、利用できるクーポンが毎日午前0時に切り替わります。ZOZO Aggregation APIではクーポン情報も保有しているので、午前0時のタイミングで強制的にバックエンドから最新情報を取得する仕様となり、キャッシュスタンピード状態になる可能性がありました。この課題に関しても、記事を執筆予定なので、是非ご期待ください。

BFFにおけるサービス可用性の考慮

BFFはフロントエンドからリクエストを受けるため、BFFに障害が発生した場合はサービス障害に直結しやすい傾向にあります。ZOZO Aggregation APIもトップページを生成するために利用されるので、障害時はZOZOTOWNのサービス自体に影響します。ところが、BFFは複数のバックエンドと通信するアーキテクチャである事から、いずれかのバックエンドにて障害が発生した際に、その影響を受けてしまう懸念がありました。サービスとしての可用性を担保するにはこの課題の対策が必要です。
そこで、ZOZO Aggregation APIでは、特定のモジュールが取得できない場合は取得済みモジュールのみでレスポンスを行う仕様にしました。タイムアウトとリトライ制御をバックエンド毎に設定しておき、バックエンドが期間内にレスポンスを返さない場合は、その他のバックエンドから取得できたモジュールのみでレスポンスを返します。
ZOZO Aggregation APIと各バックエンド間の通信におけるタイムアウトとリトライ制御は、Istioのトラフィック制御機能で実現しています。ZOZOTOWNマイクロサービスプラットフォームにおけるIstioの活用についてはこちらの記事で紹介しているので、是非ご覧ください。
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: zozo-test-api-vs
spec:
  hosts:
  - zozo-test-api-vs.zozo-test.svc.cluster.local
  http:
  - route:
    - destination:
        host: zozo-test-api-vs.zozo-test.svc.cluster.local
        subset: zozo-test-api
      weight: 100
    retries:
      attempts: 1
      perTryTimeout: 8s
      retryOn: 5xx,connect-failure
    timeout: 9s
下図で示す通り、アプリケーショントレーシングでも特定のモジュールがタイムアウトした場合、ZOZO Aggregation API自身は取得できたモジュールのみで返却している事がわかります。
なお、現在はタイムアウトとリトライ制御のみですが、バックエンドとの通信のさらなる回復性向上のためにサーキットブレーカーの採用を検討しています。

まとめ

今回はマイクロサービス化が進むZOZOTOWNにおけるBFFの有効性と構築時に発生した課題2点を紹介しました。BFFを採用した事でAPI実装やクライアント実装がシンプルになり、効率的なキャッシュ実装や通信量削減などの様々なメリットを実感しています。
今後もBFFを活用して様々な機能を追加し、適切なマイクロサービス環境の運用を目指していきます。新たな知見が得られた際はまたご紹介します。

最後に

ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、下記のリンクからぜひご応募ください。