はじめに
こんにちは。メルペイの Growth Platform で Backend Engineer をしている@hiramekunです。
この記事は、Merpay & Mercoin Advent Calendar 2024 の記事です。
Growth Platformは組織としてはメルペイに所属していますが、メルペイに限定されないさまざまな取り組みを行っています。その一つとして、商品フィードシステムのリアーキテクチャに取り組みました。これにより得られた多くの学びを今回紹介します!
背景
商品フィードとは、オンラインストアや商品カタログの情報を一括して管理し、さまざまな販売チャネルや広告プラットフォームに配信するためのデータ形式や仕組みを指します。メルカリでは多用な商品フィードに商品データを連携し、商品が広告として表示されるようにしており、外部メディアに対する商品訴求において重要な役割を担っています。
例えば、Googleのショッピングタブでは多数のサイトの出品を一覧できますが、メルカリの商品も表示されます。
(出典:GoogleのShoppingタブ)
課題
歴史的経緯から、連携先ごとに異なる商品フィードシステムが分散して作られ管理されており、このことが多くの課題を引き起こしていました。以下にいくつかの例を挙げます。
- システムによって実装・メンテナンス担当チームが異なり、コミュニケーションコストが増加。
- 商品情報の取得や非表示商品のフィルタリングなど共通する処理があるにもかかわらず、連携先ごとに固有の実装が行われ、システムごとに異なる問題が発生。
- システムごとにデータの取得元が異なり、商品の状態変更がリアルタイムにフィードに反映されない場合がある。
達成したい状態
こうした課題を解決するために、一つのシステムで全ての連携先に対する実装を提供することを目標として、商品フィード用のマイクロサービスを立ち上げることになりました。Growth Platormがオーナーシップを持っている他の既存のマイクロサービスに機能を追加するという選択肢もありました。しかし、今回は新たなマイクロサービスを立ち上げることに決め、その理由は以下の通りです:
- 既存のマイクロサービスの役割がすでに大きくなっており、その役割がさらに曖昧化することを避けるため。
- 各連携先サービスの特性に合わせてシステム設計を変更する際、他のシステムへの影響を最小限に抑えることができるため。
- 商品の更新イベントはRPSが高いため、システムの特性に応じてスケーリングが必要となる可能性があるため。
共通のフィルタリング設定や商品データの取得、商品のメタデータ付与などの処理は一つのシステムに統合することが必要です。これは、連携先に依存しない処理であり、修正が全サービスに適用されることを意図しています。
共通実装をまとめる一方、連携先に応じた固有実装は分離する必要があります。新しい連携先サービスを追加する際、必要最低限の差分で実装を完結させることが重要です。特に、外部APIへのリクエスト部分はエンドポイントやレート制限が異なるため、柔軟に変更できるようにしておく必要があります。
また、外部APIリクエストにはエラー対応が欠かせません。全てのエラーをこちらで制御できないので、常にエラーが発生する可能性を念頭に置き、リトライ可能な設計を採用しています。
技術的アプローチ
アーキテクチャ
具体的なアーキテクチャを紹介します。大枠としては、共通処理を担当するworkerと、連携サービス固有のworker(Batch Requester)に処理を分け、これらをPub/Subでつなぐ設計としました。この設計により、次のような利点があります:
- 各workerのシステム特性に応じたスケーリングが可能。
- 他のマイクロサービスへのリクエストと外部APIへのリクエストを分離し、外部APIの予測困難な動作を切り離すことが可能。
- 新しいbatch requesterをPub/Subのsubscriberとして追加することで、共通実装部分を変更せずに新しい連携サービスを追加可能。
- 商品の状態更新イベントが急激に増えた際に、Pub/Sub Topicがメッセージキューとしてシステムの安定性を高めることができる。
大枠としては、共通処理部分のworkerと連携サービス固有のworker (Batch Requeseter) にworkerを分けて実装し、その二つをPub/Subで繋げるという設計にしました。こうすることで次のような利点があると考えました。
- workerそれぞれのシステム特性に応じたスケーリングが可能になる
- 外部APIにおける不確実な挙動を他のマイクロサービスから切り離すことができる
- 新しいbatch requesterをPub/Sub Topicへのsubscriberとして追加することで、共通実装部分には手を加えずに連携サービスを追加することがきる
- 商品の状態更新イベント数スパイクした時にPub/Sub Topicがメッセージキューとしてシステムの安定性を高める
ではそれぞれのworkerについてもう少し詳しく説明します。
共通処理部分のworker
共通処理部分のworkerは、商品の状態更新イベントとして別サービスからPub/Sub Topicに流れてくるデータを受け取ります。このTopicをsubscribeしてイベントをリアルタイムに受信し、他のマイクロサービスにリクエストを送ることで追加の商品情報を付与したり、フィルター設定を参照して不適切な商品を除外します。この結果、処理された商品情報をマイクロサービス内でのみ用いるPub/Sub Topicにpublishします。
このworkerにはHPA(Horizontal Pod Autoscaler)が設定されており、CPU使用率に基づいてPod数を動的に調整します。
サービス固有のworker (Batch Requester)
次に、その商品情報を受け取る側の実装です。フィード用にカスタマイズされた商品情報のPub/Sub Topicを、連携サービスごとにデプロイされた固有のbatch requesterがsubscribeします。
batch requesterは、外部APIへのリクエストを秒単位で継続的に実行する必要があります。そのため、Go言語で実装されたPodをCronJobではなくDeploymentとしてデプロイしています。Deploymentを使用することで、より細かい時間間隔でタスクを実行でき、必要に応じたスケーリングも柔軟に対応できます。
エラーハンドリングも重要です。外部APIの一時的なエラーやネットワークエラーでリクエストが失敗することがあるため、retry機能を実装しました。本システムではPub/Subのretry機構を活用し、以下のように機能します:
- batch requesterがPub/Subからメッセージを受け取り、インメモリにバッチとして保存。
- 一定間隔でそのバッチを外部APIに送信。
- 送信が成功した場合、そのバッチに含まれる全ての商品に対応するPub/Subメッセージをack。
- 送信が失敗した場合、全ての対応メッセージをnackし、Pub/Subがメッセージを再送。
商品の状態をなるべくリアルタイムでフィードに反映したいため、ある一定の回数retryに失敗した場合はDead-letter topicに転送し、後続のリクエストを優先させます。
SLOとしては、商品フィードに正しく反映されている商品の割合を確認しています。今のところこのSLOは達成できているので、Dead-letter topicに溜まっている商品を再試行するためのジョブは必要ありませんが、将来的にはそうしたジョブを作ることも検討しています。
最後に
この商品フィードシステムを構築したことで、商品をよりリアルタイムに近い形でフィードに配信できるようになりました。また、共通の実装と各連携先の特有実装を分けることで、新しい連携先の追加がより簡単になりました。今後は新たな連携先の追加や、フィードデータのカスタマイズを進めていく予定です。
次の記事は@goroさんです。引き続きお楽しみください。