リアルタイムなプッシュ通知を可能にした配信基盤の紹介

ogp

※2022-06-07 システムアーキテクチャの画像を修正しました。

はじめに

こんにちは、MA部MA基盤ブロックの齋藤(@kyoppii13)です。

ZOZOTOWNではアプリ向けのキャンペーンやセール情報などの配信でプッシュ通知を利用しています。プッシュ通知で配信するキャンペーンはセグメントに向けたマス配信のみで、ユーザごとにパーソナライズして配信するためのパーソナライズ配信には利用していませんでした。また、パーソナライズ配信の中にはリアルタイム性が求められるキャンペーン配信も含まれます。そこで、リアルタイムキャンペーンでプッシュ通知するための配信基盤を作成しました。

本記事では、リアルタイムなプッシュ通知を実現するために作成したシステムの紹介と、安定した配信を実現するために行った工夫について紹介します。

従来のプッシュ通知と課題

本章では、従来のプッシュ通知と課題について説明します。

従来のプッシュ通知

ZOZOTOWNではアプリ向けのキャンペーンやセール情報をプッシュ通知を含む様々なチャネルで配信しています。配信するキャンペーンの種類は大きく分けて、マス配信とパーソナライズ配信の2種類があります。

マス配信は、ある条件に一致する一定のユーザに対してバッチ処理によって一括で配信しています。パーソナライズ配信はユーザごとにパーソナライズしてキャンペーン配信します。中にはリアルタイム性が求められるキャンペーン配信も含まれます。例えばカートに入れたままで注文が確定していない商品のリマインドなどがあります。マス配信とパーソナライズ配信の基盤はそれぞれ分かれて存在しています。

マス配信ではすでにプッシュ通知を利用していました。プッシュ通知にはFirebase Cloud Messaging(以降、FCM)を利用しています。FCMに対象者と送信するメッセージ内容をリクエストすることでプッシュ通知が可能です。

マス配信でのプッシュ通知について、詳しくはこちらをご覧ください。

techblog.zozo.com

問題点

パーソナライズ配信は施策効果も大きいことから、プッシュ通知を利用したいというニーズがありました。

パーソナライズ配信基盤のみでプッシュ通知をするのであればパーソナライズ配信基盤を改修し、直接プッシュ通知もできました。しかし、パーソナライズ配信基盤は改修コストが大きく、システムが複雑になってしまいます。また、将来的には配達状況などを通知するトランザクションメッセージなどでもプッシュ通知をしたいというニーズから、複数のシステムから利用できる統一的な配信基盤が必要でした。

導入した配信基盤

前述の課題を解決するために、プッシュ通知のための配信基盤を作成しました。システムのアーキテクチャを下図に示します。

architecture

プッシュ通知を配信したい場合はこのシステムに対して対象者と通知内容をリクエストします。システムはそれを受けてデータを整形し、FCMにリクエストすることでユーザにプッシュ通知が届きます。また、配信時に配信実績をBigQueryに保存します。

配信基盤の入力について

配信基盤の入力は下図の赤枠で示した部分です。

architecture_input

配信基盤は入力にPub/Subを利用しています。以下のようなフォーマットのメッセージを受け取ります。

{
  "source": Integer,
  "source_uid": Integer,
  "registration_token": String,
  "member_id": Integer,
  "push_info": {
    "title": String,
    "body": String,
    "url": String,
    "image_url": String
  }
}

各パラメータの説明を以下の表に示します。

パラメータ名 説明
source リクエスト元システム識別用パラメータ
source_uid リクエスト元システムごとにメッセージに付与するパラメータ
registration_token FCMトークン
member_id ZOZO TOWNメンバーID
push_info.title プッシュ通知タイトル
push_info.body プッシュ通知本文
push_info.url プッシュ通知本文
push_info.image_url 画像つきプッシュで表示する画像URL

sourceはリクエスト元システムを識別するためのパラメータです。source_uidはリクエスト元システムごと、メッセージへユニークに割り当てるパラメータです。sourceとsource_uidは後述の重複配信の制御にて利用します。

registration_tokenはFCMトークンです。配信端末ごとユニークに割り振られるトークンです。これで対象者を指定します。push_infoは通知内容を指定します。FCMには通知メッセージとデータメッセージの2種類があります。違いとしては、メッセージの処理をする場所が異なります。通知メッセージはFCMで、データメッセージはユーザ端末で処理をします。セグメント配信ではデータメッセージを利用しており、セグメント配信と同様のフォーマットになっています。

配信処理

前述のフォーマットでPub/Subが受け取ったメッセージを、続くCloud Functionsにて配信処理をします。配信処理の流れは次の通りです。

  1. メッセージのバリデーションチェック
  2. 重複配信の制御
  3. FCMへメッセージ送信
  4. 実績登録

配信処理はPythonにて実装しています。

バリデーションチェック

処理対象となるメッセージのフォーマットが正しいかをチェックします。具体的には、必須パラメータと型チェックを実施します。例えば、配信基盤で受け取るメッセージのうちpush_info.image_urlは画像付きプッシュのときだけ受け取る値で、それ以外のパラメータは必須です。

重複配信の制御

重複配信の制御は下図の赤枠に示した部分で実施します。

architecture duplicate check

メッセージ配信時の信頼性を表すものに以下のようなものがあります。

種類 説明
exactly once 必ず1回配信される
at least once 最低1回配信される
at most once 最大1回配信される

必ず1回の配信が保証されるexactly onceは理想的ですが、at least onceとat most onceの両方を満たす必要があり、設計難易度が高まります。今回はキャンペーンの特性上、重複配信を避けられればよいため、at most onceを採用することにしました。

重複配信の制御にはNoSQLデータベースであるCloud Datastoreを利用しています。Cloud Datastoreに保存されるデータはsourceとsource_uidの組み合わせでユニークです。このペアを利用して、配信済みかどうかの確認と登録を実施します。

素直に配信フローを考えれば以下のようになります。

  1. 配信済み確認
  2. FCMへメッセージ送信
  3. 配信済み登録

しかし、このフローで配信処理が完了後の配信登録前で中断した場合、配信処理の全体をリトライすると重複配信の可能性があります。そこで、今回実装した配信フローは、次の通りです。

  1. 配信済み確認
  2. 配信済み登録
  3. FCMへメッセージ送信

配信済み確認と配信済み登録を配信処理より前に実施することで、配信登録後に中断した場合はリトライしても配信されないことになり、at most onceが実現できます。

今後、トランザクションメッセージなどの必ず届けたい通知をする場合はat exactly onceを検討する必要があります。この点は課題です。

FCMへのメッセージ送信

FCMトークン(registration_token)と通知内容(push_info)をFCMへ送信します。FCMへのリクエストが成功した場合はメッセージを一意に識別するmessage_idを含むレスポンスが返ってきます。

実績登録

実績登録は下図の赤枠に示した部分で実施します。

architecture log

Pub/SubとDataflowを組み合わせて、ストリーミング処理にてBigQueryに実績登録しています。

Cloud FunctionsでFCMへのリクエストが終了後、通知内容を含むメッセージをPub/SubにPublishします。実績として保存するデータは通知内容、対象者、配信日時、配信の成功可否です。その後、ストリーミング処理にてDataflowからBigQueryへの書き込みがされます。

実績を保存しておくことで、集計に使用できるのはもちろんのこと、パーソナライズ配信基盤でキャンペーン配信時に実施している、ユーザごとの最適化処理にも利用できます。最適化処理によって、最適なキャンペーンを最適なチャネルでユーザに届けることができ、キャンペーンによる施策効果を向上できます。

品質担保の方法

配信処理のリトライ

Cloud Functionsで動いている配信処理はそれぞれでリトライを入れています。また、Cloud Functions自体のリトライも入れています。配信数が瞬間的に増えた場合などCloud Functionsの起動に失敗することがあるためです。Cloud Functionsのデプロイ時にretryオプションを付けることで、Cloud Functions自体のリトライが可能です。以下がデプロイ時のコマンド例です。

$ gcloud functions deploy test_function --runtime=python39 
--trigger-topic=push-test 
--retry

パフォーマンステスト

作成した配信基盤がリアルタイム基盤からのリクエストに対してパフォーマンスに問題がないかをテストする必要がありました。今回、パフォーマンステストには、以下の理由からLocustというテストツールを採用しました。

  • HTTP/HTTPSのみならずSDKを使用したテストが可能
  • 分散実行が可能
  • Pythonによるテストケースの記述

HTTP/HTTPSのみならずSDKを使用したテストが可能

配信基盤の入力部分にPub/Subを利用しています。リアルタイム基盤からのリクエストではPub/Subのクライアントライブラリを使用する予定でした。

多くの負荷テストツールはHTTP/HTTPSを前提としており、SDKを利用したテストが容易ではありません。その点、LocustではSDKを利用したテストが容易なため採用しました(参考:Locustドキュメント

分散実行が可能

将来的には複数のシステムからの利用が想定されます。Locustではmaster/worker構成によってテストを分散実行できます。workerを増やすことで、並列数を上げることができます。これによって、テスト対象システムへ大きな負荷をかけることが可能です。

Pythonによるテストケースの記述

テストケースをPythonによって記述できます。今回、Cloud Functionsの実装などでもPythonを利用しています。システム開発における使用言語を統一できました。

パフォーマンステストでは、現状のリアルタイム基盤でのスループットに係数をかけたものを目標としました。リリース前にパフォーマンステストを実施することで、重複配信していないかの確認やエラー率を出せたため、安心してリリースできるようになりました。

監視

配信時の異常にすぐ気付けるようCloud Monitoringによる監視も入れています。異常なログがあった場合はSlackとPagerDutyによる通知がされます。

今後の展望

配信基盤でのFCMトークンの取得

配信基盤へリクエストする際、通知内容とともにFCMトークンも含めるようになっています。今後は受け取ったmemberIdをもとに配信基盤側でFCMトークンを取得するようにしたいと考えています。

複数システムからの配信基盤の利用

重複配信の制御の章でも触れましたが、将来的にはトランザクションメッセージの通知など他システムからの利用も考えています。トランザクションメッセージの場合は必ずユーザにメッセージを送信する必要があります。そのため、配信処理ではat exactly onceまたはat most onceにする必要もあります。

まとめ

リアルタイムなプッシュ通知を実現するために作成した配信基盤について紹介しました。配信基盤の導入によって、プッシュ通知によるリアルタイムキャンペーンができるようになりました。本記事が皆様の参考になりましたら幸いです。

さいごに

ZOZOでは一緒にプロダクトを開発してくれるエンジニアを募集しています。ご興味のある方は下記リンクからぜひご応募ください!

corp.zozo.com

カテゴリー