2014-05-12

Apple Push Notification Serviceのエラー処理について

iOSアプリにPush通知をするのに利用するApple Push Notification service(APNs)について。配信数がある程度の規模になると面倒事が増えるのでまとめた。
本稿では疎結合なサービスとして稼動させるPush通知配信サーバーを考える。

Push通知配信サーバーの機能要件

個々のアプリケーションから分離したPush通知配信サーバーを考える場合、要件は大きく分けて次の二つになるだろう。

A. デバイストークンを溜め込んでおき、配信日時を指定して一斉に配信する

  • ゲームのイベントが始まった事を全ユーザーに通知したい
  • ユーザーセグメントを指定してキャンペーンの通知をしたい

B. 都度送信対象のデバイスをアプリケーションから受け取って即時配信をする

  • チャットルームで発言がある度に、チャットルームのメンバーに通知をしたい
  • ユーザー間のmentionを通知したい
Bの場合は、アプリケーションから切り出す必要は無さそうだが、都度送信する通知の数が100 ~ 10万程になるとエラーレスポンスの取得とリトライが必要になり、アプリケーションから切り離したくなる。
どちらの場合も、非機能要件としてコネクション管理とエラーハンドリングが必要となる。

エラー処理の何が難しいか

APNsのドキュメントには、次のようにエラーレスポンスの記述がある。
If you send a notification that is accepted by APNs, nothing is returned. If you send a notification that is malformed or otherwise unintelligible, APNs returns an error-response packet and closes the connection. Any notifications that you sent after the malformed notification using the same connection are discarded, and must be resent. Figure 5-2 shows the format of the error-response packet.


Local and Push Notification Programming Guide: Provider Communication with Apple Push Notification Service
これが厄介なのは
  • 不正と判断された通知以降に送信した通知が破棄される、つまり再送が必要
  • エラーレスポンスが得られるまで、0.3 ~ 0.7秒ほどかかる
  • 正常時は何も得られない、本当に正常なのかエラーレスポンスを取りこぼしたのか判断ができない
  • エラーレスポンスを確認したかにかかわらずコネクションが切断される
といった点があるからで、エラー処理をさぼると「10万ユーザーに通知を送ったつもりが、実は1,000人にしか届いていなかった」という事態が起こる。

最初の一つしかエラーレスポンスが得られない

通知のバイナリフォーマットは複数の通知を一度に送信する事ができる。が、不正な通知が複数含まれていた場合、送信後に得られるエラーレスポンスは最初の一つだけである。複数の通知をまとめて送らずに、1個づつ送信した場合も同様で
  1. デバイストークンAに送信
  2. デバイストークンBに送信 (不正)
  3. デバイストークンCに送信
  4. デバイストークンDに送信 (不正)
  5. デバイストークンEに送信
  6. Bのエラーレスポンスが得られる
  7. コネクションを切られる  (C, D, Eは無かった事に)
といった動きになる。

エラーレスポンスの種類

Status CodeDescription
0No errors encountered 
1Processing error
2Missing device token
3Missing topic
4Missing payload
5Invalid token size
6Invalid topic size
7Invalid payload size
8Invalid token
10Shutdown
255None (unknown)
Table 5-1 Codes in error-response packet
ドキュメントによると11種類であるが、仕様通りのフォーマットで送信していて遭遇するのは8と10だけである。
8のInvalid tokenは実際に送るまでそうであると判定できない曲者。開発ビルドのアプリで取得されたデバイストークンが本番環境に混入するとInvalid Tokenになる。それ以外にも発生するパターンがあるかもしれない。

2,3,4,5,6,7は送信前のチェックで回避できる。

エラーレスポンスをチェックしてリトライ処理を実装する

リトライ処理には次の実装が必要になる
  • リトライ用に送信済み通知を保持しておく
  • 送信後エラーレスポンスを確認する
  • エラーがあれば、該当通知以降を再送する
エラーレスポンスの確認はブロッキングでrecvするとパフォーマンスに影響が出るが、selectを使ってノンブロッキングでやろうにもタイミングが難しかったので自分はブロッキングで実装した。(つまりノンブロッキングSocketプログラミング力が足りない。)

確実にエラーレスポンスを補足したい場合は、通知を1つ送る度にエラーレスポンスを待つのが良いが、それではパフォーマンス要件を確実に満たせない。0.5秒待つとして1スレッドで10,000通知を送ると83分もかかってしまうからだ。自分は500件毎にチェックするようにした。犠牲になるパフォーマンスはAPNsのドキュメントにあるベストプラクティスにならって接続を複数持つ事で補完する。

ここに書いた内容の具体的な実装はVOYAGE GROUPで利用しているPush通知配信サーバーのコードで見る事ができる。こんな事もあろうかとOSS化してある。
これは都度デバイストークンを受け取って配信を行なう、前述Bの要件にあわせて作っている。

補足:パフォーマンステストのやり方

例えば10万件の通知に不正なトークンを数個仕込みつつ30秒以内に配信できるかテストしたい場合がある、テストなので配信対象デバイスは手元の数個しか無い。この時、端末は機内モード、通知は expiry=0 で送信するとAPNsが到達しなかった通知を破棄してくれるので延々とブルブルする事を回避できる。すぐに機内モードを解除してしまうと通知を受信してしまうので、1分ぐらい空けると良い。

事前にデバイストークンをスクリーニングする

Invalid Tokenは実際に送ってみるまでそうとわからない。だが、Aのデバイストークンを溜め込むタイプのサービスであれば、事前に無音通知を配信して確認ができる。この時、alert、badge、sound無しにしておかないとユーザーからは謎の通知が届いたように見えるので注意。iOSクライアントの実装もそのような通知が届いた場合は無視するようにしておく。
送信後エラーレスポンスをチェックして、Invalid Tokenであれば消す。これをすると、配信時にエラーレスポンスの確認が不要になり爆速で配信ができる。

ASPを使うか、自前でPush通知配信サーバーを運用するか

前述のPush通知配信サーバーの要件
  • A. デバイストークンを溜め込んでおき、配信日時を指定して一斉に配信する
  • B. 都度送信対象のデバイスをアプリケーションから受け取って即時配信をする

Push通知ASPを検討する

ASPといえば最近はMixpanelやGrowth Pushといった分析ツールとセットになったAのタイプが注目を浴びている様子。

Aタイプのサービスは予算と折り合いがつけば使い易い物はあるが、100万通知を1分以内に、といった速度要件を満たせるかは不明。Bの用途で使いたい場合、通知1件毎にHTTPでやりとりする物は避ける。HTTPのオーバーヘッドがでか過ぎていつまで経っても終らないという事態になる。

OSSの何かを検討する

Invalid Tokenが混ざる環境ではリトライ処理が実装されている物でないと、そのまま使えない。apns-proxy-serverの実装に着手する前に調べたが、エラーハンドリングが丁寧なプロダクトは見つからなかった。(良い奴があったら教えてください。)

まとめ

selectを使ったノンブロッキングソケットでのエラー処理は後日試す。エラー処理のワークアラウンドは次の記事にも詳しく書いてあるので、これから実装する場合は参考になるだろう。
あと、apns-proxy-client-pyもOSSとして公開しているので、フィードバックやプルリク等お待ちしております。
このエントリーをはてなブックマークに追加