シンクロ・フード エンジニアブログ

飲食店ドットコムを運営する、株式会社シンクロ・フードの技術ブログです

プッシュ通知一括送信をFCM Topic送信に置き換えた話

はじめまして、開発部の濱野です。
普段は飲食店ドットコム求人飲食店ドットコムといった弊社の「飲食店と求職者のマッチングサービス」に関わるサービスや業務システムの開発に携わっています。
今回はFirebase Cloud Messaging(以下FCM)のBatch Send API廃止に伴い、プッシュ通知の一括送信機能をFCM Topicへ移行した経緯と移行時の対応方針や検討したことを紹介します。

背景と課題

弊社では求人飲食店ドットコムのAndroid/iOSアプリを作成しており、ユーザの操作に伴って送信される通知のほか、メールマガジンのような一度に大量の通知を送信する機能にFCMを活用したプッシュ通知を利用しています。
2024/7/22にFCM レガシーAPIが廃止のアナウンスがあり、大量プッシュ通知を一括送信するのに使用していたBatch Send APIもFCMレガシーAPIに含まれていたため、HTTP v1 APIへの置き換えが必要になりました。

移行候補の検討

大量のプッシュ通知を送信する処理の置き換えにあたって、いくつかの処理方針が候補に挙がりました。

候補1) 1件ずつ同期的に送信する

既存の一括送信で使用していたBatch Send APIは廃止されるため、HTTP v1 APIの単一送信をループさせる形で一括送信を実現する方法を検討しました。
しかし、今回のような送信対象が多い場合、1件ずつ送信するやり方では、件数分のリクエストを送信することになるため、バッチ処理完了までの時間が長くなってしまうという欠点があります。

候補2) 1件ずつ非同期で送信する

非同期送信はバッチ処理完了までの時間が改善されますが、非同期送信を採用するにあたって検討すべき課題や処理が複雑になってしまう可能性があります。

候補3) Topic機能

FCMメッセージの大量送信に関しては調べたところ、FCMの公式ドキュメントにてTopic機能を利用することで複数デバイスにメッセージを送信できることがわかりました。 Topic機能に関して調査したところ、いくつか注意すべき点があるもののパフォーマンスを考慮しつつ大量送信が実現できることがわかりました。

結論

FCMを活用したプッシュ通知の大量送信に関する移行手法を検討した結果、今回はTopic機能を採用することになりました。同期送信方式は実装がシンプルである一方で処理時間の課題が大きく、非同期送信方式は処理時間の改善は見込めるものの、実装の複雑さやリソース管理の観点で課題がありました。
これに対し、Topic機能はFCMが公式に推奨する実装方式であり、大量送信時の処理効率が優れているという大きな利点があります。ただし、Topic機能の採用にあたっては、トピック数の制限や購読管理など、いくつかの考慮すべき点があります。これらの詳細な実装方法と対応策については、次章で説明していきます。

同期送信 非同期送信 Topic機能
実装のシンプルさ
導入の容易さ
処理時間
送信内容 ユーザーごとに変更可能 ユーザーごとに変更可能 全ユーザー共通

FCM Topic機能の概要

FCM Topic機能は、特定のトピックを購読している全デバイスに対して一括でメッセージを送信できる機能です。送信対象によってメッセージが変わらないため、Webマガジンやニュース配信などの用途で利用することが想定されています。

Topic機能は以下の3つの主要なアクションで構成されています。

  • トピックの購読登録: デバイスをトピックに関連付ける
  • トピックの購読解除: デバイスのトピック関連付けを解除する
  • トピックへのメッセージ送信: 登録されたデバイスへ一括配信を行う

Topic機能による配信の基本的な流れを図に示します。

Topic機能による配信の基本的な流れ

Batch Send APIとの違い

Batch Send APIでは送信対象のデバイストークンを指定し(最大1000件)、メッセージ内容を設定した上でFCMにプッシュ通知の送信をリクエストします。メッセージもリクエストのタイミングで送信されるので、送信件数が増えるほどメール送信などの機能と合わせてプッシュ通知したい時にメールが届いてからプッシュ通知が届くまでに間が空いてしまうという問題が発生します。
これに対し、Topic機能の場合は送信対象のデバイストークンの設定とメッセージ送信のリクエストが分かれているので、大量のプッシュ通知送信に対する送信リクエストのタイミングにはズレは生じません。

また、Topic機能には以下のような特徴があります。

  • 同一トークンの重複リクエストは自動的に1つにまとめられるので、無駄なリクエストを減らす仕組みを実装する必要がない
  • 既に購読済みのトークンに対する再登録も正常終了として扱われるので、安全に再実行できる
  • トピックの作成は明示的な操作なく、最初の購読登録時に自動的に行われる。以降はトピック名を指定することで、そのトピックを使い回すことができる

使用時の注意点

前述したようにTopic機能には以前のBatch Send APIよりメリットがある面もありますが、以下のような仕様上の注意点があります。

  • 作成できるトピックの上限数が2000件のため、配信ごとにトピックを使い捨てるようなことはできない
  • トピックの購読数の上限はないが、購読登録/購読解除は一括で1000件毎しかできない
  • トピックへの購読登録時にトピックが作成されるが、FCMにトピック登録が反映されるまでにタイムラグ(数秒程度)が発生する

プッシュ通知の一括送信実装の置き換えの実装要件

今回Topic機能は求職者の希望にあった求人情報をレコメンドする機能の通知に使用されます。このレコメンド通知は対象となるユーザーが毎日変わるため、配信対象ではないユーザーに通知を送らないように購読しているユーザーを適切に管理する必要があります。
なぜなら、Topic機能はユーザーが興味のあるトピックを自ら選び(購読:Subscribe)、送信者はトピックに対して送信(公開:Publish)することで購読しているユーザー全員にメッセージを送信するPublish/Subscribeメッセージングモデルでの用途を期待しており、今回の様な送信側が送信先を選んでメッセージを送信するという用途を想定していないからです。

以上の点からプッシュ通知の一括送信実装の置き換えでは以下の要件を満たすことを必須としました。

  • FCMプッシュ通知の一括送信をFCM Topicに置き換える
  • 複数トピックの購読/購読解除を管理したい
  • ユーザーの購読解除漏れを防ぎたい
  • 購読登録/解除やメッセージ送信時のエラーハンドリングを適切に行い、エラーログを記録する

プッシュ通知一括送信の処理フロー

プッシュ通知の一括送信は、以下のような処理フローでFCMのTopicを利用して配信しています。

プッシュ通知一括送信の処理フロー

※Platform-level message transportは、AndroidデバイスならGoogle Play 開発者サービス、iOSデバイスならAPNsに対し配信しています。

上記のシーケンス図ではわかりやすさを重視し、以下の処理を省略しています。

  • 購読登録、購読解除、メッセージ送信でのログ記録処理
  • 購読登録、購読解除時の特定エラーコードでのリトライ処理
  • トランザクション開始/コミットの設定

エラーハンドリングと例外処理

今回Topic送信への置き換えの対象となる機能は毎日定期実行され、前述したとおり都度送信先のユーザーが変わります。そのため、購読解除漏れが発生すると本来送信対象ではないユーザーにプッシュ通知が誤送信される問題が発生します。

誤送信される問題を防止するため、以下の3点の対策を行いました。

  • 各トークンの購読登録/購読解除の処理結果(ステータスコード/エラーコード)を管理すること
  • エラー発生時の対応フローを正しく行うこと
  • データ整合性を確保するためのトランザクション制御 各トークン処理結果の管理に関しては、トークンの管理テーブル作成対応など含まれますが、処理によって必要となるテーブル構造が異なるため今回は割愛しています。

FCMのエラーコードの扱い

トピックの購読登録/購読解除時に処理が失敗した場合には、処理に失敗したトークン毎にエラーコードが返されます。例えば、1000件の処理のうち10件がエラーだった場合、10件分のエラーを含むレスポンスが返されます。
エラーコードにはFCMサーバー側が原因以外のものがいくつか含まれています。そのようなエラーコードに関しては成功扱いとすることで、FCMサーバーへの購読登録/購読解除の送信が正常に完了しているかの判定の妨げにならないようにしました。
また、エラーコードの中にはunknown-errorのような場合にそのエラーが発生するのか分かりずらいものがありましたが、こちらはTopic機能での大量の購読登録/購読解除処理を検証していたところ、短期間で大量リクエストした場合に必ずunknown-errorが返されるという結果から判明しました。

以下エラーコードと対象コードの成功/失敗扱いの対応表となります。

エラーコード 説明 購読登録 購読解除
invalid-argument 無効な引数 失敗 成功
registration-token-not-registered トークンが登録されていない 失敗 成功
internal-error リクエストの処理中に FCM サーバーでエラーが発生 失敗 失敗
too-many-topics トピック数が上限に達している 失敗 成功
unknown-error FCMのバックエンドサーバーで失敗 失敗 失敗

FCMサーバー側エラー時のリトライ処理での考慮点

前述したエラーコードの中には、FCMサーバー側でエラーが発生したものが含まれていますが、そうしたエラーに関してはリクエストの再試行が推奨されています。
公式ドキュメントにてinternal-errorを考慮した場合、指数バックオフの最小間隔は60秒、最大間隔は60分までが推奨と記載されています。
また、internal-errorの場合はFCMからretry-afterヘッダーを含む情報が返されることがあります。その場合はretry-afterの秒数待機後にリトライ実行で問題ないとドキュメントに記載があります。
しかし、推奨どおりに指数バックオフを実装した場合、FCM側の状況次第では処理時間が長くなってしまうので最大間隔はある程度考慮する必要があります。
そこで一般的なFCM指数バックオフ処理のリトライ回数を知るためにFCMライブラリ(firebase-admin-java)を調べたところ、最大リトライ回数4回で実装されていたので今回は同じリトライ回数で実装することにしました。

一括処理をリトライするためのトランザクション制御

トピックの購読登録/購読解除/メッセージ送信の処理中に何かしらのエラーで処理が止まる可能性があります。この時、購読管理テーブルとFCMサーバー間でデータが不整合な状態になります。例えば、購読管理テーブルには未購読状態と記録されているのに、FCMサーバーでは購読状態となってしまいます。この場合、こちらが持っている購読状態が不整合なため購読解除時に購読解除漏れが発生してしまいます。
その問題を解消するため、購読登録処理の開始時点でトランザクションを開始し、購読登録処理が正常に終了した時点でトランザクションをコミットすることにしました(購読解除も同様の対応)
これにより購読管理テーブルとFCMサーバー間のデータ整合性が合う様になり、スピーディにリトライ対応ができるようなりました。

移行効果

FCM Topicのプッシュ通知に対応するために処理を見直した結果、以下の改善が見られました。

  • Batch Send APIを使用したプッシュ通知の一括送信処理では、送信件数やエラーをログファイルに記録していました。しかし、FCM Topicを使用した一括送信では、リトライ処理が必要かどうかを検知するために、購読状況やエラーをテーブルに記録する必要がありました。これにより、プッシュ通知の処理状況が以前よりも確認しやすくなりました。
  • Batch Send APIでは、メール送信処理とプッシュ通知の一括送信処理を1つのバッチ内で行っていました。しかし、FCM TopicのPublish/Subscribeメッセージングモデルを適用することで、プッシュ通知を単独のバッチ処理として切り出すことができました。

まとめ

FCMのレガシーAPI廃止に伴って、弊社で行っているプッシュ通知の大量送信の方式をFCMのTopic機能を用いて置き換えを行いました。今回の置き換えにより、一括送信の速度改善やメッセージ送信のタイムラグ改善、様々なエラーケースを想定した実装及びテストをすることで信頼性を可能な限り担保することができました。
今回は信頼性・安全性の確保をより意識しましたが、この経験を生かして今後のサービス開発でも信頼性・安全性の高いものを作っていきたいと思います。